From 6e9cb213625fd04cc08037288f0c5addb230b529 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 23 Sep 2026 19:21:30 +0800 Subject: [PATCH 01/31] chore: vendor v1 OpenAPI spec and generated client --- nodejs/.eslintignore | 1 + nodejs/openapi-ts.config.ts | 15 + nodejs/package.json | 7 +- nodejs/pnpm-lock.yaml | 590 +- nodejs/scripts/check-generated.mjs | 49 + nodejs/spec/hackmd-openapi.json | 6683 +++++++++++++++++ nodejs/src/generated/client.gen.ts | 16 + nodejs/src/generated/client/client.gen.ts | 161 + nodejs/src/generated/client/index.ts | 25 + nodejs/src/generated/client/types.gen.ts | 169 + nodejs/src/generated/client/utils.gen.ts | 206 + nodejs/src/generated/core/auth.gen.ts | 48 + .../src/generated/core/bodySerializer.gen.ts | 82 + nodejs/src/generated/core/params.gen.ts | 171 + .../src/generated/core/pathSerializer.gen.ts | 171 + .../generated/core/queryKeySerializer.gen.ts | 117 + .../generated/core/serverSentEvents.gen.ts | 242 + nodejs/src/generated/core/types.gen.ts | 110 + nodejs/src/generated/core/utils.gen.ts | 140 + nodejs/src/generated/index.ts | 4 + nodejs/src/generated/sdk.gen.ts | 690 ++ nodejs/src/generated/types.gen.ts | 2448 ++++++ nodejs/tsconfig.base.json | 1 + 23 files changed, 12068 insertions(+), 78 deletions(-) create mode 100644 nodejs/.eslintignore create mode 100644 nodejs/openapi-ts.config.ts create mode 100644 nodejs/scripts/check-generated.mjs create mode 100644 nodejs/spec/hackmd-openapi.json create mode 100644 nodejs/src/generated/client.gen.ts create mode 100644 nodejs/src/generated/client/client.gen.ts create mode 100644 nodejs/src/generated/client/index.ts create mode 100644 nodejs/src/generated/client/types.gen.ts create mode 100644 nodejs/src/generated/client/utils.gen.ts create mode 100644 nodejs/src/generated/core/auth.gen.ts create mode 100644 nodejs/src/generated/core/bodySerializer.gen.ts create mode 100644 nodejs/src/generated/core/params.gen.ts create mode 100644 nodejs/src/generated/core/pathSerializer.gen.ts create mode 100644 nodejs/src/generated/core/queryKeySerializer.gen.ts create mode 100644 nodejs/src/generated/core/serverSentEvents.gen.ts create mode 100644 nodejs/src/generated/core/types.gen.ts create mode 100644 nodejs/src/generated/core/utils.gen.ts create mode 100644 nodejs/src/generated/index.ts create mode 100644 nodejs/src/generated/sdk.gen.ts create mode 100644 nodejs/src/generated/types.gen.ts diff --git a/nodejs/.eslintignore b/nodejs/.eslintignore new file mode 100644 index 0000000..d3db8f9 --- /dev/null +++ b/nodejs/.eslintignore @@ -0,0 +1 @@ +src/generated/ diff --git a/nodejs/openapi-ts.config.ts b/nodejs/openapi-ts.config.ts new file mode 100644 index 0000000..50d2f30 --- /dev/null +++ b/nodejs/openapi-ts.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@hey-api/openapi-ts' + +export default defineConfig({ + input: './spec/hackmd-openapi.json', + output: { + path: './src/generated', + module: { extension: '.js' }, + postProcess: [], + }, + plugins: [ + '@hey-api/client-axios', + '@hey-api/typescript', + '@hey-api/sdk', + ], +}) diff --git a/nodejs/package.json b/nodejs/package.json index 90bf382..9e80bf3 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -54,12 +54,13 @@ "packageManager": "pnpm@10.33.2", "devDependencies": { "@faker-js/faker": "^7.6.0", + "@hey-api/openapi-ts": "0.98.2", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-typescript": "^12.1.2", "@types/eslint": "^8.21.0", "@types/jest": "^29.4.0", - "@types/node": "^13.11.1", + "@types/node": "^22.15.30", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "dotenv": "^16.0.3", @@ -68,9 +69,9 @@ "msw": "^2.7.3", "rimraf": "^4.1.2", "rollup": "^4.41.1", - "ts-jest": "^29.0.5", + "ts-jest": "^29.4.12", "ts-node": "^8.8.2", - "typescript": "^4.9.5" + "typescript": "^5.9.2" }, "dependencies": { "axios": "^1.8.4", diff --git a/nodejs/pnpm-lock.yaml b/nodejs/pnpm-lock.yaml index f011ac9..2e13549 100644 --- a/nodejs/pnpm-lock.yaml +++ b/nodejs/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@faker-js/faker': specifier: ^7.6.0 version: 7.6.0 + '@hey-api/openapi-ts': + specifier: 0.98.2 + version: 0.98.2(typescript@5.9.2) '@rollup/plugin-commonjs': specifier: ^28.0.3 version: 28.0.3(rollup@4.41.1) @@ -26,7 +29,7 @@ importers: version: 16.0.1(rollup@4.41.1) '@rollup/plugin-typescript': specifier: ^12.1.2 - version: 12.1.2(rollup@4.41.1)(tslib@1.14.1)(typescript@4.9.5) + version: 12.1.2(rollup@4.41.1)(tslib@1.14.1)(typescript@5.9.2) '@types/eslint': specifier: ^8.21.0 version: 8.21.0 @@ -34,14 +37,14 @@ importers: specifier: ^29.4.0 version: 29.4.0 '@types/node': - specifier: ^13.11.1 - version: 13.13.4 + specifier: ^22.15.30 + version: 22.15.30 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@4.9.5) + version: 6.21.0(eslint@8.57.1)(typescript@5.9.2) dotenv: specifier: ^16.0.3 version: 16.0.3 @@ -50,10 +53,10 @@ importers: version: 8.57.1 jest: specifier: ^29.4.2 - version: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)) + version: 29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)) msw: specifier: ^2.7.3 - version: 2.7.3(@types/node@13.13.4)(typescript@4.9.5) + version: 2.7.3(@types/node@22.15.30)(typescript@5.9.2) rimraf: specifier: ^4.1.2 version: 4.1.2 @@ -61,14 +64,14 @@ importers: specifier: ^4.41.1 version: 4.41.1 ts-jest: - specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.20.12)(@jest/types@29.4.2)(babel-jest@29.4.2(@babel/core@7.20.12))(jest@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)))(typescript@4.9.5) + specifier: ^29.4.12 + version: 29.4.12(@babel/core@7.20.12)(@jest/transform@29.4.2)(@jest/types@29.4.2)(babel-jest@29.4.2(@babel/core@7.20.12))(jest-util@29.4.2)(jest@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)))(typescript@5.9.2) ts-node: specifier: ^8.8.2 - version: 8.9.1(typescript@4.9.5) + version: 8.9.1(typescript@5.9.2) typescript: - specifier: ^4.9.5 - version: 4.9.5 + specifier: ^5.9.2 + version: 5.9.2 packages: @@ -262,6 +265,31 @@ packages: resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==} engines: {node: '>=14.0.0', npm: '>=6.0.0'} + '@hey-api/codegen-core@0.9.0': + resolution: {integrity: sha512-OK9/R8WuujwgvnrDIPnEiIf6WnfUOi3GaEr6kIngqoI5FUQwYbeDKHE/frTVUl2A76ZQPCrMknHtPx6Gqtwf8Q==} + engines: {node: '>=22.18.0'} + + '@hey-api/json-schema-ref-parser@1.4.3': + resolution: {integrity: sha512-UzGSDzh3QUhrnwl4atnHc2YqDO6KemYVEOwl1Ynowm/tcr0XlpdHOpyWr5UaWIJfiXTXdYRIC9k2Yxm19pcPzQ==} + engines: {node: '>=22.18.0'} + + '@hey-api/openapi-ts@0.98.2': + resolution: {integrity: sha512-2nVJXH8tpFPGTBOhxyjEd1Jw0hsRqJqeTQW3kltAjVdSU4YWxeu97x5sgNOmsbsfeg6Dqz7Wfzs26walBOuswA==} + engines: {node: '>=22.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' + + '@hey-api/shared@0.4.8': + resolution: {integrity: sha512-29Pg2FB0UW20pplYgcfiQn1hQYpbZ9D2gdDJc7nDK3xh3pvHOTGP0v3R2ueFpFnw9GN1SRhIdhiVuAYWMDimjA==} + engines: {node: '>=22.18.0'} + + '@hey-api/spec-types@0.2.0': + resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==} + + '@hey-api/types@0.1.4': + resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -402,6 +430,13 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + '@mswjs/interceptors@0.37.6': resolution: {integrity: sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==} engines: {node: '>=18'} @@ -629,6 +664,9 @@ packages: '@types/node@13.13.4': resolution: {integrity: sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==} + '@types/node@22.15.30': + resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/prettier@2.7.2': resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} @@ -728,6 +766,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -820,6 +862,18 @@ packages: buffer-from@1.1.1: resolution: {integrity: sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -847,6 +901,10 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -876,16 +934,30 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -919,10 +991,28 @@ packages: resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -947,6 +1037,10 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1046,6 +1140,9 @@ packages: resolution: {integrity: sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1144,6 +1241,13 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + giget@3.3.1: + resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1182,6 +1286,11 @@ packages: resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1242,6 +1351,11 @@ packages: is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1258,6 +1372,15 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -1279,6 +1402,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1431,6 +1558,10 @@ packages: node-notifier: optional: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1442,6 +1573,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1546,6 +1681,9 @@ packages: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -1566,6 +1704,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -1580,6 +1721,9 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1587,6 +1731,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1644,6 +1792,12 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1663,6 +1817,13 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1691,9 +1852,16 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc9@3.1.0: + resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} + react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1713,6 +1881,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.0: resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==} engines: {node: '>=10'} @@ -1741,6 +1912,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1753,6 +1928,16 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1859,26 +2044,32 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.0.5: - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + ts-jest@29.4.12: + resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' peerDependenciesMeta: '@babel/core': optional: true + '@jest/transform': + optional: true '@jest/types': optional: true babel-jest: optional: true esbuild: optional: true + jest-util: + optional: true ts-node@8.9.1: resolution: {integrity: sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==} @@ -1910,11 +2101,23 @@ packages: resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==} engines: {node: '>=16'} - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -1947,6 +2150,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -1962,6 +2168,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2218,6 +2428,56 @@ snapshots: '@faker-js/faker@7.6.0': {} + '@hey-api/codegen-core@0.9.0': + dependencies: + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + c12: 3.3.4 + color-support: 1.1.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.4.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + + '@hey-api/openapi-ts@0.98.2(typescript@5.9.2)': + dependencies: + '@hey-api/codegen-core': 0.9.0 + '@hey-api/json-schema-ref-parser': 1.4.3 + '@hey-api/shared': 0.4.8 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + '@lukeed/ms': 2.0.2 + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 15.0.0 + get-tsconfig: 4.14.0 + typescript: 5.9.2 + transitivePeerDependencies: + - magicast + + '@hey-api/shared@0.4.8': + dependencies: + '@hey-api/codegen-core': 0.9.0 + '@hey-api/json-schema-ref-parser': 1.4.3 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.8.2 + transitivePeerDependencies: + - magicast + + '@hey-api/spec-types@0.2.0': + dependencies: + '@hey-api/types': 0.1.4 + + '@hey-api/types@0.1.4': {} + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -2230,17 +2490,17 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@inquirer/confirm@5.1.8(@types/node@13.13.4)': + '@inquirer/confirm@5.1.8(@types/node@22.15.30)': dependencies: - '@inquirer/core': 10.1.9(@types/node@13.13.4) - '@inquirer/type': 3.0.5(@types/node@13.13.4) + '@inquirer/core': 10.1.9(@types/node@22.15.30) + '@inquirer/type': 3.0.5(@types/node@22.15.30) optionalDependencies: - '@types/node': 13.13.4 + '@types/node': 22.15.30 - '@inquirer/core@10.1.9(@types/node@13.13.4)': + '@inquirer/core@10.1.9(@types/node@22.15.30)': dependencies: '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@13.13.4) + '@inquirer/type': 3.0.5(@types/node@22.15.30) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -2248,13 +2508,13 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 13.13.4 + '@types/node': 22.15.30 '@inquirer/figures@1.0.11': {} - '@inquirer/type@3.0.5(@types/node@13.13.4)': + '@inquirer/type@3.0.5(@types/node@22.15.30)': optionalDependencies: - '@types/node': 13.13.4 + '@types/node': 22.15.30 '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -2275,7 +2535,7 @@ snapshots: jest-util: 29.4.2 slash: 3.0.0 - '@jest/core@29.4.2(ts-node@8.9.1(typescript@4.9.5))': + '@jest/core@29.4.2(ts-node@8.9.1(typescript@5.9.2))': dependencies: '@jest/console': 29.4.2 '@jest/reporters': 29.4.2 @@ -2289,7 +2549,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.4.2 - jest-config: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)) + jest-config: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@5.9.2)) jest-haste-map: 29.4.2 jest-message-util: 29.4.2 jest-regex-util: 29.4.2 @@ -2449,6 +2709,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsdevtools/ono@7.1.3': {} + + '@lukeed/ms@2.0.2': {} + '@mswjs/interceptors@0.37.6': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -2501,11 +2765,11 @@ snapshots: optionalDependencies: rollup: 4.41.1 - '@rollup/plugin-typescript@12.1.2(rollup@4.41.1)(tslib@1.14.1)(typescript@4.9.5)': + '@rollup/plugin-typescript@12.1.2(rollup@4.41.1)(tslib@1.14.1)(typescript@5.9.2)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.41.1) resolve: 1.22.1 - typescript: 4.9.5 + typescript: 5.9.2 optionalDependencies: rollup: 4.41.1 tslib: 1.14.1 @@ -2641,6 +2905,10 @@ snapshots: '@types/node@13.13.4': {} + '@types/node@22.15.30': + dependencies: + undici-types: 6.21.0 + '@types/prettier@2.7.2': {} '@types/resolve@1.20.2': {} @@ -2659,13 +2927,13 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.0 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.1 @@ -2673,22 +2941,22 @@ snapshots: ignore: 5.3.2 natural-compare: 1.4.0 semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@4.9.5) + ts-api-utils: 1.4.3(typescript@5.9.2) optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.1 optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -2697,21 +2965,21 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.2)': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.2) debug: 4.3.4 eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@4.9.5) + ts-api-utils: 1.4.3(typescript@5.9.2) optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.2 transitivePeerDependencies: - supports-color '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@4.9.5)': + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 @@ -2720,20 +2988,20 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@4.9.5) + ts-api-utils: 1.4.3(typescript@5.9.2) optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) '@types/json-schema': 7.0.15 '@types/semver': 7.7.0 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.2) eslint: 8.57.1 semver: 7.7.1 transitivePeerDependencies: @@ -2760,6 +3028,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -2881,6 +3151,25 @@ snapshots: buffer-from@1.1.1: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.1 + giget: 3.3.1 + jiti: 2.7.0 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.3 + rc9: 3.1.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2901,6 +3190,10 @@ snapshots: char-regex@1.0.2: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + ci-info@3.8.0: {} cjs-module-lexer@1.2.2: {} @@ -2923,14 +3216,22 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + commander@15.0.0: {} + commondir@1.0.1: {} concat-map@0.0.1: {} + confbox@0.2.4: {} + + confbox@0.3.1: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -2953,8 +3254,21 @@ snapshots: deepmerge@4.3.0: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + delayed-stream@1.0.0: {} + destr@2.0.5: {} + detect-newline@3.1.0: {} diff-sequences@29.4.2: {} @@ -2971,6 +3285,8 @@ snapshots: dotenv@16.0.3: {} + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3102,6 +3418,8 @@ snapshots: jest-message-util: 29.4.2 jest-util: 29.4.2 + exsolve@1.1.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3196,6 +3514,12 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@3.3.1: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3236,6 +3560,15 @@ snapshots: graphql@16.10.0: {} + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3285,6 +3618,8 @@ snapshots: dependencies: has: 1.0.3 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -3295,6 +3630,12 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-module@1.0.0: {} is-node-process@1.2.0: {} @@ -3309,6 +3650,10 @@ snapshots: is-stream@2.0.1: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} istanbul-lib-coverage@3.2.0: {} @@ -3371,16 +3716,16 @@ snapshots: transitivePeerDependencies: - supports-color - jest-cli@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)): + jest-cli@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)): dependencies: - '@jest/core': 29.4.2(ts-node@8.9.1(typescript@4.9.5)) + '@jest/core': 29.4.2(ts-node@8.9.1(typescript@5.9.2)) '@jest/test-result': 29.4.2 '@jest/types': 29.4.2 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 import-local: 3.1.0 - jest-config: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)) + jest-config: 29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)) jest-util: 29.4.2 jest-validate: 29.4.2 prompts: 2.4.2 @@ -3390,7 +3735,7 @@ snapshots: - supports-color - ts-node - jest-config@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)): + jest-config@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@5.9.2)): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.4.2 @@ -3416,7 +3761,37 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 13.13.4 - ts-node: 8.9.1(typescript@4.9.5) + ts-node: 8.9.1(typescript@5.9.2) + transitivePeerDependencies: + - supports-color + + jest-config@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)): + dependencies: + '@babel/core': 7.20.12 + '@jest/test-sequencer': 29.4.2 + '@jest/types': 29.4.2 + babel-jest: 29.4.2(@babel/core@7.20.12) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.0 + glob: 7.2.0 + graceful-fs: 4.2.10 + jest-circus: 29.4.2 + jest-environment-node: 29.4.2 + jest-get-type: 29.4.2 + jest-regex-util: 29.4.2 + jest-resolve: 29.4.2 + jest-runner: 29.4.2 + jest-util: 29.4.2 + jest-validate: 29.4.2 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.4.2 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.15.30 + ts-node: 8.9.1(typescript@5.9.2) transitivePeerDependencies: - supports-color @@ -3640,17 +4015,19 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)): + jest@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)): dependencies: - '@jest/core': 29.4.2(ts-node@8.9.1(typescript@4.9.5)) + '@jest/core': 29.4.2(ts-node@8.9.1(typescript@5.9.2)) '@jest/types': 29.4.2 import-local: 3.1.0 - jest-cli: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)) + jest-cli: 29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)) transitivePeerDependencies: - '@types/node' - supports-color - ts-node + jiti@2.7.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -3662,6 +4039,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -3746,14 +4127,16 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.8: {} + ms@2.1.2: {} - msw@2.7.3(@types/node@13.13.4)(typescript@4.9.5): + msw@2.7.3(@types/node@22.15.30)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.8(@types/node@13.13.4) + '@inquirer/confirm': 5.1.8(@types/node@22.15.30) '@mswjs/interceptors': 0.37.6 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 @@ -3769,7 +4152,7 @@ snapshots: type-fest: 4.38.0 yargs: 17.7.2 optionalDependencies: - typescript: 4.9.5 + typescript: 5.9.2 transitivePeerDependencies: - '@types/node' @@ -3777,6 +4160,8 @@ snapshots: natural-compare@1.4.0: {} + neo-async@2.6.2: {} + node-int64@0.4.0: {} node-releases@2.0.10: {} @@ -3787,6 +4172,8 @@ snapshots: dependencies: path-key: 3.1.1 + ohash@2.0.12: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3795,6 +4182,15 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@11.0.0: + dependencies: + default-browser: 5.5.1 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3847,6 +4243,10 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3859,6 +4259,14 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@2.3.3: + dependencies: + confbox: 0.3.1 + exsolve: 1.1.1 + pathe: 2.0.3 + + powershell-utils@0.1.0: {} + prelude-ls@1.2.1: {} pretty-format@29.4.2: @@ -3884,8 +4292,15 @@ snapshots: queue-microtask@1.2.3: {} + rc9@3.1.0: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + react-is@18.2.0: {} + readdirp@5.1.1: {} + require-directory@2.1.1: {} requires-port@1.0.0: {} @@ -3898,6 +4313,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.0: {} resolve@1.22.1: @@ -3940,6 +4357,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.41.1 fsevents: 2.3.2 + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3948,6 +4367,10 @@ snapshots: semver@7.7.1: {} + semver@7.8.2: {} + + semver@7.8.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4036,34 +4459,37 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 - ts-api-utils@1.4.3(typescript@4.9.5): + ts-api-utils@1.4.3(typescript@5.9.2): dependencies: - typescript: 4.9.5 + typescript: 5.9.2 - ts-jest@29.0.5(@babel/core@7.20.12)(@jest/types@29.4.2)(babel-jest@29.4.2(@babel/core@7.20.12))(jest@29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)))(typescript@4.9.5): + ts-jest@29.4.12(@babel/core@7.20.12)(@jest/transform@29.4.2)(@jest/types@29.4.2)(babel-jest@29.4.2(@babel/core@7.20.12))(jest-util@29.4.2)(jest@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)))(typescript@5.9.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.4.2(@types/node@13.13.4)(ts-node@8.9.1(typescript@4.9.5)) - jest-util: 29.4.2 + handlebars: 4.7.9 + jest: 29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.1 - typescript: 4.9.5 + semver: 7.8.5 + type-fest: 4.41.0 + typescript: 5.9.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.20.12 + '@jest/transform': 29.4.2 '@jest/types': 29.4.2 babel-jest: 29.4.2(@babel/core@7.20.12) + jest-util: 29.4.2 - ts-node@8.9.1(typescript@4.9.5): + ts-node@8.9.1(typescript@5.9.2): dependencies: arg: 4.1.3 diff: 4.0.2 make-error: 1.3.6 source-map-support: 0.5.19 - typescript: 4.9.5 + typescript: 5.9.2 yn: 3.1.1 tslib@1.14.1: {} @@ -4080,7 +4506,14 @@ snapshots: type-fest@4.38.0: {} - typescript@4.9.5: {} + type-fest@4.41.0: {} + + typescript@5.9.2: {} + + uglify-js@3.19.3: + optional: true + + undici-types@6.21.0: {} universalify@0.2.0: {} @@ -4115,6 +4548,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -4134,6 +4569,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/nodejs/scripts/check-generated.mjs b/nodejs/scripts/check-generated.mjs new file mode 100644 index 0000000..d49a85a --- /dev/null +++ b/nodejs/scripts/check-generated.mjs @@ -0,0 +1,49 @@ +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { readdir, readFile } from 'node:fs/promises' +import { join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageRoot = fileURLToPath(new URL('../', import.meta.url)) +const generatedRoot = fileURLToPath(new URL('../src/generated/', import.meta.url)) + +async function filesIn (directory) { + const files = [] + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) files.push(...await filesIn(path)) + else files.push(path) + } + return files.sort() +} + +async function manifest () { + const result = new Map() + for (const path of await filesIn(generatedRoot)) { + const content = await readFile(path) + result.set( + relative(generatedRoot, path), + createHash('sha256').update(content).digest('hex'), + ) + } + return result +} + +const before = await manifest() +const generation = spawnSync('pnpm', ['run', 'generate'], { + cwd: packageRoot, + stdio: 'inherit', +}) +if (generation.error) throw generation.error +if (generation.status !== 0) process.exit(generation.status ?? 1) + +const after = await manifest() +const changed = [...new Set([...before.keys(), ...after.keys()])] + .filter(path => before.get(path) !== after.get(path)) + +if (changed.length > 0) { + console.error(`Generated files changed: ${changed.join(', ')}`) + process.exitCode = 1 +} else { + console.log('Generated files match the vendored OpenAPI spec.') +} diff --git a/nodejs/spec/hackmd-openapi.json b/nodejs/spec/hackmd-openapi.json new file mode 100644 index 0000000..ae1b2bf --- /dev/null +++ b/nodejs/spec/hackmd-openapi.json @@ -0,0 +1,6683 @@ +{ + "openapi": "3.0.0", + "components": { + "examples": {}, + "headers": {}, + "parameters": {}, + "requestBodies": {}, + "responses": {}, + "schemas": { + "ApiWebhookScope": { + "anyOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": [ + "workspace" + ], + "nullable": false + } + }, + "required": [ + "type" + ], + "type": "object" + }, + { + "properties": { + "folderId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "folder" + ], + "nullable": false + } + }, + "required": [ + "folderId", + "type" + ], + "type": "object" + } + ] + }, + "ApiWebhook": { + "properties": { + "updatedAt": { + "type": "number", + "format": "double" + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "eventDeliveryMode": { + "type": "string", + "enum": [ + "all_supported_events" + ], + "nullable": false + }, + "deliveryPolicyReason": { + "type": "string", + "enum": [ + "unsafe_destination", + "suspicious_pattern", + "policy_violation", + null + ], + "nullable": true + }, + "deliveryPolicyStatus": { + "type": "string", + "enum": [ + "normal", + "suppressed" + ] + }, + "configuredStatusReason": { + "type": "string", + "enum": [ + "scope_deleted", + null + ], + "nullable": true + }, + "configuredStatus": { + "type": "string", + "enum": [ + "active", + "disabled" + ] + }, + "scope": { + "$ref": "#/components/schemas/ApiWebhookScope" + }, + "url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "updatedAt", + "createdAt", + "eventDeliveryMode", + "deliveryPolicyReason", + "deliveryPolicyStatus", + "configuredStatusReason", + "configuredStatus", + "scope", + "url", + "name", + "id" + ], + "type": "object" + }, + "WebhookApiErrorResponse": { + "properties": { + "error": { + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "message", + "code" + ], + "type": "object" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "ApiValidationFieldError": { + "properties": { + "message": { + "type": "string" + }, + "value": {} + }, + "required": [ + "message" + ], + "type": "object", + "additionalProperties": false + }, + "Record_string.ApiValidationFieldError_": { + "properties": {}, + "additionalProperties": { + "$ref": "#/components/schemas/ApiValidationFieldError" + }, + "type": "object", + "description": "Construct a type with a set of properties K of type T" + }, + "ApiValidationError": { + "properties": { + "message": { + "type": "string", + "enum": [ + "Validation Failed" + ], + "nullable": false + }, + "details": { + "$ref": "#/components/schemas/Record_string.ApiValidationFieldError_" + } + }, + "required": [ + "message", + "details" + ], + "type": "object", + "additionalProperties": false + }, + "CreateWebhookData": { + "properties": { + "scope": { + "anyOf": [ + { + "properties": { + "type": { + "type": "string", + "enum": [ + "workspace" + ], + "nullable": false + } + }, + "required": [ + "type" + ], + "type": "object" + }, + { + "properties": { + "folderId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "folder" + ], + "nullable": false + } + }, + "required": [ + "folderId", + "type" + ], + "type": "object" + } + ] + }, + "url": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true, + "maxLength": 255 + } + }, + "required": [ + "scope", + "url" + ], + "type": "object" + }, + "CreateApiWebhookBody": { + "allOf": [ + { + "$ref": "#/components/schemas/CreateWebhookData" + }, + { + "properties": { + "active": { + "type": "boolean" + } + }, + "type": "object" + } + ] + }, + "UpdateWebhookData": { + "properties": { + "active": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true, + "maxLength": 255 + } + }, + "type": "object" + }, + "UpdateApiWebhookBody": { + "$ref": "#/components/schemas/UpdateWebhookData" + }, + "WebhookDeliveryStatusCategory": { + "type": "string", + "enum": [ + "delivered", + "delivery_issue", + "not_delivered" + ] + }, + "ApiWebhookDelivery": { + "properties": { + "deliveredAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "latencyMs": { + "type": "number", + "format": "double", + "nullable": true + }, + "errorMessage": { + "type": "string", + "nullable": true + }, + "errorCategory": { + "type": "string", + "nullable": true + }, + "httpStatus": { + "type": "number", + "format": "double", + "nullable": true + }, + "statusCategory": { + "$ref": "#/components/schemas/WebhookDeliveryStatusCategory" + }, + "status": { + "type": "string", + "enum": [ + "success", + "failed", + "skipped" + ] + }, + "attemptKind": { + "type": "string", + "enum": [ + "event", + "ping" + ] + }, + "eventType": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "deliveredAt", + "createdAt", + "latencyMs", + "errorMessage", + "errorCategory", + "httpStatus", + "statusCategory", + "status", + "attemptKind", + "eventType", + "id" + ], + "type": "object" + }, + "PaginatedQuery": { + "properties": { + "limit": { + "type": "number", + "format": "double" + }, + "page": { + "type": "number", + "format": "double" + } + }, + "required": [ + "limit", + "page" + ], + "type": "object", + "additionalProperties": false + }, + "PaginatedResponse_ApiWebhookDelivery_": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ApiWebhookDelivery" + }, + "type": "array" + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/PaginatedQuery" + }, + { + "properties": { + "totalPages": { + "type": "number", + "format": "double" + }, + "total": { + "type": "number", + "format": "double" + } + }, + "type": "object" + } + ] + } + }, + "required": [ + "data", + "meta" + ], + "type": "object", + "additionalProperties": false + }, + "ApiWebhookPage": { + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1 + }, + "ApiWebhookLimit": { + "type": "integer", + "format": "int32", + "default": 20, + "minimum": 1, + "maximum": 100 + }, + "SimpleUserProfile": { + "properties": { + "name": { + "type": "string" + }, + "userPath": { + "type": "string" + }, + "photo": { + "type": "string" + }, + "biography": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name", + "userPath", + "photo", + "biography" + ], + "type": "object", + "additionalProperties": false + }, + "NotePublishType": { + "enum": [ + "edit", + "view", + "slide", + "book" + ], + "type": "string" + }, + "NotePermissionRole": { + "enum": [ + "owner", + "signed_in", + "guest" + ], + "type": "string" + }, + "FolderPath": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true + }, + "parentId": { + "type": "string", + "nullable": true + }, + "clientId": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "icon", + "color", + "parentId", + "clientId" + ], + "type": "object", + "additionalProperties": false + }, + "NoteType": { + "properties": { + "folderPaths": { + "items": { + "$ref": "#/components/schemas/FolderPath" + }, + "type": "array" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "shortId": { + "type": "string" + }, + "publishLink": { + "type": "string" + }, + "permalink": { + "type": "string", + "nullable": true + }, + "teamPath": { + "type": "string", + "nullable": true + }, + "userPath": { + "type": "string", + "nullable": true + }, + "publishedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "publishType": { + "$ref": "#/components/schemas/NotePublishType" + }, + "lastChangeUser": { + "allOf": [ + { + "$ref": "#/components/schemas/SimpleUserProfile" + } + ], + "nullable": true + }, + "tagsUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "titleUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "lastChangedAt": { + "type": "number", + "format": "double" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "writePermission", + "readPermission", + "shortId", + "publishLink", + "permalink", + "teamPath", + "userPath", + "publishedAt", + "publishType", + "lastChangeUser", + "tagsUpdatedAt", + "titleUpdatedAt", + "createdAt", + "lastChangedAt", + "description", + "tags", + "title", + "id" + ], + "type": "object" + }, + "SingleNote": { + "properties": { + "folderPaths": { + "items": { + "$ref": "#/components/schemas/FolderPath" + }, + "type": "array" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "shortId": { + "type": "string" + }, + "publishLink": { + "type": "string" + }, + "permalink": { + "type": "string", + "nullable": true + }, + "teamPath": { + "type": "string", + "nullable": true + }, + "userPath": { + "type": "string", + "nullable": true + }, + "publishedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "publishType": { + "$ref": "#/components/schemas/NotePublishType" + }, + "lastChangeUser": { + "allOf": [ + { + "$ref": "#/components/schemas/SimpleUserProfile" + } + ], + "nullable": true + }, + "tagsUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "titleUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "lastChangedAt": { + "type": "number", + "format": "double" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "required": [ + "writePermission", + "readPermission", + "shortId", + "publishLink", + "permalink", + "teamPath", + "userPath", + "publishedAt", + "publishType", + "lastChangeUser", + "tagsUpdatedAt", + "titleUpdatedAt", + "createdAt", + "lastChangedAt", + "description", + "tags", + "title", + "id", + "content" + ], + "type": "object", + "additionalProperties": false + }, + "ApiErrorResponse": { + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object", + "additionalProperties": false + }, + "CreateNoteMultiStatusResponse": { + "properties": { + "note": { + "$ref": "#/components/schemas/SingleNote" + }, + "error": { + "type": "string" + } + }, + "required": [ + "note", + "error" + ], + "type": "object", + "additionalProperties": false + }, + "ApiRequestErrorResponse": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object", + "additionalProperties": false + }, + "ApiBadRequestResponse": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiErrorResponse" + }, + { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + ] + }, + "CommentPermissionType": { + "enum": [ + "disabled", + "forbidden", + "owners", + "signed_in_users", + "everyone" + ], + "type": "string" + }, + "SuggestEditPermissionType": { + "enum": [ + "disabled", + "forbidden", + "owners", + "signed_in_users" + ], + "type": "string" + }, + "ApiNoteFeatures": { + "properties": { + "emoji-reply": { + "type": "string", + "enum": [ + "disabled", + "signed_in_users" + ] + }, + "citation": { + "type": "string", + "enum": [ + "disabled", + "signed_in_users" + ] + } + }, + "type": "object", + "additionalProperties": false + }, + "FolderName": { + "type": "string", + "example": "Engineering", + "description": "Folder name." + }, + "FolderIconCodepoint": { + "type": "string", + "example": "1F525", + "description": "Folder icon as an emoji unified codepoint string.\nExamples: `1F525` (🔥), `2764-FE0F` (❤️)." + }, + "FolderColorHex": { + "type": "string", + "example": "#FF6B6B", + "description": "Folder display color in hexadecimal format.\nAccepts `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA`." + }, + "ApiFolder": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/FolderName" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderIconCodepoint" + } + ], + "nullable": true + }, + "color": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderColorHex" + } + ], + "nullable": true + }, + "parentFolderId": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "updatedAt": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "name", + "description", + "icon", + "color", + "parentFolderId", + "createdAt", + "updatedAt" + ], + "type": "object", + "additionalProperties": false + }, + "ApiFolderOrder": { + "description": "Per-user display order of folders: maps each parent folder id (or `root`) to\nan ordered list of child folder ids. Keys and values use the same UUIDs as\n{@link ApiFolder.id} (not Yjs clientIds stored internally in preferences).", + "properties": {}, + "type": "object", + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "UpdateFolderOrderBody": { + "properties": { + "order": { + "$ref": "#/components/schemas/ApiFolderOrder" + } + }, + "required": [ + "order" + ], + "type": "object", + "additionalProperties": false + }, + "CreateUserFolderBody": { + "properties": { + "color": { + "$ref": "#/components/schemas/FolderColorHex" + }, + "icon": { + "$ref": "#/components/schemas/FolderIconCodepoint" + }, + "description": { + "type": "string" + }, + "parentFolderId": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/FolderName" + } + }, + "type": "object" + }, + "UpdateUserFolderBody": { + "properties": { + "parentFolderId": { + "type": "string", + "nullable": true + }, + "color": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderColorHex" + } + ], + "nullable": true + }, + "icon": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderIconCodepoint" + } + ], + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "name": { + "$ref": "#/components/schemas/FolderName" + } + }, + "type": "object" + }, + "ApiTrashNote": { + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "deletedAt": { + "type": "number", + "format": "double" + }, + "ownerId": { + "type": "string" + }, + "teamId": { + "type": "string", + "nullable": true + }, + "shortId": { + "type": "string" + }, + "publishType": { + "type": "string" + }, + "permanentDeletedAt": { + "type": "number", + "format": "double", + "nullable": true + } + }, + "required": [ + "id", + "title", + "tags", + "deletedAt", + "ownerId", + "teamId", + "shortId", + "publishType", + "permanentDeletedAt" + ], + "type": "object", + "additionalProperties": false + }, + "BatchRestoreTrashBody": { + "properties": { + "noteIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "noteIds" + ], + "type": "object", + "additionalProperties": false + }, + "BatchOperationError": { + "enum": [ + "NOT_FOUND", + "PERMISSION_DENIED", + "INTERNAL_ERROR" + ], + "type": "string" + }, + "TrashBatchOperationResponse": { + "properties": {}, + "additionalProperties": { + "properties": { + "reason": { + "$ref": "#/components/schemas/BatchOperationError" + }, + "status": { + "type": "string", + "enum": [ + "success", + "failure" + ] + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "type": "object" + }, + "TeamVisibilityType": { + "enum": [ + "public", + "private" + ], + "type": "string" + }, + "Team": { + "properties": { + "id": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "path": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "visibility": { + "$ref": "#/components/schemas/TeamVisibilityType" + }, + "upgraded": { + "type": "boolean" + }, + "createdAt": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "ownerId", + "name", + "logo", + "path", + "description", + "visibility", + "upgraded", + "createdAt" + ], + "type": "object", + "additionalProperties": false + }, + "CreateTeamFolderBody": { + "properties": { + "color": { + "$ref": "#/components/schemas/FolderColorHex" + }, + "icon": { + "$ref": "#/components/schemas/FolderIconCodepoint" + }, + "description": { + "type": "string" + }, + "parentFolderId": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/FolderName" + } + }, + "type": "object" + }, + "UpdateTeamFolderBody": { + "properties": { + "parentFolderId": { + "type": "string", + "nullable": true + }, + "color": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderColorHex" + } + ], + "nullable": true + }, + "icon": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderIconCodepoint" + } + ], + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "name": { + "$ref": "#/components/schemas/FolderName" + } + }, + "type": "object" + }, + "NoteVersionUser": { + "properties": { + "user_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "photo": { + "type": "string" + } + }, + "required": [ + "user_id", + "display_name" + ], + "type": "object", + "additionalProperties": false + }, + "NoteVersionMetadata": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "name_source": { + "type": "string", + "enum": [ + "user", + "system" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "created_by": { + "allOf": [ + { + "$ref": "#/components/schemas/NoteVersionUser" + } + ], + "nullable": true + }, + "authorship": { + "items": { + "$ref": "#/components/schemas/NoteVersionUser" + }, + "type": "array" + }, + "content_available": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "name_source", + "created_at", + "created_by", + "authorship", + "content_available" + ], + "type": "object", + "additionalProperties": false + }, + "GetNoteVersions": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/NoteVersionMetadata" + }, + "type": "array" + }, + "meta": { + "properties": { + "total_pages": { + "type": "number", + "format": "double" + }, + "total": { + "type": "number", + "format": "double" + }, + "limit": { + "type": "number", + "format": "double" + }, + "page": { + "type": "number", + "format": "double" + } + }, + "required": [ + "total_pages", + "total", + "limit", + "page" + ], + "type": "object" + } + }, + "required": [ + "data", + "meta" + ], + "type": "object", + "additionalProperties": false + }, + "ApiError": { + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "unavailable_ref": { + "type": "string" + } + }, + "required": [ + "error_code", + "message" + ], + "type": "object", + "additionalProperties": false + }, + "CompareNoteVersions": { + "properties": { + "unified_diff": { + "type": "string" + } + }, + "required": [ + "unified_diff" + ], + "type": "object", + "additionalProperties": false + }, + "NoteVersion": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "name_source": { + "type": "string", + "enum": [ + "user", + "system" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "created_by": { + "allOf": [ + { + "$ref": "#/components/schemas/NoteVersionUser" + } + ], + "nullable": true + }, + "authorship": { + "items": { + "$ref": "#/components/schemas/NoteVersionUser" + }, + "type": "array" + }, + "content_available": { + "type": "boolean" + }, + "content": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "name_source", + "created_at", + "created_by", + "authorship", + "content_available" + ], + "type": "object", + "additionalProperties": false + }, + "NoContentChangeError": { + "properties": { + "error_code": { + "type": "string", + "enum": [ + "no_content_change" + ], + "nullable": false + }, + "message": { + "type": "string" + }, + "latest_version_id": { + "type": "string" + }, + "latest_name_source": { + "type": "string", + "enum": [ + "system", + "user" + ] + } + }, + "required": [ + "error_code", + "message", + "latest_version_id", + "latest_name_source" + ], + "type": "object", + "additionalProperties": false + }, + "VersionAlreadyNamedError": { + "properties": { + "error_code": { + "type": "string", + "enum": [ + "version_already_named" + ], + "nullable": false + }, + "message": { + "type": "string" + }, + "version_id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "error_code", + "message", + "version_id", + "name" + ], + "type": "object", + "additionalProperties": false + }, + "CreateNoteVersionConflictError": { + "anyOf": [ + { + "$ref": "#/components/schemas/NoContentChangeError" + }, + { + "$ref": "#/components/schemas/VersionAlreadyNamedError" + } + ] + }, + "CreateNoteVersionBody": { + "properties": { + "base": { + "type": "string", + "description": "Saved content to name. Accepts note_content or version:{id}.", + "default": "note_content" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "pattern": "\\S" + }, + "description": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "name" + ], + "type": "object", + "additionalProperties": false + }, + "VersionNotNamedError": { + "properties": { + "error_code": { + "type": "string", + "enum": [ + "version_not_named" + ], + "nullable": false + }, + "message": { + "type": "string" + }, + "version_id": { + "type": "string" + } + }, + "required": [ + "error_code", + "message", + "version_id" + ], + "type": "object", + "additionalProperties": false + }, + "UpdateNoteVersionBody": { + "anyOf": [ + { + "properties": { + "description": { + "type": "string", + "maxLength": 500 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "pattern": "\\S" + }, + "base": { + "type": "string", + "description": "Existing named version to update, formatted as version:{id}." + } + }, + "required": [ + "name", + "base" + ], + "type": "object" + }, + { + "properties": { + "description": { + "type": "string", + "maxLength": 500 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "pattern": "\\S" + }, + "base": { + "type": "string", + "description": "Existing named version to update, formatted as version:{id}." + } + }, + "required": [ + "description", + "base" + ], + "type": "object" + } + ] + }, + "NoteImageUploadResponse": { + "properties": { + "data": { + "properties": { + "link": { + "type": "string" + } + }, + "required": [ + "link" + ], + "type": "object" + } + }, + "required": [ + "data" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentStatus": { + "type": "string", + "enum": [ + "open", + "hidden" + ] + }, + "ApiCommentStateReason": { + "type": "string", + "enum": [ + "resolved", + "spam", + "abuse", + "off_topic", + null + ], + "nullable": true + }, + "ApiCommentStateUserActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "user" + ], + "nullable": false + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "kind", + "userId", + "displayName" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentStateDeletedUserActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "deleted_user" + ], + "nullable": false + } + }, + "required": [ + "kind" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentStateActor": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiCommentStateUserActor" + }, + { + "$ref": "#/components/schemas/ApiCommentStateDeletedUserActor" + } + ] + }, + "ApiCommentThreadState": { + "properties": { + "status": { + "$ref": "#/components/schemas/ApiCommentStatus" + }, + "reason": { + "$ref": "#/components/schemas/ApiCommentStateReason" + }, + "actedBy": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentStateActor" + } + ], + "nullable": true + }, + "actedAt": { + "type": "number", + "format": "double", + "nullable": true, + "description": "Unix timestamp in milliseconds." + }, + "sourceCommentId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "status", + "reason", + "actedBy", + "actedAt", + "sourceCommentId" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentUserActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "user" + ], + "nullable": false + }, + "userId": { + "type": "string", + "format": "uuid" + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "kind", + "userId", + "displayName" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentGuestActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "guest" + ], + "nullable": false + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "kind", + "displayName" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentTeamActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "team" + ], + "nullable": false + }, + "teamId": { + "type": "string", + "format": "uuid" + }, + "displayName": { + "type": "string" + } + }, + "required": [ + "kind", + "teamId", + "displayName" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentDeletedUserActor": { + "properties": { + "kind": { + "type": "string", + "enum": [ + "deleted_user" + ], + "nullable": false + } + }, + "required": [ + "kind" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentActor": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiCommentUserActor" + }, + { + "$ref": "#/components/schemas/ApiCommentGuestActor" + }, + { + "$ref": "#/components/schemas/ApiCommentTeamActor" + }, + { + "$ref": "#/components/schemas/ApiCommentDeletedUserActor" + } + ] + }, + "ApiCommentState": { + "properties": { + "status": { + "$ref": "#/components/schemas/ApiCommentStatus" + }, + "reason": { + "$ref": "#/components/schemas/ApiCommentStateReason" + }, + "actedBy": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentStateActor" + } + ], + "nullable": true + }, + "actedAt": { + "type": "number", + "format": "double", + "nullable": true, + "description": "Unix timestamp in milliseconds." + } + }, + "required": [ + "status", + "reason", + "actedBy", + "actedAt" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentAnchor": { + "properties": { + "offset": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "length": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + "required": [ + "offset", + "length" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentReactionShortcode": { + "type": "string", + "enum": [ + "+1", + "-1", + "joy", + "tada", + "thinking_face", + "heart", + "rocket", + "eyes" + ] + }, + "ApiCommentReactionSummary": { + "properties": { + "shortcode": { + "$ref": "#/components/schemas/ApiCommentReactionShortcode" + }, + "count": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + }, + "required": [ + "shortcode", + "count" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentHead": { + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "threadId": { + "type": "string", + "format": "uuid" + }, + "actor": { + "$ref": "#/components/schemas/ApiCommentActor" + }, + "body": { + "type": "string" + }, + "contentType": { + "type": "string", + "enum": [ + "plain_text" + ], + "nullable": false + }, + "commentState": { + "$ref": "#/components/schemas/ApiCommentState" + }, + "isPinned": { + "type": "boolean" + }, + "anchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "currentAnchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "excerpt": { + "type": "string", + "nullable": true + }, + "reactions": { + "items": { + "$ref": "#/components/schemas/ApiCommentReactionSummary" + }, + "type": "array" + }, + "createdAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "updatedAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "isThreadHead": { + "type": "boolean", + "enum": [ + true + ], + "nullable": false + }, + "replyCount": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "threadState": { + "$ref": "#/components/schemas/ApiCommentThreadState" + } + }, + "required": [ + "id", + "threadId", + "actor", + "body", + "contentType", + "commentState", + "isPinned", + "anchor", + "currentAnchor", + "excerpt", + "reactions", + "createdAt", + "updatedAt", + "isThreadHead", + "replyCount", + "threadState" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentListReply": { + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "threadId": { + "type": "string", + "format": "uuid" + }, + "actor": { + "$ref": "#/components/schemas/ApiCommentActor" + }, + "body": { + "type": "string" + }, + "contentType": { + "type": "string", + "enum": [ + "plain_text" + ], + "nullable": false + }, + "commentState": { + "$ref": "#/components/schemas/ApiCommentState" + }, + "isPinned": { + "type": "boolean" + }, + "anchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "currentAnchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "excerpt": { + "type": "string", + "nullable": true + }, + "reactions": { + "items": { + "$ref": "#/components/schemas/ApiCommentReactionSummary" + }, + "type": "array" + }, + "createdAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "updatedAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "isThreadHead": { + "type": "boolean", + "enum": [ + false + ], + "nullable": false + } + }, + "required": [ + "id", + "threadId", + "actor", + "body", + "contentType", + "commentState", + "isPinned", + "anchor", + "currentAnchor", + "excerpt", + "reactions", + "createdAt", + "updatedAt", + "isThreadHead" + ], + "type": "object", + "additionalProperties": false + }, + "ApiComment": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiCommentHead" + }, + { + "$ref": "#/components/schemas/ApiCommentListReply" + } + ] + }, + "ApiCommentPagination": { + "properties": { + "page": { + "type": "integer", + "format": "int32", + "minimum": 1 + }, + "limit": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 100 + }, + "hasNext": { + "type": "boolean" + } + }, + "required": [ + "page", + "limit", + "hasNext" + ], + "type": "object", + "additionalProperties": false + }, + "GetNoteComments": { + "properties": { + "comments": { + "items": { + "$ref": "#/components/schemas/ApiComment" + }, + "type": "array" + }, + "pagination": { + "$ref": "#/components/schemas/ApiCommentPagination" + } + }, + "required": [ + "comments", + "pagination" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentError": { + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "errorCode", + "message" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentPage": { + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1 + }, + "ApiCommentLimit": { + "type": "integer", + "format": "int32", + "default": 50, + "minimum": 1, + "maximum": 100 + }, + "ApiCommentThreadId": { + "type": "string", + "format": "uuid" + }, + "ApiCommentDetailReply": { + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "threadId": { + "type": "string", + "format": "uuid" + }, + "actor": { + "$ref": "#/components/schemas/ApiCommentActor" + }, + "body": { + "type": "string" + }, + "contentType": { + "type": "string", + "enum": [ + "plain_text" + ], + "nullable": false + }, + "commentState": { + "$ref": "#/components/schemas/ApiCommentState" + }, + "isPinned": { + "type": "boolean" + }, + "anchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "currentAnchor": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentAnchor" + } + ], + "nullable": true + }, + "excerpt": { + "type": "string", + "nullable": true + }, + "reactions": { + "items": { + "$ref": "#/components/schemas/ApiCommentReactionSummary" + }, + "type": "array" + }, + "createdAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "updatedAt": { + "type": "number", + "format": "double", + "description": "Unix timestamp in milliseconds." + }, + "isThreadHead": { + "type": "boolean", + "enum": [ + false + ], + "nullable": false + }, + "threadState": { + "$ref": "#/components/schemas/ApiCommentThreadState" + } + }, + "required": [ + "id", + "threadId", + "actor", + "body", + "contentType", + "commentState", + "isPinned", + "anchor", + "currentAnchor", + "excerpt", + "reactions", + "createdAt", + "updatedAt", + "isThreadHead", + "threadState" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentDetail": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiCommentHead" + }, + { + "$ref": "#/components/schemas/ApiCommentDetailReply" + } + ] + }, + "ApiCommentId": { + "type": "string", + "format": "uuid" + }, + "ApiCommentResolutionCascade": { + "properties": { + "threadId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "threadId" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentResolutionResponse": { + "properties": { + "comment": { + "$ref": "#/components/schemas/ApiCommentDetail" + }, + "cascade": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiCommentResolutionCascade" + } + ], + "nullable": true + } + }, + "required": [ + "comment", + "cascade" + ], + "type": "object", + "additionalProperties": false + }, + "ApiCommentResolutionConflictError": { + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + }, + "commentState": { + "$ref": "#/components/schemas/ApiCommentState" + } + }, + "required": [ + "errorCode", + "message", + "commentState" + ], + "type": "object", + "additionalProperties": false + }, + "User": { + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "userPath": { + "type": "string" + }, + "photo": { + "type": "string" + }, + "teams": { + "items": { + "$ref": "#/components/schemas/Team" + }, + "type": "array" + }, + "Teams": { + "items": { + "$ref": "#/components/schemas/Team" + }, + "type": "array" + }, + "upgraded": { + "type": "boolean" + } + }, + "required": [ + "id", + "email", + "name", + "userPath", + "photo", + "teams", + "upgraded" + ], + "type": "object", + "additionalProperties": false + }, + "Note": { + "properties": { + "folderPaths": { + "items": { + "$ref": "#/components/schemas/FolderPath" + }, + "type": "array" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "shortId": { + "type": "string" + }, + "publishLink": { + "type": "string" + }, + "permalink": { + "type": "string", + "nullable": true + }, + "teamPath": { + "type": "string", + "nullable": true + }, + "userPath": { + "type": "string", + "nullable": true + }, + "publishedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "publishType": { + "$ref": "#/components/schemas/NotePublishType" + }, + "lastChangeUser": { + "allOf": [ + { + "$ref": "#/components/schemas/SimpleUserProfile" + } + ], + "nullable": true + }, + "tagsUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "titleUpdatedAt": { + "type": "number", + "format": "double", + "nullable": true + }, + "createdAt": { + "type": "number", + "format": "double" + }, + "lastChangedAt": { + "type": "number", + "format": "double" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "writePermission", + "readPermission", + "shortId", + "publishLink", + "permalink", + "teamPath", + "userPath", + "publishedAt", + "publishType", + "lastChangeUser", + "tagsUpdatedAt", + "titleUpdatedAt", + "createdAt", + "lastChangedAt", + "description", + "tags", + "title", + "id" + ], + "type": "object" + }, + "GetUserHistory": { + "items": { + "$ref": "#/components/schemas/Note" + }, + "type": "array" + } + }, + "securitySchemes": { + "token": { + "type": "http", + "scheme": "bearer" + } + } + }, + "info": { + "title": "HackMD OpenAPI", + "version": "1.0.0", + "description": "HackMD OpenAPI", + "license": { + "name": "UNLICENSED" + }, + "contact": { + "name": "HackMD Team", + "email": "support@hackmd.io" + } + }, + "paths": { + "/webhooks": { + "get": { + "operationId": "ListWebhooks", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiWebhook" + }, + "type": "array" + } + } + } + } + }, + "description": "List webhooks in the current user's workspace.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + }, + "post": { + "operationId": "CreateWebhook", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiWebhook" + }, + { + "properties": { + "secret": { + "type": "string" + } + }, + "required": [ + "secret" + ], + "type": "object" + } + ] + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Folder not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "429": { + "description": "Webhook limit reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Create a webhook in the current user's workspace.\nThe signing secret is returned only once and cannot be retrieved later.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiWebhookBody" + } + } + } + } + } + }, + "/webhooks/{hookId}": { + "get": { + "operationId": "GetWebhook", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhook" + } + } + } + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Get a webhook in the current user's workspace.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateWebhook", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhook" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Update a webhook. At least one of name, url, or active is required.\nScope and event selection cannot be changed.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiWebhookBody" + } + } + } + } + }, + "delete": { + "operationId": "DeleteWebhook", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Delete a webhook.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/webhooks/{hookId}/ping": { + "post": { + "operationId": "PingWebhook", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "429": { + "description": "Too many ping requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Send a test delivery. Read the result from the deliveries endpoint.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/webhooks/{hookId}/deliveries": { + "get": { + "operationId": "ListWebhookDeliveries", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_ApiWebhookDelivery_" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "List webhook deliveries within the workspace plan's retention window.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiWebhookPage" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiWebhookLimit" + } + } + ] + } + }, + "/webhooks/{hookId}/deliveries/export": { + "get": { + "operationId": "ExportWebhookDeliveries", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/x-ndjson": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Download webhook deliveries as newline-delimited JSON.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/webhooks/{hookId}/deliveries/{deliveryId}": { + "get": { + "operationId": "GetWebhookDelivery", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhookDelivery" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Delivery not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Get a webhook delivery within the workspace plan's retention window.", + "tags": [ + "Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "deliveryId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/webhooks": { + "get": { + "operationId": "ListTeamWebhooks", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiWebhook" + }, + "type": "array" + } + } + } + } + }, + "description": "List webhooks in a team workspace.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateTeamWebhook", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiWebhook" + }, + { + "properties": { + "secret": { + "type": "string" + } + }, + "required": [ + "secret" + ], + "type": "object" + } + ] + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Folder not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "429": { + "description": "Webhook limit reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Create a webhook in a team workspace.\nThe signing secret is returned only once and cannot be retrieved later.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiWebhookBody" + } + } + } + } + } + }, + "/teams/{teampath}/webhooks/{hookId}": { + "get": { + "operationId": "GetTeamWebhook", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhook" + } + } + } + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Get a webhook in a team workspace.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateTeamWebhook", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhook" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Update a team webhook. At least one of name, url, or active is required.\nScope and event selection cannot be changed.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiWebhookBody" + } + } + } + } + }, + "delete": { + "operationId": "DeleteTeamWebhook", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Delete a team webhook.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/webhooks/{hookId}/ping": { + "post": { + "operationId": "PingTeamWebhook", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "429": { + "description": "Too many ping requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Send a test delivery for a team webhook.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/webhooks/{hookId}/deliveries": { + "get": { + "operationId": "ListTeamWebhookDeliveries", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_ApiWebhookDelivery_" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "List team webhook deliveries within the plan's retention window.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiWebhookPage" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiWebhookLimit" + } + } + ] + } + }, + "/teams/{teampath}/webhooks/{hookId}/deliveries/export": { + "get": { + "operationId": "ExportTeamWebhookDeliveries", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/x-ndjson": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid webhook ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Webhook not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Download team webhook deliveries as newline-delimited JSON.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/webhooks/{hookId}/deliveries/{deliveryId}": { + "get": { + "operationId": "GetTeamWebhookDelivery", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiWebhookDelivery" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + }, + "404": { + "description": "Delivery not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookApiErrorResponse" + } + } + } + } + }, + "description": "Get a team webhook delivery within the plan's retention window.", + "tags": [ + "Team Webhooks" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "hookId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "deliveryId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/notes": { + "get": { + "operationId": "ListNotes", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NoteType" + }, + "type": "array" + } + } + } + } + }, + "description": "List all notes for the current user", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + }, + "post": { + "operationId": "CreateNote", + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleNote" + } + } + } + }, + "207": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNoteMultiStatusResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Create a new note for the current user", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [], + "requestBody": { + "description": "The note content or a JSON object with note properties.", + "required": false, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "properties": { + "origin": { + "type": "string" + }, + "parentFolderId": { + "type": "string" + }, + "permalink": { + "type": "string" + }, + "noteFeatures": { + "$ref": "#/components/schemas/ApiNoteFeatures" + }, + "suggestEditPermission": { + "$ref": "#/components/schemas/SuggestEditPermissionType" + }, + "commentPermission": { + "$ref": "#/components/schemas/CommentPermissionType" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "The note content or a JSON object with note properties." + } + } + } + } + } + }, + "/notes/{noteId}": { + "get": { + "operationId": "GetNote", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleNote" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Get a single note for the current user (or team note if accessible)", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the note to retrieve.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "delete": { + "operationId": "DeleteNote", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Delete a note for the current user", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the note to delete.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateNote", + "responses": { + "202": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Update a note's content or permissions for the current user", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the note to update.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The properties to update on the note.", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "parentFolderId": { + "type": "string", + "nullable": true + }, + "permalink": { + "type": "string" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + } + }, + "type": "object", + "description": "The properties to update on the note." + } + } + } + } + } + }, + "/folders": { + "get": { + "operationId": "ListFolders", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiFolder" + }, + "type": "array" + } + } + } + } + }, + "description": "List all folders in the current user's workspace", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + }, + "post": { + "operationId": "CreateFolder", + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolder" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Create a new folder in the current user's workspace", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [], + "requestBody": { + "description": "The folder properties.", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserFolderBody", + "description": "The folder properties." + } + } + } + } + } + }, + "/folders/folder-order": { + "get": { + "operationId": "GetFolderOrder", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolderOrder" + } + } + } + } + }, + "description": "Get your personal folder ordering for this workspace (parent folder id or `root` → ordered child folder ids).\nUses the same folder UUIDs as (not internal Yjs clientIds).", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + }, + "put": { + "operationId": "UpdateFolderOrder", + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Replace your personal folder ordering for this workspace.", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFolderOrderBody" + } + } + } + } + } + }, + "/folders/{folderId}": { + "get": { + "operationId": "GetFolder", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolder" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Get a single folder in the current user's workspace", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the folder to retrieve.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateFolder", + "responses": { + "202": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Update a folder in the current user's workspace", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the folder to update.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The properties to update.", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserFolderBody", + "description": "The properties to update." + } + } + } + } + }, + "delete": { + "operationId": "DeleteFolder", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Delete a folder in the current user's workspace", + "tags": [ + "User Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The ID of the folder to delete.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/trash": { + "get": { + "operationId": "ListPersonalTrash", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiTrashNote" + }, + "type": "array" + } + } + } + } + }, + "description": "List trashed notes in your personal workspace (same data as the internal trash API).", + "tags": [ + "Trash" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + } + }, + "/trash/batch-restore": { + "put": { + "operationId": "BatchRestore", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrashBatchOperationResponse" + } + } + } + }, + "207": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrashBatchOperationResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Restore multiple notes from trash in one request.", + "tags": [ + "Trash" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchRestoreTrashBody" + } + } + } + } + } + }, + "/trash/{noteId}/restore": { + "put": { + "operationId": "RestoreNote", + "responses": { + "204": { + "description": "" + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Restore a single note from trash.", + "tags": [ + "Trash" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "Encoded note id of the trashed note.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/trash": { + "get": { + "operationId": "ListTeamTrash", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiTrashNote" + }, + "type": "array" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiErrorResponse" + }, + { + "type": "string" + } + ] + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiErrorResponse" + }, + { + "type": "string" + } + ] + } + } + } + } + }, + "description": "List trashed notes in a team workspace (team admin only; same rules as the internal API).", + "tags": [ + "Trash" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams": { + "get": { + "operationId": "ListTeams", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Team" + }, + "type": "array" + } + } + } + } + }, + "description": "List the teams for the current user", + "tags": [ + "Teams" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + } + }, + "/teams/{teampath}/notes": { + "get": { + "operationId": "ListTeamNotes", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/NoteType" + }, + "type": "array" + } + } + } + } + }, + "description": "List all notes for a team", + "tags": [ + "Team Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateTeamNote", + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleNote" + } + } + } + }, + "207": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNoteMultiStatusResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Create a new note for a team", + "tags": [ + "Team Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The note content (string) or a JSON object with note properties (title, content, permissions, etc.).", + "required": false, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "properties": { + "origin": { + "type": "string" + }, + "parentFolderId": { + "type": "string" + }, + "permalink": { + "type": "string" + }, + "noteFeatures": { + "$ref": "#/components/schemas/ApiNoteFeatures" + }, + "suggestEditPermission": { + "$ref": "#/components/schemas/SuggestEditPermissionType" + }, + "commentPermission": { + "$ref": "#/components/schemas/CommentPermissionType" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + { + "type": "string" + } + ], + "description": "The note content (string) or a JSON object with note properties (title, content, permissions, etc.)." + } + } + } + } + } + }, + "/teams/{teampath}/notes/{noteId}": { + "get": { + "operationId": "GetTeamNote", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SingleNote" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Get a single note for a team", + "tags": [ + "Team Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the note to retrieve.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateTeamNote", + "responses": { + "202": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Update a team note", + "tags": [ + "Team Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the note to update.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The properties to update on the team note (e.g., content, permissions, permalink, parentFolderId).", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "parentFolderId": { + "type": "string", + "nullable": true + }, + "permalink": { + "type": "string" + }, + "writePermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "readPermission": { + "$ref": "#/components/schemas/NotePermissionRole" + }, + "content": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + } + }, + "type": "object", + "description": "The properties to update on the team note (e.g., content, permissions, permalink, parentFolderId)." + } + } + } + } + }, + "delete": { + "operationId": "DeleteTeamNote", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Delete a team note", + "tags": [ + "Team Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the note to delete.", + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/teams/{teampath}/folders": { + "get": { + "operationId": "ListTeamFolders", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ApiFolder" + }, + "type": "array" + } + } + } + } + }, + "description": "List all folders in a team workspace", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateTeamFolder", + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolder" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Create a new folder in a team workspace", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The folder properties.", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamFolderBody", + "description": "The folder properties." + } + } + } + } + } + }, + "/teams/{teampath}/folders/folder-order": { + "get": { + "operationId": "GetTeamFolderOrder", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolderOrder" + } + } + } + } + }, + "description": "Get your personal folder ordering for this team workspace (parent folder id or `root` → ordered child folder ids).\nUses the same folder UUIDs as (not internal Yjs clientIds).", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "operationId": "UpdateTeamFolderOrder", + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + } + }, + "description": "Replace your personal folder ordering for this team workspace.", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFolderOrderBody" + } + } + } + } + } + }, + "/teams/{teampath}/folders/{folderId}": { + "get": { + "operationId": "GetTeamFolder", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiFolder" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Get a single folder in a team workspace", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the folder to retrieve.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "patch": { + "operationId": "UpdateTeamFolder", + "responses": { + "202": { + "description": "" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiBadRequestResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "422": { + "description": "Validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiValidationError" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Update a folder in a team workspace", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the folder to update.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "The properties to update.", + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamFolderBody", + "description": "The properties to update." + } + } + } + } + }, + "delete": { + "operationId": "DeleteTeamFolder", + "responses": { + "204": { + "description": "No content" + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + }, + "description": "Delete a folder in a team workspace", + "tags": [ + "Team Folders" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The path identifier for the team.", + "in": "path", + "name": "teampath", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The ID of the folder to delete.", + "in": "path", + "name": "folderId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/notes/{noteId}/versions": { + "get": { + "operationId": "ListVersions", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNoteVersions" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "description": "List saved versions for a note.", + "summary": "List note versions", + "tags": [ + "Versions" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "named_only", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "created_by", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Return versions created at or after this ISO-8601 datetime.", + "in": "query", + "name": "created_after", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Return versions created at or before this ISO-8601 datetime.", + "in": "query", + "name": "created_before", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "default": 1, + "format": "double", + "type": "number" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 50, + "format": "double", + "type": "number" + } + } + ] + }, + "post": { + "operationId": "CreateVersion", + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoteVersionMetadata" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "409": { + "description": "No content change or version already named", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNoteVersionConflictError" + }, + "examples": { + "Example 1": { + "value": { + "error_code": "no_content_change", + "message": "Current note content has no changes to save.", + "latest_version_id": "01234567-89ab-4def-8123-456789abcdef", + "latest_name_source": "user" + } + }, + "Example 2": { + "value": { + "error_code": "version_already_named", + "message": "The version already has a name.", + "version_id": "01234567-89ab-4def-8123-456789abcdef", + "name": "Release checkpoint" + } + } + } + } + } + } + }, + "description": "Create a named version from live note content or an existing saved version. When\nlive note content has changed, this creates and names a new saved version. When\nlive content matches the latest system version, that version is named in place and\nreturned with 201. When it matches the latest user-named version, the response is\n409 `no_content_change`. Naming an already named `version:{id}` returns 409\n`version_already_named`.", + "summary": "Create a named note version", + "tags": [ + "Versions" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNoteVersionBody" + } + } + } + } + }, + "patch": { + "operationId": "UpdateVersion", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoteVersionMetadata" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "422": { + "description": "Version is not named", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionNotNamedError" + }, + "examples": { + "Example 1": { + "value": { + "error_code": "version_not_named", + "message": "The version is not named. Use POST with base=version:{id} first.", + "version_id": "01234567-89ab-4def-8123-456789abcdef" + } + } + } + } + } + } + }, + "description": "Update an existing named version's metadata with `PATCH /notes/{noteId}/versions`\nand a `base` body field formatted as `version:{id}`. The item path\n`/notes/{noteId}/versions/{versionId}` does not support PATCH.", + "summary": "Update named version metadata", + "tags": [ + "Versions" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNoteVersionBody" + } + } + } + } + } + }, + "/notes/{noteId}/versions/compare": { + "get": { + "operationId": "CompareVersions", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompareNoteVersions" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "422": { + "description": "Validation failed or version content unavailable", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApiError" + }, + { + "$ref": "#/components/schemas/ApiValidationError" + } + ] + } + } + } + } + }, + "description": "Compare two saved versions or a saved version with live note content.", + "summary": "Compare note versions", + "tags": [ + "Versions" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Required comparison source. Use note_content or version:{id}.", + "in": "query", + "name": "base", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Required comparison target. Use note_content or version:{id}.", + "in": "query", + "name": "target", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/notes/{noteId}/versions/{versionId}": { + "get": { + "operationId": "GetVersion", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoteVersion" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "description": "Get one saved version with reconstructed content when available.", + "summary": "Get a note version", + "tags": [ + "Versions" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "versionId", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/notes/{noteId}/images": { + "post": { + "operationId": "UploadNoteImage", + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NoteImageUploadResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + }, + "413": { + "description": "Payload Too Large", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + }, + "415": { + "description": "Unsupported Media Type", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + }, + "422": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiRequestErrorResponse" + } + } + } + } + }, + "description": "Upload an image for a note.", + "tags": [ + "User Notes" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "image" + ] + } + } + } + } + } + }, + "/notes/{noteId}/comments": { + "get": { + "operationId": "ListNoteComments", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNoteComments" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "403": { + "description": "Comments not readable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "404": { + "description": "Note not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + } + }, + "description": "List comments and replies visible on a note.", + "tags": [ + "Comments" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "page", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiCommentPage" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiCommentLimit" + } + }, + { + "in": "query", + "name": "sort", + "required": false, + "schema": { + "default": "asc", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + { + "in": "query", + "name": "threadId", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiCommentThreadId" + } + }, + { + "in": "query", + "name": "commentStatus", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiCommentStatus" + } + }, + { + "in": "query", + "name": "threadStatus", + "required": false, + "schema": { + "$ref": "#/components/schemas/ApiCommentStatus" + } + }, + { + "in": "query", + "name": "isThreadHead", + "required": false, + "schema": { + "type": "boolean", + "enum": [ + true + ] + } + } + ] + } + }, + "/notes/{noteId}/comments/{commentId}": { + "get": { + "operationId": "GetNoteComment", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentDetail" + } + } + } + }, + "403": { + "description": "Comments not readable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + } + }, + "description": "Get one comment or reply from a note.", + "tags": [ + "Comments" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "commentId", + "required": true, + "schema": { + "$ref": "#/components/schemas/ApiCommentId" + } + } + ] + } + }, + "/notes/{noteId}/comments/{commentId}/resolution": { + "put": { + "operationId": "ResolveNoteComment", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentResolutionResponse" + } + } + } + }, + "403": { + "description": "Comment resolution forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "409": { + "description": "Comment resolution conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentResolutionConflictError" + } + } + } + } + }, + "description": "Resolve a comment or thread.", + "tags": [ + "Comments" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "commentId", + "required": true, + "schema": { + "$ref": "#/components/schemas/ApiCommentId" + } + } + ] + }, + "delete": { + "operationId": "UnresolveNoteComment", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentResolutionResponse" + } + } + } + }, + "403": { + "description": "Comment resolution forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentError" + } + } + } + }, + "409": { + "description": "Comment resolution conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiCommentResolutionConflictError" + } + } + } + } + }, + "description": "Unresolve a comment or thread.", + "tags": [ + "Comments" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "noteId", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "commentId", + "required": true, + "schema": { + "$ref": "#/components/schemas/ApiCommentId" + } + } + ] + } + }, + "/me": { + "get": { + "operationId": "GetCurrentUser", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "description": "Get the current user's profile", + "tags": [ + "Profile" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [] + } + }, + "/history": { + "get": { + "operationId": "GetHistory", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUserHistory" + } + } + } + } + }, + "description": "Get note history for the current user", + "tags": [ + "History" + ], + "security": [ + { + "token": [] + } + ], + "parameters": [ + { + "description": "The maximum number of history items to return.", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "format": "double", + "type": "number" + } + } + ] + } + } + }, + "servers": [ + { + "url": "/" + } + ], + "tags": [ + { + "name": "Profile", + "description": "Operations to retrieve user profile information." + }, + { + "name": "Teams", + "description": "Operations related to user teams." + }, + { + "name": "History", + "description": "Operations related to user's note history." + }, + { + "name": "Team Notes", + "description": "Operations related to notes within a team." + }, + { + "name": "User Notes", + "description": "Operations related to a user's personal notes." + }, + { + "name": "User Folders", + "description": "Operations related to a user's personal folders." + }, + { + "name": "Team Folders", + "description": "Operations related to folders within a team." + }, + { + "name": "Trash", + "description": "Operations for listing and restoring trashed notes." + }, + { + "name": "Comments", + "description": "Operations for reading and resolving note comments." + }, + { + "name": "Versions", + "description": "Operations for listing, retrieving, comparing, creating, and updating note versions." + }, + { + "name": "Webhooks", + "description": "Operations for managing webhooks in a personal workspace." + }, + { + "name": "Team Webhooks", + "description": "Operations for managing webhooks in a team workspace." + } + ] +} \ No newline at end of file diff --git a/nodejs/src/generated/client.gen.ts b/nodejs/src/generated/client.gen.ts new file mode 100644 index 0000000..3920abb --- /dev/null +++ b/nodejs/src/generated/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type Client, type ClientOptions, type Config, createClient, createConfig } from './client/index.js'; +import type { ClientOptions as ClientOptions2 } from './types.gen.js'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client: Client = createClient(createConfig()); diff --git a/nodejs/src/generated/client/client.gen.ts b/nodejs/src/generated/client/client.gen.ts new file mode 100644 index 0000000..25df9d7 --- /dev/null +++ b/nodejs/src/generated/client/client.gen.ts @@ -0,0 +1,161 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; + +import { createSseClient } from '../core/serverSentEvents.gen.js'; +import type { HttpMethod } from '../core/types.gen.js'; +import { getValidRequestBody } from '../core/utils.gen.js'; +import type { Client, Config, RequestOptions } from './types.gen.js'; +import { buildUrl, createConfig, mergeConfigs, mergeHeaders, setAuthParams } from './utils.gen.js'; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + let instance: AxiosInstance; + + if (_config.axios && !('Axios' in _config.axios)) { + instance = _config.axios; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...configWithoutAuth } = _config; + instance = axios.create(configWithoutAuth); + } + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + instance.defaults = { + ...instance.defaults, + ..._config, + // @ts-expect-error + headers: mergeHeaders(instance.defaults.headers, _config.headers), + }; + return getConfig(); + }; + + const beforeRequest = async < + TData = unknown, + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + axios: options.axios ?? _config.axios ?? instance, + headers: mergeHeaders(_config.headers, options.headers), + }; + + if (opts.security) { + await setAuthParams(opts); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + const { opts, url } = await beforeRequest(options); + try { + // assign Axios here for consistency with fetch + const _axios = opts.axios!; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...optsWithoutAuth } = opts; + const response = await _axios({ + ...optsWithoutAuth, + baseURL: '', // the baseURL is already included in `url` + data: getValidRequestBody(opts), + headers: opts.headers as RawAxiosRequestHeaders, + // let `paramsSerializer()` handle query params if it exists + params: opts.paramsSerializer ? opts.query : undefined, + url, + }); + + let { data } = response; + + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return { + ...response, + data: data ?? {}, + }; + } catch (error) { + const e = error as AxiosError; + if (opts.throwOnError) { + throw e; + } + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; + } + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as Record, + method, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + // @ts-expect-error + signal: opts.signal, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => + buildUrl({ axios: instance, ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + instance, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/nodejs/src/generated/client/index.ts b/nodejs/src/generated/client/index.ts new file mode 100644 index 0000000..661a3db --- /dev/null +++ b/nodejs/src/generated/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen.js'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen.js'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen.js'; +export { buildClientParams } from '../core/params.gen.js'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen.js'; +export type { ServerSentEventsResult } from '../core/serverSentEvents.gen.js'; +export type { ClientMeta } from '../core/types.gen.js'; +export { createClient } from './client.gen.js'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + TDataShape, +} from './types.gen.js'; +export { createConfig } from './utils.gen.js'; diff --git a/nodejs/src/generated/client/types.gen.ts b/nodejs/src/generated/client/types.gen.ts new file mode 100644 index 0000000..e3894d1 --- /dev/null +++ b/nodejs/src/generated/client/types.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + AxiosError, + AxiosInstance, + AxiosRequestHeaders, + AxiosResponse, + AxiosStatic, + CreateAxiosDefaults, +} from 'axios'; + +import type { Auth } from '../core/auth.gen.js'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen.js'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen.js'; + +export interface Config + extends Omit, CoreConfig { + /** + * Axios implementation. You can use this option to provide either an + * `AxiosStatic` or an `AxiosInstance`. + * + * @default axios + */ + axios?: AxiosStatic | AxiosInstance; + /** + * Base URL for all requests made by this client. + */ + baseURL?: T['baseURL']; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | AxiosRequestHeaders + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ClientOptions { + baseURL?: string; + throwOnError?: boolean; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, +> = ThrowOnError extends true + ? Promise ? TData[keyof TData] : TData>> + : Promise< + | (AxiosResponse ? TData[keyof TData] : TData> & { + error: undefined; + }) + | (AxiosError ? TError[keyof TError] : TError> & { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + }) + >; + +type MethodFn = ( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = ( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & + Pick< + RequestOptions, + 'axios' | 'baseURL' | 'paramsSerializer' | 'querySerializer' + >, +) => string; + +export type Client = CoreClient & { + instance: AxiosInstance; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, +> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + ([TData] extends [never] ? unknown : Omit); diff --git a/nodejs/src/generated/client/utils.gen.ts b/nodejs/src/generated/client/utils.gen.ts new file mode 100644 index 0000000..99a0c09 --- /dev/null +++ b/nodejs/src/generated/client/utils.gen.ts @@ -0,0 +1,206 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen.js'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen.js'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen.js'; +import { getUrl } from '../core/utils.gen.js'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen.js'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}): ((queryParams: T) => string) => { + const querySerializer = (queryParams: T): string => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +const checkForExistence = ( + options: Pick & { + headers: Record; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if (name in options.headers || options.query?.[name]) { + return true; + } + if ( + 'Cookie' in options.headers && + options.headers['Cookie'] && + typeof options.headers['Cookie'] === 'string' + ) { + return options.headers['Cookie'].includes(`${name}=`); + } + return false; +}; + +export async function setAuthParams( + options: Pick & { + headers: Record; + }, +): Promise { + for (const auth of options.security ?? []) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': { + const value = `${name}=${token}`; + if ('Cookie' in options.headers && options.headers['Cookie']) { + options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`; + } else { + options.headers['Cookie'] = value; + } + break; + } + case 'header': + default: + options.headers[name] = token; + break; + } + } +} + +export const buildUrl: Client['buildUrl'] = (options) => { + const instanceBaseUrl = options.axios?.defaults?.baseURL; + + const baseUrl = + options.baseURL && typeof options.baseURL === 'string' ? options.baseURL : instanceBaseUrl; + + return getUrl({ + baseUrl: baseUrl as string, + path: options.path, + // let `paramsSerializer()` handle query params if it exists + query: !options.paramsSerializer ? options.query : undefined, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +/** + * Special Axios headers keywords allowing to set headers by request method. + */ +export const axiosHeadersKeywords = [ + 'common', + 'delete', + 'get', + 'head', + 'patch', + 'post', + 'put', +] as const; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Record => { + const mergedHeaders: Record = {}; + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = Object.entries(header); + + for (const [key, value] of iterator) { + if ( + axiosHeadersKeywords.includes(key as (typeof axiosHeadersKeywords)[number]) && + typeof value === 'object' + ) { + mergedHeaders[key] = { + ...(mergedHeaders[key] as Record), + ...value, + }; + } else if (value === null) { + delete mergedHeaders[key]; + } else if (Array.isArray(value)) { + for (const v of value) { + // @ts-expect-error + mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders[key] = typeof value === 'object' ? JSON.stringify(value) : (value as string); + } + } + } + return mergedHeaders; +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...override, +}); diff --git a/nodejs/src/generated/core/auth.gen.ts b/nodejs/src/generated/core/auth.gen.ts new file mode 100644 index 0000000..c663664 --- /dev/null +++ b/nodejs/src/generated/core/auth.gen.ts @@ -0,0 +1,48 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * A unique identifier for the security scheme. + * + * Defined only when there are multiple security schemes whose `Auth` + * shape would otherwise be identical. + */ + key?: string; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/nodejs/src/generated/core/bodySerializer.gen.ts b/nodejs/src/generated/core/bodySerializer.gen.ts new file mode 100644 index 0000000..71b4bba --- /dev/null +++ b/nodejs/src/generated/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen.js'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/nodejs/src/generated/core/params.gen.ts b/nodejs/src/generated/core/params.gen.ts new file mode 100644 index 0000000..0f50047 --- /dev/null +++ b/nodejs/src/generated/core/params.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +function buildKeyMap(fields: FieldsConfig, map?: KeyMap): KeyMap { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +} + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +type ParamsSlotMap = Record; + +function stripEmptySlots(params: ParamsSlotMap): void { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +} + +export function buildClientParams(args: ReadonlyArray, fields: FieldsConfig): Params { + const params: ParamsSlotMap = { + body: Object.create(null), + headers: Object.create(null), + path: Object.create(null), + query: Object.create(null), + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params as Params; +} diff --git a/nodejs/src/generated/core/pathSerializer.gen.ts b/nodejs/src/generated/core/pathSerializer.gen.ts new file mode 100644 index 0000000..fab1ed4 --- /dev/null +++ b/nodejs/src/generated/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle): '.' | ';' | ',' | '&' => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle): ',' | '|' | '%20' => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle): '.' | ';' | ',' | '&' => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}): string => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam): string => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}): string => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/nodejs/src/generated/core/queryKeySerializer.gen.ts b/nodejs/src/generated/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..773b065 --- /dev/null +++ b/nodejs/src/generated/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown): unknown | undefined => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/nodejs/src/generated/core/serverSentEvents.gen.ts b/nodejs/src/generated/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..94fd835 --- /dev/null +++ b/nodejs/src/generated/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen.js'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/nodejs/src/generated/core/types.gen.ts b/nodejs/src/generated/core/types.gen.ts new file mode 100644 index 0000000..bcef254 --- /dev/null +++ b/nodejs/src/generated/core/types.gen.ts @@ -0,0 +1,110 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen.js'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen.js'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +/** + * Arbitrary metadata passed through the `meta` request option. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface ClientMeta {} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/nodejs/src/generated/core/utils.gen.ts b/nodejs/src/generated/core/utils.gen.ts new file mode 100644 index 0000000..53c009e --- /dev/null +++ b/nodejs/src/generated/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen.js'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen.js'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE: RegExp = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer): string => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}): string => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}): unknown { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/nodejs/src/generated/index.ts b/nodejs/src/generated/index.ts new file mode 100644 index 0000000..eae4884 --- /dev/null +++ b/nodejs/src/generated/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export { batchRestore, compareVersions, createFolder, createNote, createTeamFolder, createTeamNote, createTeamWebhook, createVersion, createWebhook, deleteFolder, deleteNote, deleteTeamFolder, deleteTeamNote, deleteTeamWebhook, deleteWebhook, exportTeamWebhookDeliveries, exportWebhookDeliveries, getCurrentUser, getFolder, getFolderOrder, getHistory, getNote, getNoteComment, getTeamFolder, getTeamFolderOrder, getTeamNote, getTeamWebhook, getTeamWebhookDelivery, getVersion, getWebhook, getWebhookDelivery, listFolders, listNoteComments, listNotes, listPersonalTrash, listTeamFolders, listTeamNotes, listTeams, listTeamTrash, listTeamWebhookDeliveries, listTeamWebhooks, listVersions, listWebhookDeliveries, listWebhooks, type Options, pingTeamWebhook, pingWebhook, resolveNoteComment, restoreNote, unresolveNoteComment, updateFolder, updateFolderOrder, updateNote, updateTeamFolder, updateTeamFolderOrder, updateTeamNote, updateTeamWebhook, updateVersion, updateWebhook, uploadNoteImage } from './sdk.gen.js'; +export type { ApiBadRequestResponse, ApiComment, ApiCommentActor, ApiCommentAnchor, ApiCommentDeletedUserActor, ApiCommentDetail, ApiCommentDetailReply, ApiCommentError, ApiCommentGuestActor, ApiCommentHead, ApiCommentId, ApiCommentLimit, ApiCommentListReply, ApiCommentPage, ApiCommentPagination, ApiCommentReactionShortcode, ApiCommentReactionSummary, ApiCommentResolutionCascade, ApiCommentResolutionConflictError, ApiCommentResolutionResponse, ApiCommentState, ApiCommentStateActor, ApiCommentStateDeletedUserActor, ApiCommentStateReason, ApiCommentStateUserActor, ApiCommentStatus, ApiCommentTeamActor, ApiCommentThreadId, ApiCommentThreadState, ApiCommentUserActor, ApiError, ApiErrorResponse, ApiFolder, ApiFolderOrder, ApiNoteFeatures, ApiRequestErrorResponse, ApiTrashNote, ApiValidationError, ApiValidationFieldError, ApiWebhook, ApiWebhookDelivery, ApiWebhookLimit, ApiWebhookPage, ApiWebhookScope, BatchOperationError, BatchRestoreData, BatchRestoreError, BatchRestoreErrors, BatchRestoreResponse, BatchRestoreResponses, BatchRestoreTrashBody, ClientOptions, CommentPermissionType, CompareNoteVersions, CompareVersionsData, CompareVersionsError, CompareVersionsErrors, CompareVersionsResponse, CompareVersionsResponses, CreateApiWebhookBody, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteMultiStatusResponse, CreateNoteResponse, CreateNoteResponses, CreateNoteVersionBody, CreateNoteVersionConflictError, CreateTeamFolderBody, CreateTeamFolderData, CreateTeamFolderError, CreateTeamFolderErrors, CreateTeamFolderResponse, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteError, CreateTeamNoteErrors, CreateTeamNoteResponse, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookError, CreateTeamWebhookErrors, CreateTeamWebhookResponse, CreateTeamWebhookResponses, CreateUserFolderBody, CreateVersionData, CreateVersionError, CreateVersionErrors, CreateVersionResponse, CreateVersionResponses, CreateWebhookData, CreateWebhookData2, CreateWebhookError, CreateWebhookErrors, CreateWebhookResponse, CreateWebhookResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderError, DeleteTeamFolderErrors, DeleteTeamFolderResponse, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteError, DeleteTeamNoteErrors, DeleteTeamNoteResponse, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookError, DeleteTeamWebhookErrors, DeleteTeamWebhookResponse, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesError, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponse, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesError, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponse, ExportWebhookDeliveriesResponses, FolderColorHex, FolderIconCodepoint, FolderName, FolderPath, GetCurrentUserData, GetCurrentUserResponse, GetCurrentUserResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponse, GetFolderOrderResponses, GetFolderResponse, GetFolderResponses, GetHistoryData, GetHistoryResponse, GetHistoryResponses, GetNoteCommentData, GetNoteCommentError, GetNoteCommentErrors, GetNoteCommentResponse, GetNoteCommentResponses, GetNoteComments, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetNoteVersions, GetTeamFolderData, GetTeamFolderError, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponse, GetTeamFolderOrderResponses, GetTeamFolderResponse, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteError, GetTeamNoteErrors, GetTeamNoteResponse, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryError, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponse, GetTeamWebhookDeliveryResponses, GetTeamWebhookError, GetTeamWebhookErrors, GetTeamWebhookResponse, GetTeamWebhookResponses, GetUserHistory, GetVersionData, GetVersionError, GetVersionErrors, GetVersionResponse, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryError, GetWebhookDeliveryErrors, GetWebhookDeliveryResponse, GetWebhookDeliveryResponses, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, ListFoldersData, ListFoldersResponse, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsError, ListNoteCommentsErrors, ListNoteCommentsResponse, ListNoteCommentsResponses, ListNotesData, ListNotesResponse, ListNotesResponses, ListPersonalTrashData, ListPersonalTrashResponse, ListPersonalTrashResponses, ListTeamFoldersData, ListTeamFoldersResponse, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponse, ListTeamNotesResponses, ListTeamsData, ListTeamsResponse, ListTeamsResponses, ListTeamTrashData, ListTeamTrashError, ListTeamTrashErrors, ListTeamTrashResponse, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesError, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponse, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponse, ListTeamWebhooksResponses, ListVersionsData, ListVersionsError, ListVersionsErrors, ListVersionsResponse, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesError, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponse, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponse, ListWebhooksResponses, NoContentChangeError, Note, NoteImageUploadResponse, NotePermissionRole, NotePublishType, NoteType, NoteVersion, NoteVersionMetadata, NoteVersionUser, PaginatedQuery, PaginatedResponseApiWebhookDelivery, PingTeamWebhookData, PingTeamWebhookError, PingTeamWebhookErrors, PingTeamWebhookResponse, PingTeamWebhookResponses, PingWebhookData, PingWebhookError, PingWebhookErrors, PingWebhookResponse, PingWebhookResponses, RecordStringApiValidationFieldError, ResolveNoteCommentData, ResolveNoteCommentError, ResolveNoteCommentErrors, ResolveNoteCommentResponse, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteError, RestoreNoteErrors, RestoreNoteResponse, RestoreNoteResponses, SimpleUserProfile, SingleNote, SuggestEditPermissionType, Team, TeamVisibilityType, TrashBatchOperationResponse, UnresolveNoteCommentData, UnresolveNoteCommentError, UnresolveNoteCommentErrors, UnresolveNoteCommentResponse, UnresolveNoteCommentResponses, UpdateApiWebhookBody, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderOrderBody, UpdateFolderOrderData, UpdateFolderOrderError, UpdateFolderOrderErrors, UpdateFolderOrderResponse, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponses, UpdateNoteVersionBody, UpdateTeamFolderBody, UpdateTeamFolderData, UpdateTeamFolderError, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderError, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponse, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteError, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookError, UpdateTeamWebhookErrors, UpdateTeamWebhookResponse, UpdateTeamWebhookResponses, UpdateUserFolderBody, UpdateVersionData, UpdateVersionError, UpdateVersionErrors, UpdateVersionResponse, UpdateVersionResponses, UpdateWebhookData, UpdateWebhookData2, UpdateWebhookError, UpdateWebhookErrors, UpdateWebhookResponse, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageError, UploadNoteImageErrors, UploadNoteImageResponse, UploadNoteImageResponses, User, VersionAlreadyNamedError, VersionNotNamedError, WebhookApiErrorResponse, WebhookDeliveryStatusCategory } from './types.gen.js'; diff --git a/nodejs/src/generated/sdk.gen.ts b/nodejs/src/generated/sdk.gen.ts new file mode 100644 index 0000000..e26d5dd --- /dev/null +++ b/nodejs/src/generated/sdk.gen.ts @@ -0,0 +1,690 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client } from './client.gen.js'; +import { type Client, type ClientMeta, formDataBodySerializer, type Options as Options2, type RequestResult, type TDataShape } from './client/index.js'; +import type { BatchRestoreData, BatchRestoreErrors, BatchRestoreResponses, CompareVersionsData, CompareVersionsErrors, CompareVersionsResponses, CreateFolderData, CreateFolderErrors, CreateFolderResponses, CreateNoteData, CreateNoteErrors, CreateNoteResponses, CreateTeamFolderData, CreateTeamFolderErrors, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteErrors, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookErrors, CreateTeamWebhookResponses, CreateVersionData, CreateVersionErrors, CreateVersionResponses, CreateWebhookData2, CreateWebhookErrors, CreateWebhookResponses, DeleteFolderData, DeleteFolderErrors, DeleteFolderResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderErrors, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteErrors, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookErrors, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponses, GetCurrentUserData, GetCurrentUserResponses, GetFolderData, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponses, GetFolderResponses, GetHistoryData, GetHistoryResponses, GetNoteCommentData, GetNoteCommentErrors, GetNoteCommentResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetTeamFolderData, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponses, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteErrors, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponses, GetTeamWebhookErrors, GetTeamWebhookResponses, GetVersionData, GetVersionErrors, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryErrors, GetWebhookDeliveryResponses, GetWebhookErrors, GetWebhookResponses, ListFoldersData, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsErrors, ListNoteCommentsResponses, ListNotesData, ListNotesResponses, ListPersonalTrashData, ListPersonalTrashResponses, ListTeamFoldersData, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponses, ListTeamsData, ListTeamsResponses, ListTeamTrashData, ListTeamTrashErrors, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponses, ListVersionsData, ListVersionsErrors, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponses, PingTeamWebhookData, PingTeamWebhookErrors, PingTeamWebhookResponses, PingWebhookData, PingWebhookErrors, PingWebhookResponses, ResolveNoteCommentData, ResolveNoteCommentErrors, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteErrors, RestoreNoteResponses, UnresolveNoteCommentData, UnresolveNoteCommentErrors, UnresolveNoteCommentResponses, UpdateFolderData, UpdateFolderErrors, UpdateFolderOrderData, UpdateFolderOrderErrors, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteErrors, UpdateNoteResponses, UpdateTeamFolderData, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookErrors, UpdateTeamWebhookResponses, UpdateVersionData, UpdateVersionErrors, UpdateVersionResponses, UpdateWebhookData2, UpdateWebhookErrors, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageErrors, UploadNoteImageResponses } from './types.gen.js'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: keyof ClientMeta extends never ? Record : ClientMeta; +}; + +/** + * List webhooks in the current user's workspace. + */ +export const listWebhooks = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks', + ...options +}); + +/** + * Create a webhook in the current user's workspace. + * The signing secret is returned only once and cannot be retrieved later. + */ +export const createWebhook = (options: Options): RequestResult => (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete a webhook. + */ +export const deleteWebhook = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}', + ...options +}); + +/** + * Get a webhook in the current user's workspace. + */ +export const getWebhook = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}', + ...options +}); + +/** + * Update a webhook. At least one of name, url, or active is required. + * Scope and event selection cannot be changed. + */ +export const updateWebhook = (options: Options): RequestResult => (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Send a test delivery. Read the result from the deliveries endpoint. + */ +export const pingWebhook = (options: Options): RequestResult => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}/ping', + ...options +}); + +/** + * List webhook deliveries within the workspace plan's retention window. + */ +export const listWebhookDeliveries = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}/deliveries', + ...options +}); + +/** + * Download webhook deliveries as newline-delimited JSON. + */ +export const exportWebhookDeliveries = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'blob', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}/deliveries/export', + ...options +}); + +/** + * Get a webhook delivery within the workspace plan's retention window. + */ +export const getWebhookDelivery = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/webhooks/{hookId}/deliveries/{deliveryId}', + ...options +}); + +/** + * List webhooks in a team workspace. + */ +export const listTeamWebhooks = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks', + ...options +}); + +/** + * Create a webhook in a team workspace. + * The signing secret is returned only once and cannot be retrieved later. + */ +export const createTeamWebhook = (options: Options): RequestResult => (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete a team webhook. + */ +export const deleteTeamWebhook = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}', + ...options +}); + +/** + * Get a webhook in a team workspace. + */ +export const getTeamWebhook = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}', + ...options +}); + +/** + * Update a team webhook. At least one of name, url, or active is required. + * Scope and event selection cannot be changed. + */ +export const updateTeamWebhook = (options: Options): RequestResult => (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Send a test delivery for a team webhook. + */ +export const pingTeamWebhook = (options: Options): RequestResult => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}/ping', + ...options +}); + +/** + * List team webhook deliveries within the plan's retention window. + */ +export const listTeamWebhookDeliveries = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}/deliveries', + ...options +}); + +/** + * Download team webhook deliveries as newline-delimited JSON. + */ +export const exportTeamWebhookDeliveries = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'blob', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}/deliveries/export', + ...options +}); + +/** + * Get a team webhook delivery within the plan's retention window. + */ +export const getTeamWebhookDelivery = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/webhooks/{hookId}/deliveries/{deliveryId}', + ...options +}); + +/** + * List all notes for the current user + */ +export const listNotes = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes', + ...options +}); + +/** + * Create a new note for the current user + */ +export const createNote = (options?: Options): RequestResult => (options?.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + +/** + * Delete a note for the current user + */ +export const deleteNote = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}', + ...options +}); + +/** + * Get a single note for the current user (or team note if accessible) + */ +export const getNote = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}', + ...options +}); + +/** + * Update a note's content or permissions for the current user + */ +export const updateNote = (options: Options): RequestResult => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * List all folders in the current user's workspace + */ +export const listFolders = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders', + ...options +}); + +/** + * Create a new folder in the current user's workspace + */ +export const createFolder = (options?: Options): RequestResult => (options?.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + +/** + * Get your personal folder ordering for this workspace (parent folder id or `root` → ordered child folder ids). + * Uses the same folder UUIDs as (not internal Yjs clientIds). + */ +export const getFolderOrder = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders/folder-order', + ...options +}); + +/** + * Replace your personal folder ordering for this workspace. + */ +export const updateFolderOrder = (options?: Options): RequestResult => (options?.client ?? client).put({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders/folder-order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + +/** + * Delete a folder in the current user's workspace + */ +export const deleteFolder = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders/{folderId}', + ...options +}); + +/** + * Get a single folder in the current user's workspace + */ +export const getFolder = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders/{folderId}', + ...options +}); + +/** + * Update a folder in the current user's workspace + */ +export const updateFolder = (options: Options): RequestResult => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/folders/{folderId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * List trashed notes in your personal workspace (same data as the internal trash API). + */ +export const listPersonalTrash = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/trash', + ...options +}); + +/** + * Restore multiple notes from trash in one request. + */ +export const batchRestore = (options: Options): RequestResult => (options.client ?? client).put({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/trash/batch-restore', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Restore a single note from trash. + */ +export const restoreNote = (options: Options): RequestResult => (options.client ?? client).put({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/trash/{noteId}/restore', + ...options +}); + +/** + * List trashed notes in a team workspace (team admin only; same rules as the internal API). + */ +export const listTeamTrash = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/trash', + ...options +}); + +/** + * List the teams for the current user + */ +export const listTeams = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams', + ...options +}); + +/** + * List all notes for a team + */ +export const listTeamNotes = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/notes', + ...options +}); + +/** + * Create a new note for a team + */ +export const createTeamNote = (options: Options): RequestResult => (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/notes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete a team note + */ +export const deleteTeamNote = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/notes/{noteId}', + ...options +}); + +/** + * Get a single note for a team + */ +export const getTeamNote = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/notes/{noteId}', + ...options +}); + +/** + * Update a team note + */ +export const updateTeamNote = (options: Options): RequestResult => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/notes/{noteId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * List all folders in a team workspace + */ +export const listTeamFolders = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders', + ...options +}); + +/** + * Create a new folder in a team workspace + */ +export const createTeamFolder = (options: Options): RequestResult => (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Get your personal folder ordering for this team workspace (parent folder id or `root` → ordered child folder ids). + * Uses the same folder UUIDs as (not internal Yjs clientIds). + */ +export const getTeamFolderOrder = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders/folder-order', + ...options +}); + +/** + * Replace your personal folder ordering for this team workspace. + */ +export const updateTeamFolderOrder = (options: Options): RequestResult => (options.client ?? client).put({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders/folder-order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete a folder in a team workspace + */ +export const deleteTeamFolder = (options: Options): RequestResult => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders/{folderId}', + ...options +}); + +/** + * Get a single folder in a team workspace + */ +export const getTeamFolder = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders/{folderId}', + ...options +}); + +/** + * Update a folder in a team workspace + */ +export const updateTeamFolder = (options: Options): RequestResult => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/teams/{teampath}/folders/{folderId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * List note versions + * + * List saved versions for a note. + */ +export const listVersions = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/versions', + ...options +}); + +/** + * Update named version metadata + * + * Update an existing named version's metadata with `PATCH /notes/{noteId}/versions` + * and a `base` body field formatted as `version:{id}`. The item path + * `/notes/{noteId}/versions/{versionId}` does not support PATCH. + */ +export const updateVersion = (options: Options): RequestResult => (options.client ?? client).patch({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/versions', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Create a named note version + * + * Create a named version from live note content or an existing saved version. When + * live note content has changed, this creates and names a new saved version. When + * live content matches the latest system version, that version is named in place and + * returned with 201. When it matches the latest user-named version, the response is + * 409 `no_content_change`. Naming an already named `version:{id}` returns 409 + * `version_already_named`. + */ +export const createVersion = (options: Options): RequestResult => (options.client ?? client).post({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/versions', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Compare note versions + * + * Compare two saved versions or a saved version with live note content. + */ +export const compareVersions = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/versions/compare', + ...options +}); + +/** + * Get a note version + * + * Get one saved version with reconstructed content when available. + */ +export const getVersion = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/versions/{versionId}', + ...options +}); + +/** + * Upload an image for a note. + */ +export const uploadNoteImage = (options: Options): RequestResult => (options.client ?? client).post({ + ...formDataBodySerializer, + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/images', + ...options, + headers: { + 'Content-Type': null, + ...options.headers + } +}); + +/** + * List comments and replies visible on a note. + */ +export const listNoteComments = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/comments', + ...options +}); + +/** + * Get one comment or reply from a note. + */ +export const getNoteComment = (options: Options): RequestResult => (options.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/comments/{commentId}', + ...options +}); + +/** + * Unresolve a comment or thread. + */ +export const unresolveNoteComment = (options: Options): RequestResult => (options.client ?? client).delete({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/comments/{commentId}/resolution', + ...options +}); + +/** + * Resolve a comment or thread. + */ +export const resolveNoteComment = (options: Options): RequestResult => (options.client ?? client).put({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/notes/{noteId}/comments/{commentId}/resolution', + ...options +}); + +/** + * Get the current user's profile + */ +export const getCurrentUser = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/me', + ...options +}); + +/** + * Get note history for the current user + */ +export const getHistory = (options?: Options): RequestResult => (options?.client ?? client).get({ + responseType: 'json', + security: [{ scheme: 'bearer', type: 'http' }], + url: '/history', + ...options +}); diff --git a/nodejs/src/generated/types.gen.ts b/nodejs/src/generated/types.gen.ts new file mode 100644 index 0000000..4ca67f6 --- /dev/null +++ b/nodejs/src/generated/types.gen.ts @@ -0,0 +1,2448 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseURL: `${string}://${string}` | (string & {}); +}; + +export type ApiWebhookScope = { + type: 'workspace'; +} | { + folderId: string; + type: 'folder'; +}; + +export type ApiWebhook = { + updatedAt: number; + createdAt: number; + eventDeliveryMode: 'all_supported_events'; + deliveryPolicyReason: 'unsafe_destination' | 'suspicious_pattern' | 'policy_violation' | null; + deliveryPolicyStatus: 'normal' | 'suppressed'; + configuredStatusReason: 'scope_deleted' | null; + configuredStatus: 'active' | 'disabled'; + scope: ApiWebhookScope; + url: string; + name: string; + id: string; +}; + +export type WebhookApiErrorResponse = { + error: { + message: string; + code: string; + }; +}; + +export type ApiValidationFieldError = { + message: string; + value?: unknown; +}; + +/** + * Construct a type with a set of properties K of type T + */ +export type RecordStringApiValidationFieldError = { + [key: string]: ApiValidationFieldError; +}; + +export type ApiValidationError = { + message: 'Validation Failed'; + details: RecordStringApiValidationFieldError; +}; + +export type CreateWebhookData = { + scope: { + type: 'workspace'; + } | { + folderId: string; + type: 'folder'; + }; + url: string; + name?: string | null; +}; + +export type CreateApiWebhookBody = CreateWebhookData & { + active?: boolean; +}; + +export type UpdateWebhookData = { + active?: boolean; + url?: string; + name?: string | null; +}; + +export type UpdateApiWebhookBody = UpdateWebhookData; + +export type WebhookDeliveryStatusCategory = 'delivered' | 'delivery_issue' | 'not_delivered'; + +export type ApiWebhookDelivery = { + deliveredAt: number | null; + createdAt: number; + latencyMs: number | null; + errorMessage: string | null; + errorCategory: string | null; + httpStatus: number | null; + statusCategory: WebhookDeliveryStatusCategory; + status: 'success' | 'failed' | 'skipped'; + attemptKind: 'event' | 'ping'; + eventType: string; + id: string; +}; + +export type PaginatedQuery = { + limit: number; + page: number; +}; + +export type PaginatedResponseApiWebhookDelivery = { + data: Array; + meta: PaginatedQuery & { + totalPages?: number; + total?: number; + }; +}; + +export type ApiWebhookPage = number; + +export type ApiWebhookLimit = number; + +export type SimpleUserProfile = { + name: string; + userPath: string; + photo: string; + biography: string | null; +}; + +export type NotePublishType = 'edit' | 'view' | 'slide' | 'book'; + +export type NotePermissionRole = 'owner' | 'signed_in' | 'guest'; + +export type FolderPath = { + id: string; + name: string; + icon: string | null; + color: string | null; + parentId: string | null; + clientId: string; +}; + +export type NoteType = { + folderPaths?: Array; + writePermission: NotePermissionRole; + readPermission: NotePermissionRole; + shortId: string; + publishLink: string; + permalink: string | null; + teamPath: string | null; + userPath: string | null; + publishedAt: number | null; + publishType: NotePublishType; + lastChangeUser: SimpleUserProfile | null; + tagsUpdatedAt: number | null; + titleUpdatedAt: number | null; + createdAt: number; + lastChangedAt: number; + description: string; + tags: Array; + title: string; + id: string; +}; + +export type SingleNote = { + folderPaths?: Array; + writePermission: NotePermissionRole; + readPermission: NotePermissionRole; + shortId: string; + publishLink: string; + permalink: string | null; + teamPath: string | null; + userPath: string | null; + publishedAt: number | null; + publishType: NotePublishType; + lastChangeUser: SimpleUserProfile | null; + tagsUpdatedAt: number | null; + titleUpdatedAt: number | null; + createdAt: number; + lastChangedAt: number; + description: string; + tags: Array; + title: string; + id: string; + content: string; +}; + +export type ApiErrorResponse = { + error: string; +}; + +export type CreateNoteMultiStatusResponse = { + note: SingleNote; + error: string; +}; + +export type ApiRequestErrorResponse = { + message: string; +}; + +export type ApiBadRequestResponse = ApiErrorResponse | ApiRequestErrorResponse; + +export type CommentPermissionType = 'disabled' | 'forbidden' | 'owners' | 'signed_in_users' | 'everyone'; + +export type SuggestEditPermissionType = 'disabled' | 'forbidden' | 'owners' | 'signed_in_users'; + +export type ApiNoteFeatures = { + 'emoji-reply'?: 'disabled' | 'signed_in_users'; + citation?: 'disabled' | 'signed_in_users'; +}; + +/** + * Folder name. + */ +export type FolderName = string; + +/** + * Folder icon as an emoji unified codepoint string. + * Examples: `1F525` (🔥), `2764-FE0F` (❤️). + */ +export type FolderIconCodepoint = string; + +/** + * Folder display color in hexadecimal format. + * Accepts `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA`. + */ +export type FolderColorHex = string; + +export type ApiFolder = { + id: string; + name: FolderName; + description: string | null; + icon: FolderIconCodepoint | null; + color: FolderColorHex | null; + parentFolderId: string | null; + createdAt: number; + updatedAt: number; +}; + +/** + * Per-user display order of folders: maps each parent folder id (or `root`) to + * an ordered list of child folder ids. Keys and values use the same UUIDs as + * {@link ApiFolder.id} (not Yjs clientIds stored internally in preferences). + */ +export type ApiFolderOrder = { + [key: string]: Array; +}; + +export type UpdateFolderOrderBody = { + order: ApiFolderOrder; +}; + +export type CreateUserFolderBody = { + color?: FolderColorHex; + icon?: FolderIconCodepoint; + description?: string; + parentFolderId?: string; + name?: FolderName; +}; + +export type UpdateUserFolderBody = { + parentFolderId?: string | null; + color?: FolderColorHex | null; + icon?: FolderIconCodepoint | null; + description?: string | null; + name?: FolderName; +}; + +export type ApiTrashNote = { + id: string; + title: string; + tags: Array; + deletedAt: number; + ownerId: string; + teamId: string | null; + shortId: string; + publishType: string; + permanentDeletedAt: number | null; +}; + +export type BatchRestoreTrashBody = { + noteIds: Array; +}; + +export type BatchOperationError = 'NOT_FOUND' | 'PERMISSION_DENIED' | 'INTERNAL_ERROR'; + +export type TrashBatchOperationResponse = { + [key: string]: { + reason?: BatchOperationError; + status: 'success' | 'failure'; + }; +}; + +export type TeamVisibilityType = 'public' | 'private'; + +export type Team = { + id: string; + ownerId: string; + name: string; + logo: string; + path: string; + description: string | null; + visibility: TeamVisibilityType; + upgraded: boolean; + createdAt: number; +}; + +export type CreateTeamFolderBody = { + color?: FolderColorHex; + icon?: FolderIconCodepoint; + description?: string; + parentFolderId?: string; + name?: FolderName; +}; + +export type UpdateTeamFolderBody = { + parentFolderId?: string | null; + color?: FolderColorHex | null; + icon?: FolderIconCodepoint | null; + description?: string | null; + name?: FolderName; +}; + +export type NoteVersionUser = { + user_id: string; + display_name: string; + photo?: string; +}; + +export type NoteVersionMetadata = { + id: string; + name: string; + name_source: 'user' | 'system'; + description?: string | null; + created_at: string; + created_by: NoteVersionUser | null; + authorship: Array; + content_available: boolean; +}; + +export type GetNoteVersions = { + data: Array; + meta: { + total_pages: number; + total: number; + limit: number; + page: number; + }; +}; + +export type ApiError = { + error_code: string; + message: string; + unavailable_ref?: string; +}; + +export type CompareNoteVersions = { + unified_diff: string; +}; + +export type NoteVersion = { + id: string; + name: string; + name_source: 'user' | 'system'; + description?: string | null; + created_at: string; + created_by: NoteVersionUser | null; + authorship: Array; + content_available: boolean; + content?: string; +}; + +export type NoContentChangeError = { + error_code: 'no_content_change'; + message: string; + latest_version_id: string; + latest_name_source: 'system' | 'user'; +}; + +export type VersionAlreadyNamedError = { + error_code: 'version_already_named'; + message: string; + version_id: string; + name: string; +}; + +export type CreateNoteVersionConflictError = NoContentChangeError | VersionAlreadyNamedError; + +export type CreateNoteVersionBody = { + /** + * Saved content to name. Accepts note_content or version:{id}. + */ + base?: string; + name: string; + description?: string; +}; + +export type VersionNotNamedError = { + error_code: 'version_not_named'; + message: string; + version_id: string; +}; + +export type UpdateNoteVersionBody = { + description?: string; + name: string; + /** + * Existing named version to update, formatted as version:{id}. + */ + base: string; +} | { + description: string; + name?: string; + /** + * Existing named version to update, formatted as version:{id}. + */ + base: string; +}; + +export type NoteImageUploadResponse = { + data: { + link: string; + }; +}; + +export type ApiCommentStatus = 'open' | 'hidden'; + +export type ApiCommentStateReason = 'resolved' | 'spam' | 'abuse' | 'off_topic' | null; + +export type ApiCommentStateUserActor = { + kind: 'user'; + userId: string; + displayName: string; +}; + +export type ApiCommentStateDeletedUserActor = { + kind: 'deleted_user'; +}; + +export type ApiCommentStateActor = ApiCommentStateUserActor | ApiCommentStateDeletedUserActor; + +export type ApiCommentThreadState = { + status: ApiCommentStatus; + reason: ApiCommentStateReason; + actedBy: ApiCommentStateActor | null; + /** + * Unix timestamp in milliseconds. + */ + actedAt: number | null; + sourceCommentId: string; +}; + +export type ApiCommentUserActor = { + kind: 'user'; + userId: string; + displayName: string; +}; + +export type ApiCommentGuestActor = { + kind: 'guest'; + displayName: string; +}; + +export type ApiCommentTeamActor = { + kind: 'team'; + teamId: string; + displayName: string; +}; + +export type ApiCommentDeletedUserActor = { + kind: 'deleted_user'; +}; + +export type ApiCommentActor = ApiCommentUserActor | ApiCommentGuestActor | ApiCommentTeamActor | ApiCommentDeletedUserActor; + +export type ApiCommentState = { + status: ApiCommentStatus; + reason: ApiCommentStateReason; + actedBy: ApiCommentStateActor | null; + /** + * Unix timestamp in milliseconds. + */ + actedAt: number | null; +}; + +export type ApiCommentAnchor = { + offset: number; + length: number; +}; + +export type ApiCommentReactionShortcode = '+1' | '-1' | 'joy' | 'tada' | 'thinking_face' | 'heart' | 'rocket' | 'eyes'; + +export type ApiCommentReactionSummary = { + shortcode: ApiCommentReactionShortcode; + count: number; +}; + +export type ApiCommentHead = { + id: string; + threadId: string; + actor: ApiCommentActor; + body: string; + contentType: 'plain_text'; + commentState: ApiCommentState; + isPinned: boolean; + anchor: ApiCommentAnchor | null; + currentAnchor: ApiCommentAnchor | null; + excerpt: string | null; + reactions: Array; + /** + * Unix timestamp in milliseconds. + */ + createdAt: number; + /** + * Unix timestamp in milliseconds. + */ + updatedAt: number; + isThreadHead: true; + replyCount: number; + threadState: ApiCommentThreadState; +}; + +export type ApiCommentListReply = { + id: string; + threadId: string; + actor: ApiCommentActor; + body: string; + contentType: 'plain_text'; + commentState: ApiCommentState; + isPinned: boolean; + anchor: ApiCommentAnchor | null; + currentAnchor: ApiCommentAnchor | null; + excerpt: string | null; + reactions: Array; + /** + * Unix timestamp in milliseconds. + */ + createdAt: number; + /** + * Unix timestamp in milliseconds. + */ + updatedAt: number; + isThreadHead: false; +}; + +export type ApiComment = ApiCommentHead | ApiCommentListReply; + +export type ApiCommentPagination = { + page: number; + limit: number; + hasNext: boolean; +}; + +export type GetNoteComments = { + comments: Array; + pagination: ApiCommentPagination; +}; + +export type ApiCommentError = { + errorCode: string; + message: string; +}; + +export type ApiCommentPage = number; + +export type ApiCommentLimit = number; + +export type ApiCommentThreadId = string; + +export type ApiCommentDetailReply = { + id: string; + threadId: string; + actor: ApiCommentActor; + body: string; + contentType: 'plain_text'; + commentState: ApiCommentState; + isPinned: boolean; + anchor: ApiCommentAnchor | null; + currentAnchor: ApiCommentAnchor | null; + excerpt: string | null; + reactions: Array; + /** + * Unix timestamp in milliseconds. + */ + createdAt: number; + /** + * Unix timestamp in milliseconds. + */ + updatedAt: number; + isThreadHead: false; + threadState: ApiCommentThreadState; +}; + +export type ApiCommentDetail = ApiCommentHead | ApiCommentDetailReply; + +export type ApiCommentId = string; + +export type ApiCommentResolutionCascade = { + threadId: string; +}; + +export type ApiCommentResolutionResponse = { + comment: ApiCommentDetail; + cascade: ApiCommentResolutionCascade | null; +}; + +export type ApiCommentResolutionConflictError = { + errorCode: string; + message: string; + commentState: ApiCommentState; +}; + +export type User = { + id: string; + email: string | null; + name: string; + userPath: string; + photo: string; + teams: Array; + Teams?: Array; + upgraded: boolean; +}; + +export type Note = { + folderPaths?: Array; + writePermission: NotePermissionRole; + readPermission: NotePermissionRole; + shortId: string; + publishLink: string; + permalink: string | null; + teamPath: string | null; + userPath: string | null; + publishedAt: number | null; + publishType: NotePublishType; + lastChangeUser: SimpleUserProfile | null; + tagsUpdatedAt: number | null; + titleUpdatedAt: number | null; + createdAt: number; + lastChangedAt: number; + description: string; + tags: Array; + title: string; + id: string; +}; + +export type GetUserHistory = Array; + +export type ListWebhooksData = { + body?: never; + path?: never; + query?: never; + url: '/webhooks'; +}; + +export type ListWebhooksResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListWebhooksResponse = ListWebhooksResponses[keyof ListWebhooksResponses]; + +export type CreateWebhookData2 = { + body: CreateApiWebhookBody; + path?: never; + query?: never; + url: '/webhooks'; +}; + +export type CreateWebhookErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Folder not found + */ + 404: WebhookApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + /** + * Webhook limit reached + */ + 429: WebhookApiErrorResponse; +}; + +export type CreateWebhookError = CreateWebhookErrors[keyof CreateWebhookErrors]; + +export type CreateWebhookResponses = { + /** + * Created + */ + 201: ApiWebhook & { + secret: string; + }; +}; + +export type CreateWebhookResponse = CreateWebhookResponses[keyof CreateWebhookResponses]; + +export type DeleteWebhookData = { + body?: never; + path: { + hookId: string; + }; + query?: never; + url: '/webhooks/{hookId}'; +}; + +export type DeleteWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type DeleteWebhookError = DeleteWebhookErrors[keyof DeleteWebhookErrors]; + +export type DeleteWebhookResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteWebhookResponse = DeleteWebhookResponses[keyof DeleteWebhookResponses]; + +export type GetWebhookData = { + body?: never; + path: { + hookId: string; + }; + query?: never; + url: '/webhooks/{hookId}'; +}; + +export type GetWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type GetWebhookError = GetWebhookErrors[keyof GetWebhookErrors]; + +export type GetWebhookResponses = { + /** + * Ok + */ + 200: ApiWebhook; +}; + +export type GetWebhookResponse = GetWebhookResponses[keyof GetWebhookResponses]; + +export type UpdateWebhookData2 = { + body: UpdateWebhookData; + path: { + hookId: string; + }; + query?: never; + url: '/webhooks/{hookId}'; +}; + +export type UpdateWebhookErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type UpdateWebhookError = UpdateWebhookErrors[keyof UpdateWebhookErrors]; + +export type UpdateWebhookResponses = { + /** + * Ok + */ + 200: ApiWebhook; +}; + +export type UpdateWebhookResponse = UpdateWebhookResponses[keyof UpdateWebhookResponses]; + +export type PingWebhookData = { + body?: never; + path: { + hookId: string; + }; + query?: never; + url: '/webhooks/{hookId}/ping'; +}; + +export type PingWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; + /** + * Too many ping requests + */ + 429: WebhookApiErrorResponse; +}; + +export type PingWebhookError = PingWebhookErrors[keyof PingWebhookErrors]; + +export type PingWebhookResponses = { + /** + * No content + */ + 204: void; +}; + +export type PingWebhookResponse = PingWebhookResponses[keyof PingWebhookResponses]; + +export type ListWebhookDeliveriesData = { + body?: never; + path: { + hookId: string; + }; + query?: { + page?: ApiWebhookPage; + limit?: ApiWebhookLimit; + }; + url: '/webhooks/{hookId}/deliveries'; +}; + +export type ListWebhookDeliveriesErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type ListWebhookDeliveriesError = ListWebhookDeliveriesErrors[keyof ListWebhookDeliveriesErrors]; + +export type ListWebhookDeliveriesResponses = { + /** + * Ok + */ + 200: PaginatedResponseApiWebhookDelivery; +}; + +export type ListWebhookDeliveriesResponse = ListWebhookDeliveriesResponses[keyof ListWebhookDeliveriesResponses]; + +export type ExportWebhookDeliveriesData = { + body?: never; + path: { + hookId: string; + }; + query?: never; + url: '/webhooks/{hookId}/deliveries/export'; +}; + +export type ExportWebhookDeliveriesErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type ExportWebhookDeliveriesError = ExportWebhookDeliveriesErrors[keyof ExportWebhookDeliveriesErrors]; + +export type ExportWebhookDeliveriesResponses = { + /** + * Ok + */ + 200: string; +}; + +export type ExportWebhookDeliveriesResponse = ExportWebhookDeliveriesResponses[keyof ExportWebhookDeliveriesResponses]; + +export type GetWebhookDeliveryData = { + body?: never; + path: { + hookId: string; + deliveryId: string; + }; + query?: never; + url: '/webhooks/{hookId}/deliveries/{deliveryId}'; +}; + +export type GetWebhookDeliveryErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Delivery not found + */ + 404: WebhookApiErrorResponse; +}; + +export type GetWebhookDeliveryError = GetWebhookDeliveryErrors[keyof GetWebhookDeliveryErrors]; + +export type GetWebhookDeliveryResponses = { + /** + * Ok + */ + 200: ApiWebhookDelivery; +}; + +export type GetWebhookDeliveryResponse = GetWebhookDeliveryResponses[keyof GetWebhookDeliveryResponses]; + +export type ListTeamWebhooksData = { + body?: never; + path: { + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks'; +}; + +export type ListTeamWebhooksResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListTeamWebhooksResponse = ListTeamWebhooksResponses[keyof ListTeamWebhooksResponses]; + +export type CreateTeamWebhookData = { + body: CreateApiWebhookBody; + path: { + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks'; +}; + +export type CreateTeamWebhookErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Folder not found + */ + 404: WebhookApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + /** + * Webhook limit reached + */ + 429: WebhookApiErrorResponse; +}; + +export type CreateTeamWebhookError = CreateTeamWebhookErrors[keyof CreateTeamWebhookErrors]; + +export type CreateTeamWebhookResponses = { + /** + * Created + */ + 201: ApiWebhook & { + secret: string; + }; +}; + +export type CreateTeamWebhookResponse = CreateTeamWebhookResponses[keyof CreateTeamWebhookResponses]; + +export type DeleteTeamWebhookData = { + body?: never; + path: { + teampath: string; + hookId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}'; +}; + +export type DeleteTeamWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type DeleteTeamWebhookError = DeleteTeamWebhookErrors[keyof DeleteTeamWebhookErrors]; + +export type DeleteTeamWebhookResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteTeamWebhookResponse = DeleteTeamWebhookResponses[keyof DeleteTeamWebhookResponses]; + +export type GetTeamWebhookData = { + body?: never; + path: { + teampath: string; + hookId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}'; +}; + +export type GetTeamWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type GetTeamWebhookError = GetTeamWebhookErrors[keyof GetTeamWebhookErrors]; + +export type GetTeamWebhookResponses = { + /** + * Ok + */ + 200: ApiWebhook; +}; + +export type GetTeamWebhookResponse = GetTeamWebhookResponses[keyof GetTeamWebhookResponses]; + +export type UpdateTeamWebhookData = { + body: UpdateWebhookData; + path: { + teampath: string; + hookId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}'; +}; + +export type UpdateTeamWebhookErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type UpdateTeamWebhookError = UpdateTeamWebhookErrors[keyof UpdateTeamWebhookErrors]; + +export type UpdateTeamWebhookResponses = { + /** + * Ok + */ + 200: ApiWebhook; +}; + +export type UpdateTeamWebhookResponse = UpdateTeamWebhookResponses[keyof UpdateTeamWebhookResponses]; + +export type PingTeamWebhookData = { + body?: never; + path: { + teampath: string; + hookId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}/ping'; +}; + +export type PingTeamWebhookErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; + /** + * Too many ping requests + */ + 429: WebhookApiErrorResponse; +}; + +export type PingTeamWebhookError = PingTeamWebhookErrors[keyof PingTeamWebhookErrors]; + +export type PingTeamWebhookResponses = { + /** + * No content + */ + 204: void; +}; + +export type PingTeamWebhookResponse = PingTeamWebhookResponses[keyof PingTeamWebhookResponses]; + +export type ListTeamWebhookDeliveriesData = { + body?: never; + path: { + teampath: string; + hookId: string; + }; + query?: { + page?: ApiWebhookPage; + limit?: ApiWebhookLimit; + }; + url: '/teams/{teampath}/webhooks/{hookId}/deliveries'; +}; + +export type ListTeamWebhookDeliveriesErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type ListTeamWebhookDeliveriesError = ListTeamWebhookDeliveriesErrors[keyof ListTeamWebhookDeliveriesErrors]; + +export type ListTeamWebhookDeliveriesResponses = { + /** + * Ok + */ + 200: PaginatedResponseApiWebhookDelivery; +}; + +export type ListTeamWebhookDeliveriesResponse = ListTeamWebhookDeliveriesResponses[keyof ListTeamWebhookDeliveriesResponses]; + +export type ExportTeamWebhookDeliveriesData = { + body?: never; + path: { + teampath: string; + hookId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}/deliveries/export'; +}; + +export type ExportTeamWebhookDeliveriesErrors = { + /** + * Invalid webhook ID + */ + 400: WebhookApiErrorResponse; + /** + * Webhook not found + */ + 404: WebhookApiErrorResponse; +}; + +export type ExportTeamWebhookDeliveriesError = ExportTeamWebhookDeliveriesErrors[keyof ExportTeamWebhookDeliveriesErrors]; + +export type ExportTeamWebhookDeliveriesResponses = { + /** + * Ok + */ + 200: string; +}; + +export type ExportTeamWebhookDeliveriesResponse = ExportTeamWebhookDeliveriesResponses[keyof ExportTeamWebhookDeliveriesResponses]; + +export type GetTeamWebhookDeliveryData = { + body?: never; + path: { + teampath: string; + hookId: string; + deliveryId: string; + }; + query?: never; + url: '/teams/{teampath}/webhooks/{hookId}/deliveries/{deliveryId}'; +}; + +export type GetTeamWebhookDeliveryErrors = { + /** + * Invalid request + */ + 400: WebhookApiErrorResponse; + /** + * Delivery not found + */ + 404: WebhookApiErrorResponse; +}; + +export type GetTeamWebhookDeliveryError = GetTeamWebhookDeliveryErrors[keyof GetTeamWebhookDeliveryErrors]; + +export type GetTeamWebhookDeliveryResponses = { + /** + * Ok + */ + 200: ApiWebhookDelivery; +}; + +export type GetTeamWebhookDeliveryResponse = GetTeamWebhookDeliveryResponses[keyof GetTeamWebhookDeliveryResponses]; + +export type ListNotesData = { + body?: never; + path?: never; + query?: never; + url: '/notes'; +}; + +export type ListNotesResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListNotesResponse = ListNotesResponses[keyof ListNotesResponses]; + +export type CreateNoteData = { + /** + * The note content or a JSON object with note properties. + */ + body?: { + origin?: string; + parentFolderId?: string; + permalink?: string; + noteFeatures?: ApiNoteFeatures; + suggestEditPermission?: SuggestEditPermissionType; + commentPermission?: CommentPermissionType; + writePermission?: NotePermissionRole; + readPermission?: NotePermissionRole; + content?: string; + description?: string; + tags?: Array; + title?: string; + } | string; + path?: never; + query?: never; + url: '/notes'; +}; + +export type CreateNoteErrors = { + 400: ApiBadRequestResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 409: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type CreateNoteError = CreateNoteErrors[keyof CreateNoteErrors]; + +export type CreateNoteResponses = { + 201: SingleNote; + 207: CreateNoteMultiStatusResponse; +}; + +export type CreateNoteResponse = CreateNoteResponses[keyof CreateNoteResponses]; + +export type DeleteNoteData = { + body?: never; + path: { + /** + * The ID of the note to delete. + */ + noteId: string; + }; + query?: never; + url: '/notes/{noteId}'; +}; + +export type DeleteNoteErrors = { + 400: ApiErrorResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type DeleteNoteError = DeleteNoteErrors[keyof DeleteNoteErrors]; + +export type DeleteNoteResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteNoteResponse = DeleteNoteResponses[keyof DeleteNoteResponses]; + +export type GetNoteData = { + body?: never; + path: { + /** + * The ID of the note to retrieve. + */ + noteId: string; + }; + query?: never; + url: '/notes/{noteId}'; +}; + +export type GetNoteErrors = { + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type GetNoteError = GetNoteErrors[keyof GetNoteErrors]; + +export type GetNoteResponses = { + 200: SingleNote; +}; + +export type GetNoteResponse = GetNoteResponses[keyof GetNoteResponses]; + +export type UpdateNoteData = { + /** + * The properties to update on the note. + */ + body: { + parentFolderId?: string | null; + permalink?: string; + writePermission?: NotePermissionRole; + readPermission?: NotePermissionRole; + content?: string; + description?: string; + tags?: Array; + title?: string; + }; + path: { + /** + * The ID of the note to update. + */ + noteId: string; + }; + query?: never; + url: '/notes/{noteId}'; +}; + +export type UpdateNoteErrors = { + 400: ApiBadRequestResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 409: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type UpdateNoteError = UpdateNoteErrors[keyof UpdateNoteErrors]; + +export type UpdateNoteResponses = { + 202: unknown; +}; + +export type ListFoldersData = { + body?: never; + path?: never; + query?: never; + url: '/folders'; +}; + +export type ListFoldersResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListFoldersResponse = ListFoldersResponses[keyof ListFoldersResponses]; + +export type CreateFolderData = { + /** + * The folder properties. + */ + body?: CreateUserFolderBody; + path?: never; + query?: never; + url: '/folders'; +}; + +export type CreateFolderErrors = { + 400: ApiBadRequestResponse; + 404: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type CreateFolderError = CreateFolderErrors[keyof CreateFolderErrors]; + +export type CreateFolderResponses = { + 201: ApiFolder; +}; + +export type CreateFolderResponse = CreateFolderResponses[keyof CreateFolderResponses]; + +export type GetFolderOrderData = { + body?: never; + path?: never; + query?: never; + url: '/folders/folder-order'; +}; + +export type GetFolderOrderResponses = { + /** + * Ok + */ + 200: ApiFolderOrder; +}; + +export type GetFolderOrderResponse = GetFolderOrderResponses[keyof GetFolderOrderResponses]; + +export type UpdateFolderOrderData = { + body?: UpdateFolderOrderBody; + path?: never; + query?: never; + url: '/folders/folder-order'; +}; + +export type UpdateFolderOrderErrors = { + 400: ApiBadRequestResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type UpdateFolderOrderError = UpdateFolderOrderErrors[keyof UpdateFolderOrderErrors]; + +export type UpdateFolderOrderResponses = { + 204: void; +}; + +export type UpdateFolderOrderResponse = UpdateFolderOrderResponses[keyof UpdateFolderOrderResponses]; + +export type DeleteFolderData = { + body?: never; + path: { + /** + * The ID of the folder to delete. + */ + folderId: string; + }; + query?: never; + url: '/folders/{folderId}'; +}; + +export type DeleteFolderErrors = { + 400: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type DeleteFolderError = DeleteFolderErrors[keyof DeleteFolderErrors]; + +export type DeleteFolderResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteFolderResponse = DeleteFolderResponses[keyof DeleteFolderResponses]; + +export type GetFolderData = { + body?: never; + path: { + /** + * The ID of the folder to retrieve. + */ + folderId: string; + }; + query?: never; + url: '/folders/{folderId}'; +}; + +export type GetFolderErrors = { + 400: ApiErrorResponse; + 404: ApiErrorResponse; +}; + +export type GetFolderError = GetFolderErrors[keyof GetFolderErrors]; + +export type GetFolderResponses = { + 200: ApiFolder; +}; + +export type GetFolderResponse = GetFolderResponses[keyof GetFolderResponses]; + +export type UpdateFolderData = { + /** + * The properties to update. + */ + body?: UpdateUserFolderBody; + path: { + /** + * The ID of the folder to update. + */ + folderId: string; + }; + query?: never; + url: '/folders/{folderId}'; +}; + +export type UpdateFolderErrors = { + 400: ApiBadRequestResponse; + 404: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type UpdateFolderError = UpdateFolderErrors[keyof UpdateFolderErrors]; + +export type UpdateFolderResponses = { + 202: unknown; +}; + +export type ListPersonalTrashData = { + body?: never; + path?: never; + query?: never; + url: '/trash'; +}; + +export type ListPersonalTrashResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListPersonalTrashResponse = ListPersonalTrashResponses[keyof ListPersonalTrashResponses]; + +export type BatchRestoreData = { + body: BatchRestoreTrashBody; + path?: never; + query?: never; + url: '/trash/batch-restore'; +}; + +export type BatchRestoreErrors = { + 404: ApiErrorResponse; +}; + +export type BatchRestoreError = BatchRestoreErrors[keyof BatchRestoreErrors]; + +export type BatchRestoreResponses = { + 200: TrashBatchOperationResponse; + 207: TrashBatchOperationResponse; +}; + +export type BatchRestoreResponse = BatchRestoreResponses[keyof BatchRestoreResponses]; + +export type RestoreNoteData = { + body?: never; + path: { + /** + * Encoded note id of the trashed note. + */ + noteId: string; + }; + query?: never; + url: '/trash/{noteId}/restore'; +}; + +export type RestoreNoteErrors = { + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type RestoreNoteError = RestoreNoteErrors[keyof RestoreNoteErrors]; + +export type RestoreNoteResponses = { + 204: void; +}; + +export type RestoreNoteResponse = RestoreNoteResponses[keyof RestoreNoteResponses]; + +export type ListTeamTrashData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/trash'; +}; + +export type ListTeamTrashErrors = { + 403: ApiErrorResponse | string; + 404: ApiErrorResponse | string; +}; + +export type ListTeamTrashError = ListTeamTrashErrors[keyof ListTeamTrashErrors]; + +export type ListTeamTrashResponses = { + 200: Array; +}; + +export type ListTeamTrashResponse = ListTeamTrashResponses[keyof ListTeamTrashResponses]; + +export type ListTeamsData = { + body?: never; + path?: never; + query?: never; + url: '/teams'; +}; + +export type ListTeamsResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListTeamsResponse = ListTeamsResponses[keyof ListTeamsResponses]; + +export type ListTeamNotesData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/notes'; +}; + +export type ListTeamNotesResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListTeamNotesResponse = ListTeamNotesResponses[keyof ListTeamNotesResponses]; + +export type CreateTeamNoteData = { + /** + * The note content (string) or a JSON object with note properties (title, content, permissions, etc.). + */ + body?: { + origin?: string; + parentFolderId?: string; + permalink?: string; + noteFeatures?: ApiNoteFeatures; + suggestEditPermission?: SuggestEditPermissionType; + commentPermission?: CommentPermissionType; + writePermission?: NotePermissionRole; + readPermission?: NotePermissionRole; + content?: string; + description?: string; + tags?: Array; + title?: string; + } | string; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/notes'; +}; + +export type CreateTeamNoteErrors = { + 400: ApiBadRequestResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 409: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type CreateTeamNoteError = CreateTeamNoteErrors[keyof CreateTeamNoteErrors]; + +export type CreateTeamNoteResponses = { + 201: SingleNote; + 207: CreateNoteMultiStatusResponse; +}; + +export type CreateTeamNoteResponse = CreateTeamNoteResponses[keyof CreateTeamNoteResponses]; + +export type DeleteTeamNoteData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the note to delete. + */ + noteId: string; + }; + query?: never; + url: '/teams/{teampath}/notes/{noteId}'; +}; + +export type DeleteTeamNoteErrors = { + 400: ApiErrorResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type DeleteTeamNoteError = DeleteTeamNoteErrors[keyof DeleteTeamNoteErrors]; + +export type DeleteTeamNoteResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteTeamNoteResponse = DeleteTeamNoteResponses[keyof DeleteTeamNoteResponses]; + +export type GetTeamNoteData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the note to retrieve. + */ + noteId: string; + }; + query?: never; + url: '/teams/{teampath}/notes/{noteId}'; +}; + +export type GetTeamNoteErrors = { + 403: ApiErrorResponse; + 404: ApiErrorResponse; +}; + +export type GetTeamNoteError = GetTeamNoteErrors[keyof GetTeamNoteErrors]; + +export type GetTeamNoteResponses = { + 200: SingleNote; +}; + +export type GetTeamNoteResponse = GetTeamNoteResponses[keyof GetTeamNoteResponses]; + +export type UpdateTeamNoteData = { + /** + * The properties to update on the team note (e.g., content, permissions, permalink, parentFolderId). + */ + body: { + parentFolderId?: string | null; + permalink?: string; + writePermission?: NotePermissionRole; + readPermission?: NotePermissionRole; + content?: string; + description?: string; + tags?: Array; + title?: string; + }; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the note to update. + */ + noteId: string; + }; + query?: never; + url: '/teams/{teampath}/notes/{noteId}'; +}; + +export type UpdateTeamNoteErrors = { + 400: ApiBadRequestResponse; + 403: ApiErrorResponse; + 404: ApiErrorResponse; + 409: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type UpdateTeamNoteError = UpdateTeamNoteErrors[keyof UpdateTeamNoteErrors]; + +export type UpdateTeamNoteResponses = { + 202: unknown; +}; + +export type ListTeamFoldersData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/folders'; +}; + +export type ListTeamFoldersResponses = { + /** + * Ok + */ + 200: Array; +}; + +export type ListTeamFoldersResponse = ListTeamFoldersResponses[keyof ListTeamFoldersResponses]; + +export type CreateTeamFolderData = { + /** + * The folder properties. + */ + body?: CreateTeamFolderBody; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/folders'; +}; + +export type CreateTeamFolderErrors = { + 400: ApiBadRequestResponse; + 404: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type CreateTeamFolderError = CreateTeamFolderErrors[keyof CreateTeamFolderErrors]; + +export type CreateTeamFolderResponses = { + 201: ApiFolder; +}; + +export type CreateTeamFolderResponse = CreateTeamFolderResponses[keyof CreateTeamFolderResponses]; + +export type GetTeamFolderOrderData = { + body?: never; + path: { + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/folders/folder-order'; +}; + +export type GetTeamFolderOrderResponses = { + /** + * Ok + */ + 200: ApiFolderOrder; +}; + +export type GetTeamFolderOrderResponse = GetTeamFolderOrderResponses[keyof GetTeamFolderOrderResponses]; + +export type UpdateTeamFolderOrderData = { + body?: UpdateFolderOrderBody; + path: { + teampath: string; + }; + query?: never; + url: '/teams/{teampath}/folders/folder-order'; +}; + +export type UpdateTeamFolderOrderErrors = { + 400: ApiBadRequestResponse; + /** + * Validation failed + */ + 422: ApiValidationError; +}; + +export type UpdateTeamFolderOrderError = UpdateTeamFolderOrderErrors[keyof UpdateTeamFolderOrderErrors]; + +export type UpdateTeamFolderOrderResponses = { + 204: void; +}; + +export type UpdateTeamFolderOrderResponse = UpdateTeamFolderOrderResponses[keyof UpdateTeamFolderOrderResponses]; + +export type DeleteTeamFolderData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the folder to delete. + */ + folderId: string; + }; + query?: never; + url: '/teams/{teampath}/folders/{folderId}'; +}; + +export type DeleteTeamFolderErrors = { + 400: ApiErrorResponse; + 404: ApiErrorResponse; + 500: ApiErrorResponse; +}; + +export type DeleteTeamFolderError = DeleteTeamFolderErrors[keyof DeleteTeamFolderErrors]; + +export type DeleteTeamFolderResponses = { + /** + * No content + */ + 204: void; +}; + +export type DeleteTeamFolderResponse = DeleteTeamFolderResponses[keyof DeleteTeamFolderResponses]; + +export type GetTeamFolderData = { + body?: never; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the folder to retrieve. + */ + folderId: string; + }; + query?: never; + url: '/teams/{teampath}/folders/{folderId}'; +}; + +export type GetTeamFolderErrors = { + 400: ApiErrorResponse; + 404: ApiErrorResponse; +}; + +export type GetTeamFolderError = GetTeamFolderErrors[keyof GetTeamFolderErrors]; + +export type GetTeamFolderResponses = { + 200: ApiFolder; +}; + +export type GetTeamFolderResponse = GetTeamFolderResponses[keyof GetTeamFolderResponses]; + +export type UpdateTeamFolderData = { + /** + * The properties to update. + */ + body?: UpdateTeamFolderBody; + path: { + /** + * The path identifier for the team. + */ + teampath: string; + /** + * The ID of the folder to update. + */ + folderId: string; + }; + query?: never; + url: '/teams/{teampath}/folders/{folderId}'; +}; + +export type UpdateTeamFolderErrors = { + 400: ApiBadRequestResponse; + 404: ApiErrorResponse; + /** + * Validation failed + */ + 422: ApiValidationError; + 500: ApiErrorResponse; +}; + +export type UpdateTeamFolderError = UpdateTeamFolderErrors[keyof UpdateTeamFolderErrors]; + +export type UpdateTeamFolderResponses = { + 202: unknown; +}; + +export type ListVersionsData = { + body?: never; + path: { + noteId: string; + }; + query?: { + named_only?: boolean; + q?: string; + created_by?: string; + /** + * Return versions created at or after this ISO-8601 datetime. + */ + created_after?: string; + /** + * Return versions created at or before this ISO-8601 datetime. + */ + created_before?: string; + page?: number; + limit?: number; + }; + url: '/notes/{noteId}/versions'; +}; + +export type ListVersionsErrors = { + /** + * Invalid request + */ + 400: ApiError; + /** + * Forbidden + */ + 403: ApiError; + /** + * Not found + */ + 404: ApiError; +}; + +export type ListVersionsError = ListVersionsErrors[keyof ListVersionsErrors]; + +export type ListVersionsResponses = { + /** + * Ok + */ + 200: GetNoteVersions; +}; + +export type ListVersionsResponse = ListVersionsResponses[keyof ListVersionsResponses]; + +export type UpdateVersionData = { + body: UpdateNoteVersionBody; + path: { + noteId: string; + }; + query?: never; + url: '/notes/{noteId}/versions'; +}; + +export type UpdateVersionErrors = { + /** + * Invalid request + */ + 400: ApiError; + /** + * Forbidden + */ + 403: ApiError; + /** + * Not found + */ + 404: ApiError; + /** + * Version is not named + */ + 422: VersionNotNamedError; +}; + +export type UpdateVersionError = UpdateVersionErrors[keyof UpdateVersionErrors]; + +export type UpdateVersionResponses = { + /** + * Ok + */ + 200: NoteVersionMetadata; +}; + +export type UpdateVersionResponse = UpdateVersionResponses[keyof UpdateVersionResponses]; + +export type CreateVersionData = { + body: CreateNoteVersionBody; + path: { + noteId: string; + }; + query?: never; + url: '/notes/{noteId}/versions'; +}; + +export type CreateVersionErrors = { + /** + * Invalid request + */ + 400: ApiError; + /** + * Forbidden + */ + 403: ApiError; + /** + * Not found + */ + 404: ApiError; + /** + * No content change or version already named + */ + 409: CreateNoteVersionConflictError; +}; + +export type CreateVersionError = CreateVersionErrors[keyof CreateVersionErrors]; + +export type CreateVersionResponses = { + /** + * Created + */ + 201: NoteVersionMetadata; +}; + +export type CreateVersionResponse = CreateVersionResponses[keyof CreateVersionResponses]; + +export type CompareVersionsData = { + body?: never; + path: { + noteId: string; + }; + query: { + /** + * Required comparison source. Use note_content or version:{id}. + */ + base: string; + /** + * Required comparison target. Use note_content or version:{id}. + */ + target: string; + }; + url: '/notes/{noteId}/versions/compare'; +}; + +export type CompareVersionsErrors = { + /** + * Invalid request + */ + 400: ApiError; + /** + * Forbidden + */ + 403: ApiError; + /** + * Not found + */ + 404: ApiError; + /** + * Validation failed or version content unavailable + */ + 422: ApiError | ApiValidationError; +}; + +export type CompareVersionsError = CompareVersionsErrors[keyof CompareVersionsErrors]; + +export type CompareVersionsResponses = { + /** + * Ok + */ + 200: CompareNoteVersions; +}; + +export type CompareVersionsResponse = CompareVersionsResponses[keyof CompareVersionsResponses]; + +export type GetVersionData = { + body?: never; + path: { + noteId: string; + versionId: string; + }; + query?: never; + url: '/notes/{noteId}/versions/{versionId}'; +}; + +export type GetVersionErrors = { + /** + * Forbidden + */ + 403: ApiError; + /** + * Not found + */ + 404: ApiError; +}; + +export type GetVersionError = GetVersionErrors[keyof GetVersionErrors]; + +export type GetVersionResponses = { + /** + * Ok + */ + 200: NoteVersion; +}; + +export type GetVersionResponse = GetVersionResponses[keyof GetVersionResponses]; + +export type UploadNoteImageData = { + body: { + image: Blob | File; + }; + path: { + noteId: string; + }; + query?: never; + url: '/notes/{noteId}/images'; +}; + +export type UploadNoteImageErrors = { + /** + * Bad Request + */ + 400: ApiRequestErrorResponse; + /** + * Unauthorized + */ + 401: string; + /** + * Forbidden + */ + 403: ApiRequestErrorResponse; + /** + * Payload Too Large + */ + 413: ApiRequestErrorResponse; + /** + * Unsupported Media Type + */ + 415: ApiRequestErrorResponse; + 422: ApiRequestErrorResponse; + /** + * Too Many Requests + */ + 429: ApiRequestErrorResponse; +}; + +export type UploadNoteImageError = UploadNoteImageErrors[keyof UploadNoteImageErrors]; + +export type UploadNoteImageResponses = { + 201: NoteImageUploadResponse; +}; + +export type UploadNoteImageResponse = UploadNoteImageResponses[keyof UploadNoteImageResponses]; + +export type ListNoteCommentsData = { + body?: never; + path: { + noteId: string; + }; + query?: { + page?: ApiCommentPage; + limit?: ApiCommentLimit; + sort?: 'asc' | 'desc'; + threadId?: ApiCommentThreadId; + commentStatus?: ApiCommentStatus; + threadStatus?: ApiCommentStatus; + isThreadHead?: true; + }; + url: '/notes/{noteId}/comments'; +}; + +export type ListNoteCommentsErrors = { + /** + * Invalid request + */ + 400: ApiCommentError; + /** + * Comments not readable + */ + 403: ApiCommentError; + /** + * Note not found + */ + 404: ApiCommentError; +}; + +export type ListNoteCommentsError = ListNoteCommentsErrors[keyof ListNoteCommentsErrors]; + +export type ListNoteCommentsResponses = { + /** + * Ok + */ + 200: GetNoteComments; +}; + +export type ListNoteCommentsResponse = ListNoteCommentsResponses[keyof ListNoteCommentsResponses]; + +export type GetNoteCommentData = { + body?: never; + path: { + noteId: string; + commentId: ApiCommentId; + }; + query?: never; + url: '/notes/{noteId}/comments/{commentId}'; +}; + +export type GetNoteCommentErrors = { + /** + * Comments not readable + */ + 403: ApiCommentError; + /** + * Not found + */ + 404: ApiCommentError; +}; + +export type GetNoteCommentError = GetNoteCommentErrors[keyof GetNoteCommentErrors]; + +export type GetNoteCommentResponses = { + /** + * Ok + */ + 200: ApiCommentDetail; +}; + +export type GetNoteCommentResponse = GetNoteCommentResponses[keyof GetNoteCommentResponses]; + +export type UnresolveNoteCommentData = { + body?: never; + path: { + noteId: string; + commentId: ApiCommentId; + }; + query?: never; + url: '/notes/{noteId}/comments/{commentId}/resolution'; +}; + +export type UnresolveNoteCommentErrors = { + /** + * Comment resolution forbidden + */ + 403: ApiCommentError; + /** + * Not found + */ + 404: ApiCommentError; + /** + * Comment resolution conflict + */ + 409: ApiCommentResolutionConflictError; +}; + +export type UnresolveNoteCommentError = UnresolveNoteCommentErrors[keyof UnresolveNoteCommentErrors]; + +export type UnresolveNoteCommentResponses = { + /** + * Ok + */ + 200: ApiCommentResolutionResponse; +}; + +export type UnresolveNoteCommentResponse = UnresolveNoteCommentResponses[keyof UnresolveNoteCommentResponses]; + +export type ResolveNoteCommentData = { + body?: never; + path: { + noteId: string; + commentId: ApiCommentId; + }; + query?: never; + url: '/notes/{noteId}/comments/{commentId}/resolution'; +}; + +export type ResolveNoteCommentErrors = { + /** + * Comment resolution forbidden + */ + 403: ApiCommentError; + /** + * Not found + */ + 404: ApiCommentError; + /** + * Comment resolution conflict + */ + 409: ApiCommentResolutionConflictError; +}; + +export type ResolveNoteCommentError = ResolveNoteCommentErrors[keyof ResolveNoteCommentErrors]; + +export type ResolveNoteCommentResponses = { + /** + * Ok + */ + 200: ApiCommentResolutionResponse; +}; + +export type ResolveNoteCommentResponse = ResolveNoteCommentResponses[keyof ResolveNoteCommentResponses]; + +export type GetCurrentUserData = { + body?: never; + path?: never; + query?: never; + url: '/me'; +}; + +export type GetCurrentUserResponses = { + /** + * Ok + */ + 200: User; +}; + +export type GetCurrentUserResponse = GetCurrentUserResponses[keyof GetCurrentUserResponses]; + +export type GetHistoryData = { + body?: never; + path?: never; + query?: { + /** + * The maximum number of history items to return. + */ + limit?: number; + }; + url: '/history'; +}; + +export type GetHistoryResponses = { + /** + * Ok + */ + 200: GetUserHistory; +}; + +export type GetHistoryResponse = GetHistoryResponses[keyof GetHistoryResponses]; diff --git a/nodejs/tsconfig.base.json b/nodejs/tsconfig.base.json index e73e99a..4735610 100644 --- a/nodejs/tsconfig.base.json +++ b/nodejs/tsconfig.base.json @@ -7,6 +7,7 @@ "outDir": "dist", "rootDir": ".", "strict": true, + "skipLibCheck": true, "target": "ESNext", "esModuleInterop": true, "resolveJsonModule": true, From f843b842a39232cbdcb9d702fc6b4f510c397eb8 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 23 Sep 2026 19:25:11 +0800 Subject: [PATCH 02/31] feat: expose generated raw API entry point --- nodejs/package.json | 12 +++++++++++- nodejs/rollup.config.js | 16 ++++++++++++---- nodejs/src/raw.ts | 19 +++++++++++++++++++ nodejs/tests/types/raw.cts | 15 +++++++++++++++ nodejs/tests/types/raw.mts | 15 +++++++++++++++ nodejs/tsconfig.json | 3 +++ 6 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 nodejs/src/raw.ts create mode 100644 nodejs/tests/types/raw.cts create mode 100644 nodejs/tests/types/raw.mts diff --git a/nodejs/package.json b/nodejs/package.json index 9e80bf3..fa91d40 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -17,6 +17,11 @@ "require": "./dist/index.cjs", "types": "./dist/index.d.ts" }, + "./raw": { + "import": "./dist/raw.js", + "require": "./dist/raw.cjs", + "types": "./dist/raw.d.ts" + }, "./dist": { "import": "./dist/index.js", "require": "./dist/index.cjs", @@ -26,7 +31,12 @@ }, "scripts": { "clean": "rimraf dist", - "build": "pnpm run clean && rollup -c", + "build": "pnpm run clean && rollup -c && pnpm check:node-types", + "generate": "openapi-ts", + "check:generated": "node scripts/check-generated.mjs", + "check:node-types": "tsc --noEmit --strict --module NodeNext tests/types/raw.mts tests/types/raw.cts", + "typecheck": "tsc --noEmit -p tsconfig.json", + "spec:pull": "curl -fsSL https://api.hackmd.io/v1/docs/swagger.json -o spec/hackmd-openapi.json", "watch": "pnpm run clean && rollup -c -w", "prepublishOnly": "pnpm run build", "lint": "eslint src --fix --ext .ts", diff --git a/nodejs/rollup.config.js b/nodejs/rollup.config.js index ae16c6f..cf7e01b 100644 --- a/nodejs/rollup.config.js +++ b/nodejs/rollup.config.js @@ -5,9 +5,13 @@ import typescript from '@rollup/plugin-typescript' export default [ // ESM build { - input: 'src/index.ts', + input: { + index: 'src/index.ts', + raw: 'src/raw.ts', + }, output: { - file: 'dist/index.js', + dir: 'dist', + entryFileNames: '[name].js', format: 'esm', sourcemap: true, }, @@ -27,9 +31,13 @@ export default [ }, // CJS build { - input: 'src/index.ts', + input: { + index: 'src/index.ts', + raw: 'src/raw.ts', + }, output: { - file: 'dist/index.cjs', + dir: 'dist', + entryFileNames: '[name].cjs', format: 'cjs', sourcemap: true, exports: 'named' diff --git a/nodejs/src/raw.ts b/nodejs/src/raw.ts new file mode 100644 index 0000000..2b74edc --- /dev/null +++ b/nodejs/src/raw.ts @@ -0,0 +1,19 @@ +/** + * Generated, one-to-one HackMD API operations and their wire types. + * + * This surface is regenerated from `spec/hackmd-openapi.json`. Use the + * hand-written `API` class from the package root when you need its compatibility + * helpers such as response unwrapping, retries, and ETag handling. + */ +export * from './generated/index.js' +export { client } from './generated/client.gen.js' +export { + createClient, + createConfig, +} from './generated/client/index.js' +export type { + Client, + Config, + RequestOptions as RawRequestOptions, + RequestResult as RawRequestResult, +} from './generated/client/index.js' diff --git a/nodejs/tests/types/raw.cts b/nodejs/tests/types/raw.cts new file mode 100644 index 0000000..8218500 --- /dev/null +++ b/nodejs/tests/types/raw.cts @@ -0,0 +1,15 @@ +import { createClient, getNote } from '@hackmd/api/raw' + +const client = createClient({ + auth: 'test-token', + baseURL: 'https://api.hackmd.io/v1', +}) + +export async function readNoteContent (): Promise { + const response = await getNote({ + client, + path: { noteId: 'note-id' }, + throwOnError: true, + }) + return response.data.content +} diff --git a/nodejs/tests/types/raw.mts b/nodejs/tests/types/raw.mts new file mode 100644 index 0000000..8218500 --- /dev/null +++ b/nodejs/tests/types/raw.mts @@ -0,0 +1,15 @@ +import { createClient, getNote } from '@hackmd/api/raw' + +const client = createClient({ + auth: 'test-token', + baseURL: 'https://api.hackmd.io/v1', +}) + +export async function readNoteContent (): Promise { + const response = await getNote({ + client, + path: { noteId: 'note-id' }, + throwOnError: true, + }) + return response.data.content +} diff --git a/nodejs/tsconfig.json b/nodejs/tsconfig.json index 2ea9295..7ec1544 100644 --- a/nodejs/tsconfig.json +++ b/nodejs/tsconfig.json @@ -2,5 +2,8 @@ "extends": "./tsconfig.base.json", "include": [ "**/*" + ], + "exclude": [ + "tests/types/**/*" ] } From db442951342e8183c4d2fb304cade87516527e8e Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 23 Sep 2026 19:41:15 +0800 Subject: [PATCH 03/31] test: cover generated raw API and CI checks --- .github/workflows/ci.yml | 8 ++ nodejs/README.md | 40 ++++++++++ nodejs/jest.config.ts | 3 + nodejs/tests/generated.spec.ts | 135 +++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 nodejs/tests/generated.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833b243..bf73557 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,14 @@ jobs: working-directory: nodejs run: pnpm install --frozen-lockfile + - name: Check generated client + working-directory: nodejs + run: pnpm check:generated + + - name: Typecheck + working-directory: nodejs + run: pnpm typecheck + - name: Run lint working-directory: nodejs run: pnpm lint diff --git a/nodejs/README.md b/nodejs/README.md index 037e68a..81bbfa9 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -134,10 +134,50 @@ const uploadedFromNode = await client.uploadNoteImage('note-id', image, { console.log(uploadedFromNode.data.link) ``` +### Generated Raw API + +The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, +one-to-one function. Create a client to share authentication and the API endpoint: + +```typescript +import { createClient, getNote } from '@hackmd/api/raw' + +const client = createClient({ + auth: 'YOUR_ACCESS_TOKEN', + baseURL: 'https://api.hackmd.io/v1', +}) + +const response = await getNote({ + client, + path: { noteId: 'NOTE_ID' }, + throwOnError: true, +}) + +console.log(response.data.content) +``` + +The package root remains the compatibility layer with response unwrapping, +retries, and ETag handling. Files under `src/generated` are generated from the +vendored OpenAPI document and must not be edited manually. + ## API See the [code](./src/index.ts) and [typings](./src/type.ts). The API client is written in TypeScript, so you can get auto-completion and type checking in any TypeScript Language Server powered editor or IDE. +## Regenerating the raw client + +Generation requires Node.js 22.18 or newer. + +```bash +pnpm spec:pull +pnpm generate +pnpm check:generated +``` + +The OpenAPI document is committed at `spec/hackmd-openapi.json`, and generated +sources are committed under `src/generated` so package builds remain offline and +deterministic. + ## E2E tests (live API) Integration tests call a real HackMD API (staging or production). They are **not** run by `pnpm test` or the default CI job. diff --git a/nodejs/jest.config.ts b/nodejs/jest.config.ts index 838e7e1..11b029d 100644 --- a/nodejs/jest.config.ts +++ b/nodejs/jest.config.ts @@ -5,6 +5,9 @@ const customJestConfig: JestConfigWithTsJest = { testEnvironment: "node", transformIgnorePatterns: ["/node_modules/"], extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, setupFiles: ["dotenv/config"], testPathIgnorePatterns: ["/node_modules/", "/tests/e2e/"], } diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts new file mode 100644 index 0000000..3f502a2 --- /dev/null +++ b/nodejs/tests/generated.spec.ts @@ -0,0 +1,135 @@ +import { http, HttpResponse } from 'msw' + +import * as generatedSdk from '../src/generated/sdk.gen' +import { createClient, getCurrentUser } from '../src/raw' +import * as openApiDocument from '../spec/hackmd-openapi.json' +import { server } from './mock' + +const EXPECTED_OPERATIONS = [ + 'batchRestore', + 'compareVersions', + 'createFolder', + 'createNote', + 'createTeamFolder', + 'createTeamNote', + 'createTeamWebhook', + 'createVersion', + 'createWebhook', + 'deleteFolder', + 'deleteNote', + 'deleteTeamFolder', + 'deleteTeamNote', + 'deleteTeamWebhook', + 'deleteWebhook', + 'exportTeamWebhookDeliveries', + 'exportWebhookDeliveries', + 'getCurrentUser', + 'getFolder', + 'getFolderOrder', + 'getHistory', + 'getNote', + 'getNoteComment', + 'getTeamFolder', + 'getTeamFolderOrder', + 'getTeamNote', + 'getTeamWebhook', + 'getTeamWebhookDelivery', + 'getVersion', + 'getWebhook', + 'getWebhookDelivery', + 'listFolders', + 'listNoteComments', + 'listNotes', + 'listPersonalTrash', + 'listTeamFolders', + 'listTeamNotes', + 'listTeams', + 'listTeamTrash', + 'listTeamWebhookDeliveries', + 'listTeamWebhooks', + 'listVersions', + 'listWebhookDeliveries', + 'listWebhooks', + 'pingTeamWebhook', + 'pingWebhook', + 'resolveNoteComment', + 'restoreNote', + 'unresolveNoteComment', + 'updateFolder', + 'updateFolderOrder', + 'updateNote', + 'updateTeamFolder', + 'updateTeamFolderOrder', + 'updateTeamNote', + 'updateTeamWebhook', + 'updateVersion', + 'updateWebhook', + 'uploadNoteImage', +] as const + +const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const + +type OpenApiOperation = { + operationId?: string + responses?: Record +} +type OpenApiPath = Partial> + +function operationNamesFromSpec (): string[] { + const paths = openApiDocument.paths as Record + return Object.values(paths) + .flatMap(path => HTTP_METHODS.flatMap(method => { + const operationId = path[method]?.operationId + if (!operationId) return [] + return [operationId.charAt(0).toLowerCase() + operationId.slice(1)] + })) + .sort() +} + +describe('generated raw API', () => { + beforeAll(() => server.listen()) + afterEach(() => server.resetHandlers()) + afterAll(() => server.close()) + + test('covers every reviewed v1 operation', () => { + const expected = [...EXPECTED_OPERATIONS].sort() + + expect(expected).toHaveLength(59) + expect(operationNamesFromSpec()).toEqual(expected) + expect(Object.keys(generatedSdk).sort()).toEqual(expected) + }) + + test('detail GETs have only the documented 200 success response', () => { + const paths = openApiDocument.paths as Record + for (const path of [ + '/notes/{noteId}', + '/teams/{teampath}/notes/{noteId}', + '/folders/{folderId}', + '/teams/{teampath}/folders/{folderId}', + ]) { + const responses = paths[path]?.get?.responses ?? {} + const successStatuses = Object.keys(responses).filter(status => /^2\d\d$/.test(status)) + expect(successStatuses).toEqual(['200']) + } + }) + + test('supports an authenticated custom Axios client', async () => { + let authorization: string | null = null + server.use( + http.get('https://api.hackmd.io/v1/me', ({ request }) => { + authorization = request.headers.get('authorization') + return HttpResponse.json({ id: 'user-id', name: 'Test User' }) + }), + ) + const client = createClient({ + auth: 'test-token', + baseURL: 'https://api.hackmd.io/v1', + }) + + const response = await getCurrentUser({ client, throwOnError: true }) + + expect(authorization).toBe('Bearer test-token') + expect(response.status).toBe(200) + expect(response.data).toMatchObject({ id: 'user-id', name: 'Test User' }) + }) +}) From 6752084264e60e86a6d08049d52c6b4528fd63e0 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 23 Sep 2026 23:40:44 +0800 Subject: [PATCH 04/31] fix: sync note detail ETag contract --- nodejs/spec/hackmd-openapi.json | 38 +++++++++++++++++++++++++++++++++ nodejs/tests/generated.spec.ts | 13 +++++++++++ 2 files changed, 51 insertions(+) diff --git a/nodejs/spec/hackmd-openapi.json b/nodejs/spec/hackmd-openapi.json index ae1b2bf..f866513 100644 --- a/nodejs/spec/hackmd-openapi.json +++ b/nodejs/spec/hackmd-openapi.json @@ -3801,6 +3801,25 @@ "$ref": "#/components/schemas/SingleNote" } } + }, + "headers": { + "ETag": { + "schema": { + "type": "string" + }, + "required": true + } + } + }, + "304": { + "description": "", + "headers": { + "ETag": { + "schema": { + "type": "string" + }, + "required": true + } } }, "403": { @@ -4889,6 +4908,25 @@ "$ref": "#/components/schemas/SingleNote" } } + }, + "headers": { + "ETag": { + "schema": { + "type": "string" + }, + "required": true + } + } + }, + "304": { + "description": "", + "headers": { + "ETag": { + "schema": { + "type": "string" + }, + "required": true + } } }, "403": { diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index 3f502a2..0725fd6 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -113,6 +113,19 @@ describe('generated raw API', () => { } }) + test('note detail GETs document a bodyless 304 and ETag headers', () => { + const paths = openApiDocument.paths as Record + for (const path of ['/notes/{noteId}', '/teams/{teampath}/notes/{noteId}']) { + const responses = paths[path]?.get?.responses as Record + }> + expect(responses['200'].headers).toHaveProperty('ETag') + expect(responses['304'].headers).toHaveProperty('ETag') + expect(responses['304']).not.toHaveProperty('content') + } + }) + test('supports an authenticated custom Axios client', async () => { let authorization: string | null = null server.use( From e09afe2366812adf16378599d9b03af6b775acc7 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Wed, 23 Sep 2026 23:53:36 +0800 Subject: [PATCH 05/31] docs: add generated API reference and local preview --- .github/workflows/ci.yml | 4 + .github/workflows/pages.yml | 55 +++ nodejs/.gitignore | 1 + nodejs/.stackblitzrc | 3 + nodejs/README.md | 34 +- nodejs/docs/README.md | 39 +++ nodejs/package.json | 6 +- nodejs/pnpm-lock.yaml | 545 ++++++++++++++++++++++++++++++ nodejs/src/index.ts | 6 +- nodejs/src/raw.ts | 2 + nodejs/tests/types/playground.mts | 14 + nodejs/typedoc.json | 32 ++ 12 files changed, 726 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 nodejs/.stackblitzrc create mode 100644 nodejs/docs/README.md create mode 100644 nodejs/tests/types/playground.mts create mode 100644 nodejs/typedoc.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf73557..e00ad70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,10 @@ jobs: working-directory: nodejs run: pnpm build + - name: Build API reference + working-directory: nodejs + run: pnpm docs:build + - name: Run tests working-directory: nodejs env: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..020aa7f --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,55 @@ +name: API reference + +on: + push: + branches: [master] + +permissions: + contents: read + +concurrency: + group: github-pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v6 + with: + version: 10.33.2 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: nodejs/pnpm-lock.yaml + + - name: Install dependencies + working-directory: nodejs + run: pnpm install --frozen-lockfile + + - name: Build API reference + working-directory: nodejs + run: pnpm docs:build + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v4 + with: + path: nodejs/.docs-dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + permissions: + pages: write + id-token: write + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/nodejs/.gitignore b/nodejs/.gitignore index 51c5b10..d3f3234 100644 --- a/nodejs/.gitignore +++ b/nodejs/.gitignore @@ -82,6 +82,7 @@ web_modules/ # Nuxt.js build / generate output .nuxt dist +.docs-dist/ # Gatsby files .cache/ diff --git a/nodejs/.stackblitzrc b/nodejs/.stackblitzrc new file mode 100644 index 0000000..fb92836 --- /dev/null +++ b/nodejs/.stackblitzrc @@ -0,0 +1,3 @@ +{ + "startCommand": "pnpm build" +} diff --git a/nodejs/README.md b/nodejs/README.md index 81bbfa9..1b4491a 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -105,16 +105,18 @@ const data = await client.getMe({ unwrapData: true }) ### ETag Support -The client supports ETag-based caching for note retrieval. You can pass an ETag to check if the content has changed: +The client can send an ETag to check whether a note changed. The server +generates the ETag and decides whether to return 304; the client does not cache +the note body for you: ```javascript -// First request -const note = await client.getNote('note-id') -const etag = note.etag - -// Subsequent request with ETag -const updatedNote = await client.getNote('note-id', { etag }) -// If the note hasn't changed, the response will have status 304 +const first = await client.getNote('note-id', { unwrapData: false }) +const response = await client.getNote('note-id', { + etag: first.headers.etag, + unwrapData: false, +}) +// 304 has no body; keep first.data. Otherwise, use response.data. +const note = response.status === 304 ? first.data : response.data ``` ### Image Upload @@ -156,13 +158,21 @@ const response = await getNote({ console.log(response.data.content) ``` -The package root remains the compatibility layer with response unwrapping, -retries, and ETag handling. Files under `src/generated` are generated from the -vendored OpenAPI document and must not be edited manually. +The package root retains the existing `API` class. Files under +`src/generated` are generated from the vendored OpenAPI document and must not +be edited manually. ## API -See the [code](./src/index.ts) and [typings](./src/type.ts). The API client is written in TypeScript, so you can get auto-completion and type checking in any TypeScript Language Server powered editor or IDE. +The [API reference](https://hackmdio.github.io/api-client/) covers the SDK, +compatibility API, and every raw operation and DTO. To explore autocomplete +without a real token, open the [type-only StackBlitz example](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts). +The Pages site is deployed from `master` only. + +Run `pnpm docs:dev` from `nodejs` and open `http://127.0.0.1:3000` to preview +the reference locally. It builds the HTML once before serving; rerun the +command after changing source or docs. The output in `.docs-dist` is not +committed. ## Regenerating the raw client diff --git a/nodejs/docs/README.md b/nodejs/docs/README.md new file mode 100644 index 0000000..56132af --- /dev/null +++ b/nodejs/docs/README.md @@ -0,0 +1,39 @@ +# HackMD API for TypeScript + +This reference covers the existing `API` class and every generated operation +and type in `@hackmd/api/raw`. + +```sh +npm install @hackmd/api +``` + +## API client + +```ts +import { API } from '@hackmd/api' + +const client = new API('YOUR_ACCESS_TOKEN') +const note = await client.getNote('NOTE_ID') +console.log(note.content) +``` + +Existing applications can keep using `API` and its current method signatures. + +## Generated raw API + +```ts +import { createClient, getNote } from '@hackmd/api/raw' + +const client = createClient({ auth: 'YOUR_ACCESS_TOKEN' }) +const response = await getNote({ client, path: { noteId: 'NOTE_ID' }, throwOnError: true }) +console.log(response.data.content) +``` + +Use the raw reference for the full OpenAPI operation and DTO catalog. + +## Explore the types + +[Open the type-only example in StackBlitz](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts). +The example makes no requests and requires no real token. It is useful for +autocomplete and type inspection; run real API calls only with your own token +in a trusted local environment. diff --git a/nodejs/package.json b/nodejs/package.json index fa91d40..c443dc0 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -34,7 +34,9 @@ "build": "pnpm run clean && rollup -c && pnpm check:node-types", "generate": "openapi-ts", "check:generated": "node scripts/check-generated.mjs", - "check:node-types": "tsc --noEmit --strict --module NodeNext tests/types/raw.mts tests/types/raw.cts", + "check:node-types": "tsc --noEmit --strict --module NodeNext tests/types/raw.mts tests/types/raw.cts tests/types/playground.mts", + "docs:build": "typedoc", + "docs:dev": "pnpm docs:build && serve .docs-dist --listen tcp://127.0.0.1:3000", "typecheck": "tsc --noEmit -p tsconfig.json", "spec:pull": "curl -fsSL https://api.hackmd.io/v1/docs/swagger.json -o spec/hackmd-openapi.json", "watch": "pnpm run clean && rollup -c -w", @@ -79,8 +81,10 @@ "msw": "^2.7.3", "rimraf": "^4.1.2", "rollup": "^4.41.1", + "serve": "14.2.6", "ts-jest": "^29.4.12", "ts-node": "^8.8.2", + "typedoc": "0.28.20", "typescript": "^5.9.2" }, "dependencies": { diff --git a/nodejs/pnpm-lock.yaml b/nodejs/pnpm-lock.yaml index 2e13549..1df5e82 100644 --- a/nodejs/pnpm-lock.yaml +++ b/nodejs/pnpm-lock.yaml @@ -63,12 +63,18 @@ importers: rollup: specifier: ^4.41.1 version: 4.41.1 + serve: + specifier: 14.2.6 + version: 14.2.6 ts-jest: specifier: ^29.4.12 version: 29.4.12(@babel/core@7.20.12)(@jest/transform@29.4.2)(@jest/types@29.4.2)(babel-jest@29.4.2(@babel/core@7.20.12))(jest-util@29.4.2)(jest@29.4.2(@types/node@22.15.30)(ts-node@8.9.1(typescript@5.9.2)))(typescript@5.9.2) ts-node: specifier: ^8.8.2 version: 8.9.1(typescript@5.9.2) + typedoc: + specifier: 0.28.20 + version: 0.28.20(typescript@5.9.2) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -265,6 +271,9 @@ packages: resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==} engines: {node: '>=14.0.0', npm: '>=6.0.0'} + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@hey-api/codegen-core@0.9.0': resolution: {integrity: sha512-OK9/R8WuujwgvnrDIPnEiIf6WnfUOi3GaEr6kIngqoI5FUQwYbeDKHE/frTVUl2A76ZQPCrMknHtPx6Gqtwf8Q==} engines: {node: '>=22.18.0'} @@ -613,6 +622,21 @@ packages: cpu: [x64] os: [win32] + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.25.21': resolution: {integrity: sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==} @@ -646,6 +670,9 @@ packages: '@types/graceful-fs@4.1.6': resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/istanbul-lib-coverage@2.0.4': resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} @@ -685,6 +712,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -753,6 +783,9 @@ packages: resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} deprecated: Potential CWE-502 - Update to 1.3.1 or higher + '@zeit/schemas@2.36.0': + resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -766,6 +799,12 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -778,6 +817,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -786,13 +829,23 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -837,12 +890,24 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + boxen@7.0.0: + resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} + engines: {node: '>=14.16'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -866,6 +931,14 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -890,13 +963,25 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + caniuse-lite@1.0.30001452: resolution: {integrity: sha512-Lkp0vFjMkBB3GTpLR8zk4NwW5EdRdnitwYJHDOOKIU85x4ckYCPQ+9WlVvSVClHxVReefkUMtWZH2l9KGlD51w==} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -912,10 +997,18 @@ packages: cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + clipboardy@3.0.0: + resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -949,6 +1042,14 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -958,6 +1059,10 @@ packages: confbox@0.3.1: resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -972,6 +1077,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -984,6 +1097,10 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1045,6 +1162,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.4.295: resolution: {integrity: sha512-lEO94zqf1bDA3aepxwnWoHUjA8sZ+2owgcSZjYQy0+uOSEclJX0VieZC+r+wLpSxUHRd6gG32znTWmr+5iGzFw==} @@ -1055,6 +1175,13 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -1156,6 +1283,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1345,12 +1475,20 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1395,6 +1533,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-port-reachable@4.0.0: + resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -1402,6 +1544,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -1591,6 +1737,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1617,6 +1766,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1634,6 +1786,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1647,10 +1802,17 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-it@14.3.2: + resolution: {integrity: sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdurl@2.1.0: + resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1662,10 +1824,18 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -1674,9 +1844,16 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1684,6 +1861,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -1704,6 +1884,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -1724,6 +1908,10 @@ packages: ohash@2.0.12: resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1778,6 +1966,9 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1785,6 +1976,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1842,6 +2036,10 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1852,9 +2050,17 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + rc9@3.1.0: resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} @@ -1862,10 +2068,21 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + registry-auth-token@3.3.2: + resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -1919,6 +2136,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1938,6 +2158,14 @@ packages: engines: {node: '>=10'} hasBin: true + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + + serve@14.2.6: + resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} + engines: {node: '>= 14'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1992,10 +2220,18 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -2004,6 +2240,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2097,6 +2337,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.38.0: resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==} engines: {node: '>=16'} @@ -2105,11 +2349,21 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typedoc@0.28.20: + resolution: {integrity: sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==} + engines: {node: '>= 18', pnpm: '>= 10'} + hasBin: true + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -2128,6 +2382,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-check@1.5.4: + resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2138,6 +2395,10 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -2146,6 +2407,10 @@ packages: engines: {node: '>= 8'} hasBin: true + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2161,6 +2426,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2179,6 +2448,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2428,6 +2702,14 @@ snapshots: '@faker-js/faker@7.6.0': {} + '@gerrit0/mini-shiki@3.23.0': + dependencies: + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@hey-api/codegen-core@0.9.0': dependencies: '@hey-api/types': 0.1.4 @@ -2842,6 +3124,26 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.41.1': optional: true + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.25.21': {} '@sinonjs/commons@2.0.0': @@ -2886,6 +3188,10 @@ snapshots: dependencies: '@types/node': 13.13.4 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/istanbul-lib-coverage@2.0.4': {} '@types/istanbul-lib-report@3.0.0': @@ -2921,6 +3227,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/unist@3.0.3': {} + '@types/yargs-parser@21.0.0': {} '@types/yargs@17.0.22': @@ -3015,6 +3323,8 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@zeit/schemas@2.36.0': {} + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -3028,6 +3338,17 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -3036,19 +3357,27 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.3.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 + arch@2.2.0: {} + arg@4.1.3: {} + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3121,6 +3450,19 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + + boxen@7.0.0: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.0.1 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -3130,6 +3472,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3155,6 +3501,10 @@ snapshots: dependencies: run-applescript: 7.1.0 + bytes@3.0.0: {} + + bytes@3.1.2: {} + c12@3.3.4: dependencies: chokidar: 5.0.0 @@ -3181,13 +3531,21 @@ snapshots: camelcase@6.3.0: {} + camelcase@7.0.1: {} + caniuse-lite@1.0.30001452: {} + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.0.1: {} + char-regex@1.0.2: {} chokidar@5.0.0: @@ -3198,8 +3556,16 @@ snapshots: cjs-module-lexer@1.2.2: {} + cli-boxes@3.0.0: {} + cli-width@4.1.0: {} + clipboardy@3.0.0: + dependencies: + arch: 2.2.0 + execa: 5.1.1 + is-wsl: 2.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3226,12 +3592,30 @@ snapshots: commondir@1.0.1: {} + compressible@2.0.18: + dependencies: + mime-db: 1.52.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + concat-map@0.0.1: {} confbox@0.2.4: {} confbox@0.3.1: {} + content-disposition@0.5.2: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -3244,12 +3628,18 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.3.4: dependencies: ms: 2.1.2 dedent@0.7.0: {} + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.0: {} @@ -3293,12 +3683,18 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.4.295: {} emittery@0.13.1: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -3434,6 +3830,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.8: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -3612,12 +4010,16 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + is-arrayish@0.2.1: {} is-core-module@2.11.0: dependencies: has: 1.0.3 + is-docker@2.2.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -3644,12 +4046,18 @@ snapshots: is-path-inside@3.0.3: {} + is-port-reachable@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.7 is-stream@2.0.1: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 @@ -4051,6 +4459,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -4070,6 +4480,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.2: + dependencies: + uc.micro: 2.1.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -4086,6 +4500,8 @@ snapshots: dependencies: yallist: 3.1.1 + lunr@2.3.9: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -4100,8 +4516,19 @@ snapshots: dependencies: tmpl: 1.0.5 + markdown-it@14.3.2: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} + mdurl@2.1.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -4111,24 +4538,40 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.33.0: {} + mime-db@1.52.0: {} + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + mime-types@2.1.35: dependencies: mime-db: 1.52.0 mimic-fn@2.1.0: {} + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.11 + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 minimist@1.2.8: {} + ms@2.0.0: {} + ms@2.1.2: {} msw@2.7.3(@types/node@22.15.30)(typescript@5.9.2): @@ -4160,6 +4603,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.4: {} + neo-async@2.6.2: {} node-int64@0.4.0: {} @@ -4174,6 +4619,8 @@ snapshots: ohash@2.0.12: {} + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4235,10 +4682,14 @@ snapshots: path-is-absolute@1.0.1: {} + path-is-inside@1.0.2: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-to-regexp@3.3.0: {} + path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -4286,23 +4737,45 @@ snapshots: dependencies: punycode: 2.3.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} querystringify@2.2.0: {} queue-microtask@1.2.3: {} + range-parser@1.2.0: {} + rc9@3.1.0: dependencies: defu: 6.1.7 destr: 2.0.5 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-is@18.2.0: {} readdirp@5.1.1: {} + registry-auth-token@3.3.2: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-cwd@3.0.0: @@ -4363,6 +4836,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + semver@6.3.1: {} semver@7.7.1: {} @@ -4371,6 +4846,32 @@ snapshots: semver@7.8.5: {} + serve-handler@6.1.7: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.5 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + + serve@14.2.6: + dependencies: + '@zeit/schemas': 2.36.0 + ajv: 8.18.0 + arg: 5.0.2 + boxen: 7.0.0 + chalk: 5.0.1 + chalk-template: 0.4.0 + clipboardy: 3.0.0 + compression: 1.8.1 + is-port-reachable: 4.0.0 + serve-handler: 6.1.7 + update-check: 1.5.4 + transitivePeerDependencies: + - supports-color + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4418,14 +4919,26 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -4504,12 +5017,25 @@ snapshots: type-fest@0.21.3: {} + type-fest@2.19.0: {} + type-fest@4.38.0: {} type-fest@4.41.0: {} + typedoc@0.28.20(typescript@5.9.2): + dependencies: + '@gerrit0/mini-shiki': 3.23.0 + lunr: 2.3.9 + markdown-it: 14.3.2 + minimatch: 10.2.6 + typescript: 5.9.2 + yaml: 2.9.1 + typescript@5.9.2: {} + uc.micro@2.1.0: {} + uglify-js@3.19.3: optional: true @@ -4523,6 +5049,11 @@ snapshots: escalade: 3.1.1 picocolors: 1.1.1 + update-check@1.5.4: + dependencies: + registry-auth-token: 3.3.2 + registry-url: 3.1.0 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -4538,6 +5069,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + vary@1.1.2: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -4546,6 +5079,10 @@ snapshots: dependencies: isexe: 2.0.0 + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -4562,6 +5099,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -4578,6 +5121,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.1: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 1ea9bb7..0064135 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -1,3 +1,5 @@ +/** @module @hackmd/api */ + import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { CreateNoteOptions, @@ -28,7 +30,7 @@ import { UpdateUserFolderBody, UploadNoteImageOptions, UploadNoteImageResponse, -} from './type' +} from './type.js' import * as HackMDErrors from './error' export type RequestOptions = { @@ -312,6 +314,6 @@ export class API { } } -export * from './type' +export * from './type.js' export default API diff --git a/nodejs/src/raw.ts b/nodejs/src/raw.ts index 2b74edc..3ab6c40 100644 --- a/nodejs/src/raw.ts +++ b/nodejs/src/raw.ts @@ -1,4 +1,6 @@ /** + * @module @hackmd/api/raw + * * Generated, one-to-one HackMD API operations and their wire types. * * This surface is regenerated from `spec/hackmd-openapi.json`. Use the diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts new file mode 100644 index 0000000..b4f5d51 --- /dev/null +++ b/nodejs/tests/types/playground.mts @@ -0,0 +1,14 @@ +/** Type-only playground: these functions are not called and send no requests. */ +import type { API, GetUserNote } from '@hackmd/api' +import type { SingleNote as RawNote } from '@hackmd/api/raw' + +async function exploreClient (client: API): Promise { + return client.getNote('NOTE_ID') +} + +function exploreRawNote (note: RawNote): string { + return note.content +} + +void exploreClient +void exploreRawNote diff --git a/nodejs/typedoc.json b/nodejs/typedoc.json new file mode 100644 index 0000000..20a13f2 --- /dev/null +++ b/nodejs/typedoc.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "name": "@hackmd/api", + "entryPoints": ["src/index.ts", "src/raw.ts"], + "tsconfig": "tsconfig.build.json", + "readme": "docs/README.md", + "out": ".docs-dist", + "intentionallyNotExported": [ + "Client", + "RequestFn", + "MethodFn", + "BuildUrlFn", + "SseFn", + "Options", + "AuthToken", + "BodySerializer", + "QuerySerializerOptions", + "QuerySerializer", + "ClientOptions", + "Auth", + "TDataShape", + "OptionReturnType", + "ClientMeta", + "StreamEvent" + ], + "externalSymbolLinkMappings": { + "typescript": { + "JSON.stringify": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify" + } + }, + "treatWarningsAsErrors": true +} From 77656634dd7bf1ce0730ed927cd94650238b5b65 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 00:55:04 +0800 Subject: [PATCH 06/31] refactor: delegate legacy getNote to generated client --- nodejs/src/index.ts | 39 +++++++++++++++++----- nodejs/src/type.ts | 53 +++++++---------------------- nodejs/tests/etag.spec.ts | 55 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 13 ++++++++ nodejs/tests/types/raw.cts | 11 +++++++ 5 files changed, 121 insertions(+), 50 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 0064135..050ed12 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -1,6 +1,8 @@ /** @module @hackmd/api */ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import { createClient, type Client } from './generated/client/index.js' +import { getNote as generatedGetNote } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -44,6 +46,13 @@ const defaultOption: RequestOptions = { type OptionReturnType = Opt extends { unwrapData: false } ? AxiosResponse : Opt extends { unwrapData: true } ? T : T +export type GetNoteSuccess = GetUserNote & { status: 200; etag?: string } +export type GetNoteNotModified = { status: 304; etag?: string } +export type GetNoteResult = GetNoteSuccess | GetNoteNotModified +export type GetNoteRawResult = + | (AxiosResponse & { status: 200 }) + | (AxiosResponse & { status: 304 }) + export type APIClientOptions = { wrapResponseErrors: boolean; timeout?: number; @@ -55,6 +64,7 @@ export type APIClientOptions = { export class API { private axios: AxiosInstance + private generatedClient: Client constructor ( readonly accessToken: string, @@ -76,6 +86,10 @@ export class API { baseURL: hackmdAPIEndpointURL, timeout: options.timeout }) + this.generatedClient = createClient({ + axios: this.axios, + baseURL: hackmdAPIEndpointURL.replace(/\/+$/, ''), + }) this.axios.interceptors.request.use( (config: InternalAxiosRequestConfig) => { @@ -183,15 +197,22 @@ export class API { return this.unwrapData(this.axios.get("notes"), options.unwrapData) as unknown as OptionReturnType } - async getNote (noteId: string, options = defaultOption as Opt): Promise> { - // Prepare request config with etag if provided in options - const config = options.etag ? { - headers: { 'If-None-Match': options.etag }, - // Consider 304 responses as successful - validateStatus: (status: number) => (status >= 200 && status < 300) || status === 304 - } : undefined - const request = this.axios.get(`notes/${noteId}`, config) - return this.unwrapData(request, options.unwrapData, true) as unknown as OptionReturnType + async getNote (noteId: string): Promise + async getNote (noteId: string, options: { unwrapData: false; etag?: undefined }): Promise & { status: 200 }> + async getNote (noteId: string, options: { unwrapData: false; etag: string }): Promise + async getNote (noteId: string, options: { unwrapData?: true; etag: string }): Promise + async getNote (noteId: string, options: { unwrapData?: true; etag?: undefined }): Promise + async getNote (noteId: string, options: RequestOptions): Promise + async getNote (noteId: string, options: RequestOptions = defaultOption): Promise { + const request = generatedGetNote({ + client: this.generatedClient, + path: { noteId }, + headers: options.etag ? { 'If-None-Match': options.etag } : undefined, + validateStatus: (status: number) => + (status >= 200 && status < 300) || Boolean(options.etag && status === 304), + throwOnError: true, + }) + return this.unwrapData(request, options.unwrapData, true) as unknown as Promise } async createNote (payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 9dd93e8..f670486 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,3 +1,10 @@ +import type { + FolderPath as GeneratedFolderPath, + NoteType as GeneratedNote, + SimpleUserProfile as GeneratedSimpleUserProfile, + SingleNote as GeneratedSingleNote, +} from './generated/types.gen.js' + export enum TeamVisibilityType { PUBLIC = 'public', PRIVATE = 'private', @@ -51,13 +58,7 @@ export type User = { teams: Team[] } -export type SimpleUserProfile = { - name: string, - userPath: string - photo: string - biography: string | null - createdAt: Date -} +export type SimpleUserProfile = GeneratedSimpleUserProfile export enum NotePermissionRole { OWNER = 'owner', @@ -65,39 +66,10 @@ export enum NotePermissionRole { GUEST = 'guest' } -/** Folder breadcrumb segment as returned on notes (OpenAPI `FolderPath`). */ -export type FolderPath = { - id: string - name: string - icon: string | null - color: string | null - parentId: string | null - clientId: string -} - -export type Note = { - id: string - title: string - tags: string[] - lastChangedAt: string - createdAt: string - lastChangeUser: SimpleUserProfile | null - publishType: NotePublishType - publishedAt: string | null - userPath: string | null - teamPath: string | null - permalink: string | null - shortId: string - publishLink: string - - readPermission: NotePermissionRole - writePermission: NotePermissionRole - folderPaths?: FolderPath[] -} - -export type SingleNote = Note & { - content: string -} +/** Note response types are generated from the v1 OpenAPI contract. */ +export type FolderPath = GeneratedFolderPath +export type Note = GeneratedNote +export type SingleNote = GeneratedSingleNote export type UpdateNoteOptions = Partial> & { parentFolderId?: string @@ -185,4 +157,3 @@ export type DeleteFolderResult = void export type DeleteTeamFolderResult = void export type GetFolderOrder = ApiFolderOrder export type GetTeamFolderOrder = ApiFolderOrder - diff --git a/nodejs/tests/etag.spec.ts b/nodejs/tests/etag.spec.ts index ce0d2ae..b027d49 100644 --- a/nodejs/tests/etag.spec.ts +++ b/nodejs/tests/etag.spec.ts @@ -1,5 +1,6 @@ import { server } from './mock' import { API } from '../src' +import { HttpResponseError } from '../src/error' import { http, HttpResponse } from 'msw' let client: API @@ -55,6 +56,7 @@ describe('Etag support', () => { // Verify data properties still exist expect(response).toHaveProperty('id', 'test-note-id') expect(response).toHaveProperty('title', 'Test Note') + expect(response.status).toBe(200) }) test('should include etag in response headers when unwrapData is false', async () => { @@ -199,6 +201,59 @@ describe('Etag support', () => { // With unwrapData: true and a 304 response, we just get the etag expect(response).toHaveProperty('etag', mockEtag) expect(response).toHaveProperty('status', 304) + expect(response).not.toHaveProperty('content') + }) + + test('uses the legacy Axios instance for custom base URL and authorization', async () => { + let authorization: string | null = null + server.use( + http.get('https://custom.hackmd.test/v1/notes/test-note-id', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json({ id: 'test-note-id', content: 'hello' }) + }) + ) + + const customClient = new API('custom-token', 'https://custom.hackmd.test/v1/') + const response = await customClient.getNote('test-note-id') + + expect(authorization).toBe('Bearer custom-token') + expect(response.status).toBe(200) + expect(response.content).toBe('hello') + }) + + test('keeps legacy error wrapping for generated requests', async () => { + server.use( + http.get('https://api.hackmd.io/v1/notes/missing-note', () => + HttpResponse.json({ error: 'Note not found' }, { status: 404 }) + ) + ) + + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getNote('missing-note')).rejects.toBeInstanceOf(HttpResponseError) + }) + + test('keeps legacy retry behavior for generated requests', async () => { + let attempts = 0 + server.use( + http.get('https://api.hackmd.io/v1/notes/retry-note', () => { + attempts++ + if (attempts === 1) return HttpResponse.json({}, { status: 503 }) + return HttpResponse.json({ id: 'retry-note', content: 'retried' }) + }) + ) + + const retryClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: { maxRetries: 1, baseDelay: 0 }, + }) + const response = await retryClient.getNote('retry-note') + + expect(attempts).toBe(2) + expect(response.content).toBe('retried') }) }) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index b4f5d51..d8776db 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -6,9 +6,22 @@ async function exploreClient (client: API): Promise { return client.getNote('NOTE_ID') } +async function exploreConditionalNote (client: API): Promise { + const response = await client.getNote('NOTE_ID', { etag: 'W/"cached"' }) + if (response.status === 304) { + // @ts-expect-error A 304 response has no note content. + response.content + return undefined + } + const lastChangedAt: number = response.lastChangedAt + void lastChangedAt + return response.content +} + function exploreRawNote (note: RawNote): string { return note.content } void exploreClient +void exploreConditionalNote void exploreRawNote diff --git a/nodejs/tests/types/raw.cts b/nodejs/tests/types/raw.cts index 8218500..4f7539e 100644 --- a/nodejs/tests/types/raw.cts +++ b/nodejs/tests/types/raw.cts @@ -1,4 +1,5 @@ import { createClient, getNote } from '@hackmd/api/raw' +import { API } from '@hackmd/api' const client = createClient({ auth: 'test-token', @@ -13,3 +14,13 @@ export async function readNoteContent (): Promise { }) return response.data.content } + +export async function readLegacyConditionalNote (legacy: API): Promise { + const response = await legacy.getNote('note-id', { etag: 'W/"cached"', unwrapData: false }) + if (response.status === 304) { + // @ts-expect-error A 304 response does not promise note data. + response.data.content + return undefined + } + return response.data.content +} From 88c829d28a518029d75c6d7f5b5c138c099b6343 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 01:03:15 +0800 Subject: [PATCH 07/31] refactor: delegate legacy getNoteList to generated client --- nodejs/src/index.ts | 4 ++-- nodejs/tests/api.spec.ts | 33 ++++++++++++++++++++++++++++++- nodejs/tests/types/playground.mts | 6 ++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 050ed12..94f40fa 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getNote as generatedGetNote } from './generated/sdk.gen.js' +import { getNote as generatedGetNote, listNotes } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -194,7 +194,7 @@ export class API { } async getNoteList (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get("notes"), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(listNotes({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getNote (noteId: string): Promise diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 998c8c0..bd79091 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -1,7 +1,7 @@ import { server } from './mock' import { API } from '../src' import { http, HttpResponse } from 'msw' -import { InternalServerError, TooManyRequestsError } from '../src/error' +import { HttpResponseError, InternalServerError, TooManyRequestsError } from '../src/error' let client: API @@ -39,6 +39,37 @@ test('getMe unwrapped', async () => { expect(response).toHaveProperty('photo') }) +test('getNoteList keeps the legacy response shapes and authorization', async () => { + const note = { id: 'note-1', title: 'A note', createdAt: 1700000000000 } + let authorization: string | null = null + server.use( + http.get('https://api.hackmd.io/v1/notes', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json([note]) + }) + ) + + expect(await client.getNoteList()).toEqual([note]) + const raw = await client.getNoteList({ unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual([note]) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getNoteList keeps legacy error wrapping', async () => { + server.use( + http.get('https://api.hackmd.io/v1/notes', () => + HttpResponse.json({ error: 'Forbidden' }, { status: 403 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getNoteList()).rejects.toBeInstanceOf(HttpResponseError) +}) + test('should throw axios error object if set wrapResponseErrors to false', async () => { const customCilent = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, { wrapResponseErrors: false, diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index d8776db..8ebbdee 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -18,10 +18,16 @@ async function exploreConditionalNote (client: API): Promise return response.content } +async function exploreNoteList (client: API): Promise { + const notes = await client.getNoteList() + return notes[0].createdAt +} + function exploreRawNote (note: RawNote): string { return note.content } void exploreClient void exploreConditionalNote +void exploreNoteList void exploreRawNote From 880e907c66a2c3a2e445c68a06b80d42b860c268 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 01:32:33 +0800 Subject: [PATCH 08/31] refactor: delegate legacy getHistory to generated client --- nodejs/src/index.ts | 12 ++++++++--- nodejs/src/type.ts | 3 ++- nodejs/tests/api.spec.ts | 34 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 6 ++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 94f40fa..ca84c9d 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getNote as generatedGetNote, listNotes } from './generated/sdk.gen.js' +import { getHistory as generatedGetHistory, getNote as generatedGetNote, listNotes } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -40,6 +40,8 @@ export type RequestOptions = { etag?: string | undefined; } +export type GetHistoryOptions = RequestOptions & { limit?: number } + const defaultOption: RequestOptions = { unwrapData: true, } @@ -189,8 +191,12 @@ export class API { return this.unwrapData(this.axios.get("me"), options.unwrapData) as unknown as OptionReturnType } - async getHistory (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get("history"), options.unwrapData) as unknown as OptionReturnType + async getHistory (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetHistory({ + client: this.generatedClient, + query: options.limit === undefined ? undefined : { limit: options.limit }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async getNoteList (options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index f670486..258fe99 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,5 +1,6 @@ import type { FolderPath as GeneratedFolderPath, + GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, @@ -81,7 +82,7 @@ export type GetMe = User // User notes export type GetUserNotes = Note[] export type GetUserNote = SingleNote -export type GetUserHistory = Note[] +export type GetUserHistory = GeneratedUserHistory export type CreateUserNote = SingleNote export type UpdateUserNote = void export type DeleteUserNote = void diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index bd79091..3d07fd2 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -70,6 +70,40 @@ test('getNoteList keeps legacy error wrapping', async () => { await expect(customClient.getNoteList()).rejects.toBeInstanceOf(HttpResponseError) }) +test('getHistory keeps the legacy response shapes and authorization', async () => { + const history = [{ id: 'note-1', title: 'Recent note', createdAt: 1700000000000 }] + let authorization: string | null = null + const limits: Array = [] + server.use( + http.get('https://api.hackmd.io/v1/history', ({ request }) => { + authorization = request.headers.get('Authorization') + limits.push(new URL(request.url).searchParams.get('limit')) + return HttpResponse.json(history) + }) + ) + + expect(await client.getHistory()).toEqual(history) + const raw = await client.getHistory({ unwrapData: false, limit: 5 }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(history) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) + expect(limits).toEqual([null, '5']) +}) + +test('getHistory keeps legacy error wrapping', async () => { + server.use( + http.get('https://api.hackmd.io/v1/history', () => + HttpResponse.json({ error: 'Forbidden' }, { status: 403 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getHistory()).rejects.toBeInstanceOf(HttpResponseError) +}) + test('should throw axios error object if set wrapResponseErrors to false', async () => { const customCilent = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, { wrapResponseErrors: false, diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 8ebbdee..18f86d4 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -23,6 +23,11 @@ async function exploreNoteList (client: API): Promise { return notes[0].createdAt } +async function exploreHistory (client: API): Promise { + const notes = await client.getHistory({ limit: 5 }) + return notes[0].lastChangedAt +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -30,4 +35,5 @@ function exploreRawNote (note: RawNote): string { void exploreClient void exploreConditionalNote void exploreNoteList +void exploreHistory void exploreRawNote From 968d8d433c0c4197361b6ca5c52efece08b50688 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 01:57:43 +0800 Subject: [PATCH 09/31] refactor: delegate legacy getTeamNotes to generated client --- nodejs/src/index.ts | 8 ++++++-- nodejs/tests/api.spec.ts | 31 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 6 ++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index ca84c9d..e4c6679 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getHistory as generatedGetHistory, getNote as generatedGetNote, listNotes } from './generated/sdk.gen.js' +import { getHistory as generatedGetHistory, getNote as generatedGetNote, listNotes, listTeamNotes } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -252,7 +252,11 @@ export class API { } async getTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get(`teams/${teamPath}/notes`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(listTeamNotes({ + client: this.generatedClient, + path: { teampath: teamPath }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async createTeamNote (teamPath: string, payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 3d07fd2..5fadd99 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -104,6 +104,37 @@ test('getHistory keeps legacy error wrapping', async () => { await expect(customClient.getHistory()).rejects.toBeInstanceOf(HttpResponseError) }) +test('getTeamNotes keeps the legacy response shapes and team path', async () => { + const notes = [{ id: 'team-note-1', title: 'Team note', createdAt: 1700000000000 }] + let authorization: string | null = null + server.use( + http.get('https://api.hackmd.io/v1/teams/test-team/notes', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json(notes) + }) + ) + + expect(await client.getTeamNotes('test-team')).toEqual(notes) + const raw = await client.getTeamNotes('test-team', { unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(notes) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getTeamNotes keeps legacy error wrapping', async () => { + server.use( + http.get('https://api.hackmd.io/v1/teams/missing-team/notes', () => + HttpResponse.json({ error: 'Team not found' }, { status: 404 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getTeamNotes('missing-team')).rejects.toBeInstanceOf(HttpResponseError) +}) + test('should throw axios error object if set wrapResponseErrors to false', async () => { const customCilent = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, { wrapResponseErrors: false, diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 18f86d4..6417018 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -28,6 +28,11 @@ async function exploreHistory (client: API): Promise { return notes[0].lastChangedAt } +async function exploreTeamNotes (client: API): Promise { + const notes = await client.getTeamNotes('TEAM_PATH') + return notes[0].createdAt +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -36,4 +41,5 @@ void exploreClient void exploreConditionalNote void exploreNoteList void exploreHistory +void exploreTeamNotes void exploreRawNote From f2ae87c51260c843cbfa0b31991f674bbb016701 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 02:09:51 +0800 Subject: [PATCH 10/31] refactor: delegate legacy getFolderList to generated client --- nodejs/src/index.ts | 4 ++-- nodejs/src/type.ts | 12 ++---------- nodejs/tests/api.spec.ts | 22 +++++++++++++++++++++- nodejs/tests/types/playground.mts | 6 ++++++ 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index e4c6679..311b090 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getHistory as generatedGetHistory, getNote as generatedGetNote, listNotes, listTeamNotes } from './generated/sdk.gen.js' +import { getHistory as generatedGetHistory, getNote as generatedGetNote, listFolders, listNotes, listTeamNotes } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -276,7 +276,7 @@ export class API { } async getFolderList (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get('folders'), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(listFolders({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async createFolder (payload: CreateUserFolderBody, options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 258fe99..bccbc9a 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,4 +1,5 @@ import type { + ApiFolder as GeneratedApiFolder, FolderPath as GeneratedFolderPath, GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, @@ -108,16 +109,7 @@ export type UpdateTeamNote = void export type DeleteTeamNote = void // Folders (user & team workspaces) -export type ApiFolder = { - id: string - name: string - description: string | null - icon: string | null - color: string | null - parentFolderId: string | null - createdAt: number - updatedAt: number -} +export type ApiFolder = GeneratedApiFolder /** Maps each parent folder id or the literal `root` to ordered child folder ids. */ export type ApiFolderOrder = Record diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 5fadd99..1fe9fd3 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -196,8 +196,10 @@ test('should throw HackMD error object', async () => { }) test('getFolderList returns folders from /folders', async () => { + let authorization: string | null = null server.use( - http.get('https://api.hackmd.io/v1/folders', () => { + http.get('https://api.hackmd.io/v1/folders', ({ request }) => { + authorization = request.headers.get('Authorization') return HttpResponse.json([ { id: 'folder-1', @@ -217,6 +219,24 @@ test('getFolderList returns folders from /folders', async () => { expect(folders).toHaveLength(1) expect(folders[0]).toMatchObject({ id: 'folder-1', name: 'Research' }) + const raw = await client.getFolderList({ unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(folders) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getFolderList keeps legacy error wrapping', async () => { + server.use( + http.get('https://api.hackmd.io/v1/folders', () => + HttpResponse.json({ error: 'Forbidden' }, { status: 403 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getFolderList()).rejects.toBeInstanceOf(HttpResponseError) }) test('updateFolderOrder sends order payload', async () => { diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 6417018..ed41a02 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -33,6 +33,11 @@ async function exploreTeamNotes (client: API): Promise { return notes[0].createdAt } +async function exploreFolders (client: API): Promise { + const folders = await client.getFolderList() + return folders[0].createdAt +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -42,4 +47,5 @@ void exploreConditionalNote void exploreNoteList void exploreHistory void exploreTeamNotes +void exploreFolders void exploreRawNote From f77cfdbfdc9b064ef83e324c61eb73e77f529e49 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 02:26:45 +0800 Subject: [PATCH 11/31] refactor: delegate legacy getTeamFolderList to generated client --- nodejs/src/index.ts | 8 +++++-- nodejs/tests/api.spec.ts | 40 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 6 +++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 311b090..4a76358 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getHistory as generatedGetHistory, getNote as generatedGetNote, listFolders, listNotes, listTeamNotes } from './generated/sdk.gen.js' +import { getHistory as generatedGetHistory, getNote as generatedGetNote, listFolders, listNotes, listTeamFolders, listTeamNotes } from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -304,7 +304,11 @@ export class API { } async getTeamFolderList (teamPath: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get(`teams/${teamPath}/folders`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(listTeamFolders({ + client: this.generatedClient, + path: { teampath: teamPath }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async createTeamFolder (teamPath: string, payload: CreateTeamFolderBody, options = defaultOption as Opt): Promise> { diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 1fe9fd3..ec30075 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -239,6 +239,46 @@ test('getFolderList keeps legacy error wrapping', async () => { await expect(customClient.getFolderList()).rejects.toBeInstanceOf(HttpResponseError) }) +test('getTeamFolderList keeps the legacy response shapes and team path', async () => { + const folders = [{ + id: 'folder-1', + name: 'Team Research', + description: null, + icon: null, + color: null, + parentFolderId: null, + createdAt: 1700000000000, + updatedAt: 1700000000000, + }] + let authorization: string | null = null + server.use( + http.get('https://api.hackmd.io/v1/teams/test-team/folders', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json(folders) + }) + ) + + expect(await client.getTeamFolderList('test-team')).toEqual(folders) + const raw = await client.getTeamFolderList('test-team', { unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(folders) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getTeamFolderList keeps legacy error wrapping', async () => { + server.use( + http.get('https://api.hackmd.io/v1/teams/missing-team/folders', () => + HttpResponse.json({ error: 'Team not found' }, { status: 404 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getTeamFolderList('missing-team')).rejects.toBeInstanceOf(HttpResponseError) +}) + test('updateFolderOrder sends order payload', async () => { let requestBody: unknown diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index ed41a02..1b247c1 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -38,6 +38,11 @@ async function exploreFolders (client: API): Promise { return folders[0].createdAt } +async function exploreTeamFolders (client: API): Promise { + const folders = await client.getTeamFolderList('TEAM_PATH') + return folders[0].updatedAt +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -48,4 +53,5 @@ void exploreNoteList void exploreHistory void exploreTeamNotes void exploreFolders +void exploreTeamFolders void exploreRawNote From 6781ad506743e14671581643585c0e1e6999de12 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 02:35:58 +0800 Subject: [PATCH 12/31] refactor: delegate legacy folder reads to generated client --- nodejs/src/index.ts | 33 +++++++++++--- nodejs/tests/api.spec.ts | 74 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 9 ++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 4a76358..37e7af6 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -2,7 +2,18 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' -import { getHistory as generatedGetHistory, getNote as generatedGetNote, listFolders, listNotes, listTeamFolders, listTeamNotes } from './generated/sdk.gen.js' +import { + getFolder as generatedGetFolder, + getFolderOrder as generatedGetFolderOrder, + getHistory as generatedGetHistory, + getNote as generatedGetNote, + getTeamFolder as generatedGetTeamFolder, + getTeamFolderOrder as generatedGetTeamFolderOrder, + listFolders, + listNotes, + listTeamFolders, + listTeamNotes, +} from './generated/sdk.gen.js' import { CreateNoteOptions, CreateTeamFolderBody, @@ -284,7 +295,11 @@ export class API { } async getFolder (folderId: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get(`folders/${folderId}`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedGetFolder({ + client: this.generatedClient, + path: { folderId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async updateFolder (folderId: string, payload: UpdateUserFolderBody, options = defaultOption as Opt): Promise> { @@ -296,7 +311,7 @@ export class API { } async getFolderOrder (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get('folders/folder-order'), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedGetFolderOrder({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async updateFolderOrder (payload: UpdateFolderOrderBody, options = defaultOption as Opt): Promise> { @@ -316,7 +331,11 @@ export class API { } async getTeamFolder (teamPath: string, folderId: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get(`teams/${teamPath}/folders/${folderId}`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedGetTeamFolder({ + client: this.generatedClient, + path: { teampath: teamPath, folderId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async updateTeamFolder (teamPath: string, folderId: string, payload: UpdateTeamFolderBody, options = defaultOption as Opt): Promise> { @@ -328,7 +347,11 @@ export class API { } async getTeamFolderOrder (teamPath: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get(`teams/${teamPath}/folders/folder-order`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedGetTeamFolderOrder({ + client: this.generatedClient, + path: { teampath: teamPath }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async updateTeamFolderOrder (teamPath: string, payload: UpdateFolderOrderBody, options = defaultOption as Opt): Promise> { diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index ec30075..48865a1 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -279,6 +279,80 @@ test('getTeamFolderList keeps legacy error wrapping', async () => { await expect(customClient.getTeamFolderList('missing-team')).rejects.toBeInstanceOf(HttpResponseError) }) +const folder = { + id: 'folder-1', + name: 'Research', + description: null, + icon: null, + color: null, + parentFolderId: null, + createdAt: 1700000000000, + updatedAt: 1700000000000, +} +const folderOrder = { root: ['folder-1'] } + +test.each([ + { + name: 'getFolder', + path: '/folders/folder-1', + body: folder, + get: (api: API) => api.getFolder('folder-1'), + getRaw: (api: API) => api.getFolder('folder-1', { unwrapData: false }), + }, + { + name: 'getTeamFolder', + path: '/teams/test-team/folders/folder-1', + body: folder, + get: (api: API) => api.getTeamFolder('test-team', 'folder-1'), + getRaw: (api: API) => api.getTeamFolder('test-team', 'folder-1', { unwrapData: false }), + }, + { + name: 'getFolderOrder', + path: '/folders/folder-order', + body: folderOrder, + get: (api: API) => api.getFolderOrder(), + getRaw: (api: API) => api.getFolderOrder({ unwrapData: false }), + }, + { + name: 'getTeamFolderOrder', + path: '/teams/test-team/folders/folder-order', + body: folderOrder, + get: (api: API) => api.getTeamFolderOrder('test-team'), + getRaw: (api: API) => api.getTeamFolderOrder('test-team', { unwrapData: false }), + }, +])('$name keeps legacy response shapes and authorization', async ({ path, body, get, getRaw }) => { + let authorization: string | null = null + server.use( + http.get(`https://api.hackmd.io/v1${path}`, ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json(body) + }) + ) + + expect(await get(client)).toEqual(body) + const raw = await getRaw(client) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(body) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test.each([ + ['getFolder', '/folders/missing', (api: API) => api.getFolder('missing')], + ['getTeamFolder', '/teams/test-team/folders/missing', (api: API) => api.getTeamFolder('test-team', 'missing')], +] as const)('%s keeps legacy error wrapping', async (_name, path, get) => { + server.use( + http.get(`https://api.hackmd.io/v1${path}`, () => + HttpResponse.json({ error: 'Folder not found' }, { status: 404 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(get(customClient)).rejects.toBeInstanceOf(HttpResponseError) +}) + test('updateFolderOrder sends order payload', async () => { let requestBody: unknown diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 1b247c1..aa6dcfa 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -43,6 +43,14 @@ async function exploreTeamFolders (client: API): Promise { return folders[0].updatedAt } +async function exploreFolderReads (client: API): Promise { + const personalFolder = await client.getFolder('FOLDER_ID') + const teamFolder = await client.getTeamFolder('TEAM_PATH', 'FOLDER_ID') + const personalOrder = await client.getFolderOrder() + const teamOrder = await client.getTeamFolderOrder('TEAM_PATH') + return [personalFolder.id, teamFolder.id, ...personalOrder.root, ...teamOrder.root] +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -54,4 +62,5 @@ void exploreHistory void exploreTeamNotes void exploreFolders void exploreTeamFolders +void exploreFolderReads void exploreRawNote From 14a6df9f351430fb57fafe37f60d42d95f1f4e98 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 02:56:25 +0800 Subject: [PATCH 13/31] refactor: delegate legacy folder writes to generated client --- nodejs/src/index.ts | 52 +++++++++++++--- nodejs/src/type.ts | 4 +- nodejs/tests/api.spec.ts | 99 +++++++++++++++++++++++++++---- nodejs/tests/types/playground.mts | 17 ++++++ 4 files changed, 150 insertions(+), 22 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 37e7af6..e2cb6fa 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -3,6 +3,10 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' import { + createFolder as generatedCreateFolder, + createTeamFolder as generatedCreateTeamFolder, + deleteFolder as generatedDeleteFolder, + deleteTeamFolder as generatedDeleteTeamFolder, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, @@ -13,6 +17,10 @@ import { listNotes, listTeamFolders, listTeamNotes, + updateFolder as generatedUpdateFolder, + updateFolderOrder as generatedUpdateFolderOrder, + updateTeamFolder as generatedUpdateTeamFolder, + updateTeamFolderOrder as generatedUpdateTeamFolderOrder, } from './generated/sdk.gen.js' import { CreateNoteOptions, @@ -291,7 +299,7 @@ export class API { } async createFolder (payload: CreateUserFolderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.post('folders', payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedCreateFolder({ client: this.generatedClient, body: payload, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getFolder (folderId: string, options = defaultOption as Opt): Promise> { @@ -303,11 +311,20 @@ export class API { } async updateFolder (folderId: string, payload: UpdateUserFolderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.patch(`folders/${folderId}`, payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedUpdateFolder({ + client: this.generatedClient, + path: { folderId }, + body: payload, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async deleteFolder (folderId: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.delete(`folders/${folderId}`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedDeleteFolder({ + client: this.generatedClient, + path: { folderId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async getFolderOrder (options = defaultOption as Opt): Promise> { @@ -315,7 +332,7 @@ export class API { } async updateFolderOrder (payload: UpdateFolderOrderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.put('folders/folder-order', payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedUpdateFolderOrder({ client: this.generatedClient, body: payload, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getTeamFolderList (teamPath: string, options = defaultOption as Opt): Promise> { @@ -327,7 +344,12 @@ export class API { } async createTeamFolder (teamPath: string, payload: CreateTeamFolderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.post(`teams/${teamPath}/folders`, payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedCreateTeamFolder({ + client: this.generatedClient, + path: { teampath: teamPath }, + body: payload, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async getTeamFolder (teamPath: string, folderId: string, options = defaultOption as Opt): Promise> { @@ -339,11 +361,20 @@ export class API { } async updateTeamFolder (teamPath: string, folderId: string, payload: UpdateTeamFolderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.patch(`teams/${teamPath}/folders/${folderId}`, payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedUpdateTeamFolder({ + client: this.generatedClient, + path: { teampath: teamPath, folderId }, + body: payload, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async deleteTeamFolder (teamPath: string, folderId: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.delete(`teams/${teamPath}/folders/${folderId}`), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedDeleteTeamFolder({ + client: this.generatedClient, + path: { teampath: teamPath, folderId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async getTeamFolderOrder (teamPath: string, options = defaultOption as Opt): Promise> { @@ -355,7 +386,12 @@ export class API { } async updateTeamFolderOrder (teamPath: string, payload: UpdateFolderOrderBody, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.put(`teams/${teamPath}/folders/folder-order`, payload), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedUpdateTeamFolderOrder({ + client: this.generatedClient, + path: { teampath: teamPath }, + body: payload, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index bccbc9a..83f79b9 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -144,8 +144,8 @@ export type GetFolder = ApiFolder export type GetTeamFolder = ApiFolder export type CreateFolderResult = ApiFolder export type CreateTeamFolderResult = ApiFolder -export type UpdateFolderResult = ApiFolder -export type UpdateTeamFolderResult = ApiFolder +export type UpdateFolderResult = void +export type UpdateTeamFolderResult = void export type DeleteFolderResult = void export type DeleteTeamFolderResult = void export type GetFolderOrder = ApiFolderOrder diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 48865a1..4dbdda2 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -353,24 +353,99 @@ test.each([ await expect(get(customClient)).rejects.toBeInstanceOf(HttpResponseError) }) -test('updateFolderOrder sends order payload', async () => { - let requestBody: unknown +const folderWriteCases = [ + { + name: 'createFolder', path: '/folders', method: 'POST', status: 201, + payload: { name: 'Research' }, + run: (api: API) => api.createFolder({ name: 'Research' }), + runRaw: (api: API) => api.createFolder({ name: 'Research' }, { unwrapData: false }), + }, + { + name: 'createTeamFolder', path: '/teams/test-team/folders', method: 'POST', status: 201, + payload: { name: 'Research' }, + run: (api: API) => api.createTeamFolder('test-team', { name: 'Research' }), + runRaw: (api: API) => api.createTeamFolder('test-team', { name: 'Research' }, { unwrapData: false }), + }, + { + name: 'updateFolder', path: '/folders/folder-1', method: 'PATCH', status: 202, + payload: { name: 'Updated' }, + run: (api: API) => api.updateFolder('folder-1', { name: 'Updated' }), + runRaw: (api: API) => api.updateFolder('folder-1', { name: 'Updated' }, { unwrapData: false }), + }, + { + name: 'updateTeamFolder', path: '/teams/test-team/folders/folder-1', method: 'PATCH', status: 202, + payload: { name: 'Updated' }, + run: (api: API) => api.updateTeamFolder('test-team', 'folder-1', { name: 'Updated' }), + runRaw: (api: API) => api.updateTeamFolder('test-team', 'folder-1', { name: 'Updated' }, { unwrapData: false }), + }, + { + name: 'deleteFolder', path: '/folders/folder-1', method: 'DELETE', status: 204, + payload: undefined, + run: (api: API) => api.deleteFolder('folder-1'), + runRaw: (api: API) => api.deleteFolder('folder-1', { unwrapData: false }), + }, + { + name: 'deleteTeamFolder', path: '/teams/test-team/folders/folder-1', method: 'DELETE', status: 204, + payload: undefined, + run: (api: API) => api.deleteTeamFolder('test-team', 'folder-1'), + runRaw: (api: API) => api.deleteTeamFolder('test-team', 'folder-1', { unwrapData: false }), + }, + { + name: 'updateFolderOrder', path: '/folders/folder-order', method: 'PUT', status: 204, + payload: { order: { root: ['folder-1'] } }, + run: (api: API) => api.updateFolderOrder({ order: { root: ['folder-1'] } }), + runRaw: (api: API) => api.updateFolderOrder({ order: { root: ['folder-1'] } }, { unwrapData: false }), + }, + { + name: 'updateTeamFolderOrder', path: '/teams/test-team/folders/folder-order', method: 'PUT', status: 204, + payload: { order: { root: ['folder-1'] } }, + run: (api: API) => api.updateTeamFolderOrder('test-team', { order: { root: ['folder-1'] } }), + runRaw: (api: API) => api.updateTeamFolderOrder('test-team', { order: { root: ['folder-1'] } }, { unwrapData: false }), + }, +] +test.each(folderWriteCases)('$name keeps the path, payload, status, and response shape', async ({ path, method, payload, status, run, runRaw }) => { + const requests: Array<{ method: string; body: string; authorization: string | null }> = [] server.use( - http.put('https://api.hackmd.io/v1/folders/folder-order', async ({ request }) => { - requestBody = await request.json() - - return HttpResponse.json({}) - }), + http.all(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ + method: request.method, + body: await request.text(), + authorization: request.headers.get('Authorization'), + }) + return status === 201 ? HttpResponse.json(folder, { status }) : new HttpResponse(null, { status }) + }) ) - await client.updateFolderOrder({ - order: { root: ['a', 'b'], parent: ['c'] }, - }) + const unwrapped = await run(client) + const raw = await runRaw(client) + expect(raw.status).toBe(status) + if (status === 201) { + expect(unwrapped).toEqual(folder) + expect(raw.data).toEqual(folder) + } else { + expect([undefined, '']).toContain(unwrapped) + expect([undefined, '']).toContain(raw.data) + } + expect(requests).toEqual([1, 2].map(() => ({ + method, + body: payload ? JSON.stringify(payload) : '', + authorization: `Bearer ${process.env.HACKMD_ACCESS_TOKEN}`, + }))) +}) - expect(requestBody).toEqual({ - order: { root: ['a', 'b'], parent: ['c'] }, +test('folder writes keep legacy error wrapping', async () => { + server.use( + http.patch('https://api.hackmd.io/v1/folders/missing', () => + HttpResponse.json({ error: 'Folder not found' }, { status: 404 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, }) + + await expect(customClient.updateFolder('missing', { name: 'Updated' })).rejects.toBeInstanceOf(HttpResponseError) }) test('uploadNoteImage sends image as multipart form data', async () => { diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index aa6dcfa..55e68e3 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -51,6 +51,22 @@ async function exploreFolderReads (client: API): Promise { return [personalFolder.id, teamFolder.id, ...personalOrder.root, ...teamOrder.root] } +async function exploreFolderWrites (client: API): Promise { + const folder = await client.createFolder({ name: 'Research' }) + const teamFolder = await client.createTeamFolder('TEAM_PATH', { name: 'Research' }) + const updated: void = await client.updateFolder(folder.id, { name: 'Updated' }) + const teamUpdated: void = await client.updateTeamFolder('TEAM_PATH', teamFolder.id, { name: 'Updated' }) + const ordered: void = await client.updateFolderOrder({ order: { root: [folder.id] } }) + const teamOrdered: void = await client.updateTeamFolderOrder('TEAM_PATH', { order: { root: [teamFolder.id] } }) + const raw = await client.deleteFolder(folder.id, { unwrapData: false }) + const status: number = raw.status + void updated + void teamUpdated + void ordered + void teamOrdered + void status +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -63,4 +79,5 @@ void exploreTeamNotes void exploreFolders void exploreTeamFolders void exploreFolderReads +void exploreFolderWrites void exploreRawNote From ba8f6dd6270980ff176d4b4b30cc1cc0590d701d Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 03:09:43 +0800 Subject: [PATCH 14/31] refactor: delegate legacy note updates and deletes to generated client --- nodejs/src/index.ts | 56 +++++++++++++++++---- nodejs/src/type.ts | 5 +- nodejs/tests/api.spec.ts | 81 +++++++++++++++++++++++++++---- nodejs/tests/e2e/api.e2e.spec.ts | 6 +-- nodejs/tests/etag.spec.ts | 39 ++------------- nodejs/tests/types/playground.mts | 12 +++++ nodejs/typedoc.json | 1 + 7 files changed, 138 insertions(+), 62 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index e2cb6fa..b0f8862 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -6,7 +6,9 @@ import { createFolder as generatedCreateFolder, createTeamFolder as generatedCreateTeamFolder, deleteFolder as generatedDeleteFolder, + deleteNote as generatedDeleteNote, deleteTeamFolder as generatedDeleteTeamFolder, + deleteTeamNote as generatedDeleteTeamNote, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, @@ -19,8 +21,10 @@ import { listTeamNotes, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, + updateNote as generatedUpdateNote, updateTeamFolder as generatedUpdateTeamFolder, updateTeamFolderOrder as generatedUpdateTeamFolderOrder, + updateTeamNote as generatedUpdateTeamNote, } from './generated/sdk.gen.js' import { CreateNoteOptions, @@ -34,7 +38,6 @@ import { GetUserTeams, GetTeamNotes, CreateTeamNote, - SingleNote, UpdateNoteOptions, GetFolders, GetFolder, @@ -66,6 +69,11 @@ const defaultOption: RequestOptions = { } type OptionReturnType = Opt extends { unwrapData: false } ? AxiosResponse : Opt extends { unwrapData: true } ? T : T +type UpdateNoteReturnType = Opt extends { unwrapData: false } + ? AxiosResponse & { status: 202 } + : UpdateNoteResult + +export type UpdateNoteResult = { status: 202; etag?: string } export type GetNoteSuccess = GetUserNote & { status: 200; etag?: string } export type GetNoteNotModified = { status: 304; etag?: string } @@ -244,16 +252,30 @@ export class API { return this.unwrapData(this.axios.post("notes", payload), options.unwrapData, true) as unknown as OptionReturnType } - async updateNoteContent (noteId: string, content?: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.patch(`notes/${noteId}`, { content }), options.unwrapData, true) as unknown as OptionReturnType + async updateNoteContent (noteId: string, content?: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUpdateNote({ + client: this.generatedClient, + path: { noteId }, + body: { content }, + throwOnError: true, + }), options.unwrapData, true) as unknown as UpdateNoteReturnType } - async updateNote (noteId: string, payload: UpdateNoteOptions, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.patch(`notes/${noteId}`, payload), options.unwrapData, true) as unknown as OptionReturnType + async updateNote (noteId: string, payload: UpdateNoteOptions, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUpdateNote({ + client: this.generatedClient, + path: { noteId }, + body: payload, + throwOnError: true, + }), options.unwrapData, true) as unknown as UpdateNoteReturnType } - async deleteNote (noteId: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.delete(`notes/${noteId}`), options.unwrapData) as unknown as OptionReturnType + async deleteNote (noteId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedDeleteNote({ + client: this.generatedClient, + path: { noteId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType } async uploadNoteImage (noteId: string, image: Blob, options = defaultOption as Opt): Promise> { @@ -283,15 +305,29 @@ export class API { } async updateTeamNoteContent (teamPath: string, noteId: string, content?: string): Promise { - return this.axios.patch(`teams/${teamPath}/notes/${noteId}`, { content }) + return generatedUpdateTeamNote({ + client: this.generatedClient, + path: { teampath: teamPath, noteId }, + body: { content }, + throwOnError: true, + }) } async updateTeamNote (teamPath: string, noteId: string, options: UpdateNoteOptions): Promise { - return this.axios.patch(`teams/${teamPath}/notes/${noteId}`, options) + return generatedUpdateTeamNote({ + client: this.generatedClient, + path: { teampath: teamPath, noteId }, + body: options, + throwOnError: true, + }) } async deleteTeamNote (teamPath: string, noteId: string): Promise { - return this.axios.delete(`teams/${teamPath}/notes/${noteId}`) + return generatedDeleteTeamNote({ + client: this.generatedClient, + path: { teampath: teamPath, noteId }, + throwOnError: true, + }) } async getFolderList (options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 83f79b9..a0c75ba 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -5,6 +5,7 @@ import type { NoteType as GeneratedNote, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, + UpdateNoteData as GeneratedUpdateNoteData, } from './generated/types.gen.js' export enum TeamVisibilityType { @@ -73,9 +74,7 @@ export type FolderPath = GeneratedFolderPath export type Note = GeneratedNote export type SingleNote = GeneratedSingleNote -export type UpdateNoteOptions = Partial> & { - parentFolderId?: string -} +export type UpdateNoteOptions = GeneratedUpdateNoteData['body'] // User export type GetMe = User diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 4dbdda2..356b481 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -490,14 +490,7 @@ test('should support updating team note title and tags metadata', async () => { server.use( http.patch('https://api.hackmd.io/v1/teams/test-team/notes/test-note-id', async ({ request }) => { requestBody = await request.json() - - return HttpResponse.json( - { - id: 'test-note-id', - title: 'Updated Team Note', - tags: updatedTags - } - ) + return new HttpResponse(null, { status: 202 }) }) ) @@ -510,7 +503,77 @@ test('should support updating team note title and tags metadata', async () => { title: 'Updated Team Note', tags: updatedTags }) - expect(response).toHaveProperty('status', 200) + expect(response.status).toBe(202) + expect([undefined, '']).toContain(response.data) +}) + +test('updateNote keeps the legacy status/etag wrapper and raw response', async () => { + const payload = { description: 'Updated', parentFolderId: null } + const requests: string[] = [] + server.use( + http.patch('https://api.hackmd.io/v1/notes/test-note-id', async ({ request }) => { + requests.push(await request.text()) + return new HttpResponse(null, { status: 202, headers: { ETag: 'W/"updated"' } }) + }) + ) + + expect(await client.updateNote('test-note-id', payload)).toEqual({ status: 202, etag: 'W/"updated"' }) + const raw = await client.updateNote('test-note-id', payload, { unwrapData: false }) + expect(raw.status).toBe(202) + expect([undefined, '']).toContain(raw.data) + expect(requests).toEqual([JSON.stringify(payload), JSON.stringify(payload)]) +}) + +test('updateTeamNoteContent keeps the team path and raw response', async () => { + let requestBody: unknown + server.use( + http.patch('https://api.hackmd.io/v1/teams/test-team/notes/test-note-id', async ({ request }) => { + requestBody = await request.json() + return new HttpResponse(null, { status: 202 }) + }) + ) + + const response = await client.updateTeamNoteContent('test-team', 'test-note-id', 'Updated content') + expect(requestBody).toEqual({ content: 'Updated content' }) + expect(response.status).toBe(202) +}) + +test.each([ + { + name: 'deleteNote', path: '/notes/test-note-id', + run: (api: API) => api.deleteNote('test-note-id', { unwrapData: false }), + }, + { + name: 'deleteTeamNote', path: '/teams/test-team/notes/test-note-id', + run: (api: API) => api.deleteTeamNote('test-team', 'test-note-id'), + }, +])('$name keeps the path and 204 response', async ({ path, run }) => { + let authorization: string | null = null + server.use( + http.delete(`https://api.hackmd.io/v1${path}`, ({ request }) => { + authorization = request.headers.get('Authorization') + return new HttpResponse(null, { status: 204 }) + }) + ) + + const response = await run(client) + expect(response.status).toBe(204) + expect([undefined, '']).toContain(response.data) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('note mutations keep legacy error wrapping', async () => { + server.use( + http.patch('https://api.hackmd.io/v1/teams/test-team/notes/missing', () => + HttpResponse.json({ error: 'Note not found' }, { status: 404 }) + ) + ) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.updateTeamNote('test-team', 'missing', { title: 'Updated' })).rejects.toBeInstanceOf(HttpResponseError) }) test('should not retry non-idempotent requests when wrapping errors', async () => { diff --git a/nodejs/tests/e2e/api.e2e.spec.ts b/nodejs/tests/e2e/api.e2e.spec.ts index 8a350fb..ffe5726 100644 --- a/nodejs/tests/e2e/api.e2e.spec.ts +++ b/nodejs/tests/e2e/api.e2e.spec.ts @@ -131,11 +131,7 @@ describe('HackMD API (live e2e)', () => { tags: ['e2e', 'updated'], }, { unwrapData: false }) - expect([200, 202]).toContain(patch.status) - const patchedBody = patch.data as { content?: string } - if (typeof patchedBody.content === 'string' && patchedBody.content.length > 0) { - expect(patchedBody.content).toContain('patched') - } + expect(patch.status).toBe(202) const n = await client.getNote(noteId) expect(n.title).toBe(title) diff --git a/nodejs/tests/etag.spec.ts b/nodejs/tests/etag.spec.ts index b027d49..faadbdc 100644 --- a/nodejs/tests/etag.spec.ts +++ b/nodejs/tests/etag.spec.ts @@ -297,31 +297,14 @@ describe('Etag support', () => { server.use( http.patch('https://api.hackmd.io/v1/notes/test-note-id', () => { - return HttpResponse.json( - { - id: 'test-note-id', - title: 'Updated Test Note', - content: 'Updated content via updateNote' - }, - { - headers: { - 'ETag': mockEtag - } - } - ) + return new HttpResponse(null, { status: 202, headers: { ETag: mockEtag } }) }) ) // Make request with default unwrapData: true const response = await client.updateNoteContent('test-note-id', 'Updated content') - // Verify response has etag property - expect(response).toHaveProperty('etag', mockEtag) - - // Verify data properties still exist - expect(response).toHaveProperty('id', 'test-note-id') - expect(response).toHaveProperty('title', 'Updated Test Note') - expect(response).toHaveProperty('content', 'Updated content via updateNote') + expect(response).toEqual({ status: 202, etag: mockEtag }) }) test('should support updating note title and tags metadata', async () => { @@ -333,19 +316,7 @@ describe('Etag support', () => { http.patch('https://api.hackmd.io/v1/notes/test-note-id', async ({ request }) => { requestBody = await request.json() - return HttpResponse.json( - { - id: 'test-note-id', - title: 'Updated Metadata Title', - tags: updatedTags, - content: 'Updated content via updateNote' - }, - { - headers: { - 'ETag': mockEtag - } - } - ) + return new HttpResponse(null, { status: 202, headers: { ETag: mockEtag } }) }) ) @@ -358,9 +329,7 @@ describe('Etag support', () => { title: 'Updated Metadata Title', tags: updatedTags }) - expect(response).toHaveProperty('etag', mockEtag) - expect(response).toHaveProperty('title', 'Updated Metadata Title') - expect(response.tags).toEqual(updatedTags) + expect(response).toEqual({ status: 202, etag: mockEtag }) }) }) }) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 55e68e3..9d51c60 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -67,6 +67,17 @@ async function exploreFolderWrites (client: API): Promise { void status } +async function exploreNoteMutations (client: API): Promise { + const updated = await client.updateNote('NOTE_ID', { description: 'Updated', parentFolderId: null }) + const status: 202 = updated.status + const raw = await client.updateNoteContent('NOTE_ID', 'Updated', { unwrapData: false }) + const rawStatus: 202 = raw.status + const deleted: void = await client.deleteNote('NOTE_ID') + void status + void rawStatus + void deleted +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -80,4 +91,5 @@ void exploreFolders void exploreTeamFolders void exploreFolderReads void exploreFolderWrites +void exploreNoteMutations void exploreRawNote diff --git a/nodejs/typedoc.json b/nodejs/typedoc.json index 20a13f2..55005a8 100644 --- a/nodejs/typedoc.json +++ b/nodejs/typedoc.json @@ -20,6 +20,7 @@ "Auth", "TDataShape", "OptionReturnType", + "UpdateNoteReturnType", "ClientMeta", "StreamEvent" ], From 931da04c89ad7a9188e50ebad7bc84ca49993078 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 05:23:34 +0800 Subject: [PATCH 15/31] refactor: delegate legacy note creation to generated client --- nodejs/src/index.ts | 30 +++++++++++++++++++++++++----- nodejs/src/type.ts | 19 ++++++------------- nodejs/tests/api.spec.ts | 30 ++++++++++++++++++++++++++++++ nodejs/tests/e2e/api.e2e.spec.ts | 4 ++++ nodejs/tests/etag.spec.ts | 3 +++ nodejs/tests/types/playground.mts | 11 +++++++++++ nodejs/typedoc.json | 2 ++ 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index b0f8862..284a364 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -4,7 +4,9 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestCo import { createClient, type Client } from './generated/client/index.js' import { createFolder as generatedCreateFolder, + createNote as generatedCreateNote, createTeamFolder as generatedCreateTeamFolder, + createTeamNote as generatedCreateTeamNote, deleteFolder as generatedDeleteFolder, deleteNote as generatedDeleteNote, deleteTeamFolder as generatedDeleteTeamFolder, @@ -28,16 +30,17 @@ import { } from './generated/sdk.gen.js' import { CreateNoteOptions, + CreateNoteMultiStatusResponse, CreateTeamFolderBody, CreateUserFolderBody, GetMe, GetUserHistory, GetUserNotes, GetUserNote, - CreateUserNote, GetUserTeams, GetTeamNotes, CreateTeamNote, + SingleNote, UpdateNoteOptions, GetFolders, GetFolder, @@ -72,8 +75,20 @@ type OptionReturnType = Opt extends { unwrapData: false } ? AxiosRespons type UpdateNoteReturnType = Opt extends { unwrapData: false } ? AxiosResponse & { status: 202 } : UpdateNoteResult +type CreateNoteReturnType = Opt extends { unwrapData: false } + ? CreateNoteRawResult + : CreateNoteResult +type CreateTeamNoteReturnType = Opt extends { unwrapData: false } + ? CreateNoteRawResult + : CreateTeamNote export type UpdateNoteResult = { status: 202; etag?: string } +export type CreateNoteResult = + | (SingleNote & { status: 201; etag?: string }) + | (CreateNoteMultiStatusResponse & { status: 207; etag?: string }) +export type CreateNoteRawResult = + | (AxiosResponse & { status: 201 }) + | (AxiosResponse & { status: 207 }) export type GetNoteSuccess = GetUserNote & { status: 200; etag?: string } export type GetNoteNotModified = { status: 304; etag?: string } @@ -248,8 +263,8 @@ export class API { return this.unwrapData(request, options.unwrapData, true) as unknown as Promise } - async createNote (payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.post("notes", payload), options.unwrapData, true) as unknown as OptionReturnType + async createNote (payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCreateNote({ client: this.generatedClient, body: payload, throwOnError: true }), options.unwrapData, true) as unknown as CreateNoteReturnType } async updateNoteContent (noteId: string, content?: string, options = defaultOption as Opt): Promise> { @@ -300,8 +315,13 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } - async createTeamNote (teamPath: string, payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.post(`teams/${teamPath}/notes`, payload), options.unwrapData) as unknown as OptionReturnType + async createTeamNote (teamPath: string, payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCreateTeamNote({ + client: this.generatedClient, + path: { teampath: teamPath }, + body: payload, + throwOnError: true, + }), options.unwrapData) as unknown as CreateTeamNoteReturnType } async updateTeamNoteContent (teamPath: string, noteId: string, content?: string): Promise { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index a0c75ba..574a673 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,5 +1,7 @@ import type { ApiFolder as GeneratedApiFolder, + CreateNoteData as GeneratedCreateNoteData, + CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, FolderPath as GeneratedFolderPath, GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, @@ -28,17 +30,8 @@ export enum CommentPermissionType { EVERYONE = 'everyone' } -export type CreateNoteOptions = { - title?: string - content?: string - description?: string - tags?: string[] - readPermission?: NotePermissionRole, - writePermission?: NotePermissionRole, - commentPermission?: CommentPermissionType, - permalink?: string - parentFolderId?: string -} +export type CreateNoteOptions = Exclude, string> +export type CreateNoteMultiStatusResponse = GeneratedCreateNoteMultiStatusResponse export type Team = { id: string @@ -83,7 +76,7 @@ export type GetMe = User export type GetUserNotes = Note[] export type GetUserNote = SingleNote export type GetUserHistory = GeneratedUserHistory -export type CreateUserNote = SingleNote +export type CreateUserNote = SingleNote | CreateNoteMultiStatusResponse export type UpdateUserNote = void export type DeleteUserNote = void @@ -103,7 +96,7 @@ export type GetUserTeams = Team[] // Team notes export type GetTeamNotes = Note[] -export type CreateTeamNote = SingleNote +export type CreateTeamNote = SingleNote | CreateNoteMultiStatusResponse export type UpdateTeamNote = void export type DeleteTeamNote = void diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 356b481..43a66aa 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -576,6 +576,36 @@ test('note mutations keep legacy error wrapping', async () => { await expect(customClient.updateTeamNote('test-team', 'missing', { title: 'Updated' })).rejects.toBeInstanceOf(HttpResponseError) }) +test.each([ + { scope: 'personal', path: '/notes', status: 201, body: { id: 'note-1', title: 'New note' } }, + { scope: 'personal', path: '/notes', status: 207, body: { note: { id: 'note-1', title: 'New note' }, error: 'Failed to add note to folder' } }, + { scope: 'team', path: '/teams/test-team/notes', status: 201, body: { id: 'note-1', title: 'New note' } }, + { scope: 'team', path: '/teams/test-team/notes', status: 207, body: { note: { id: 'note-1', title: 'New note' }, error: 'Failed to add note to folder' } }, +])('createNote $scope $status preserves its request and legacy response wrapper', async ({ scope, path, status, body }) => { + const requests: Array<{ body: unknown; authorization: string | null }> = [] + server.use( + http.post(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ body: await request.json(), authorization: request.headers.get('Authorization') }) + return HttpResponse.json(body, { status, headers: { ETag: 'W/"created"' } }) + }) + ) + const payload = { title: 'New note', noteFeatures: { citation: 'signed_in_users' as const } } + const result = scope === 'personal' + ? await client.createNote(payload) + : await client.createTeamNote('test-team', payload) + const raw = scope === 'personal' + ? await client.createNote(payload, { unwrapData: false }) + : await client.createTeamNote('test-team', payload, { unwrapData: false }) + + expect(result).toEqual(scope === 'personal' ? { ...body, status, etag: 'W/"created"' } : body) + expect(raw.status).toBe(status) + expect(raw.data).toEqual(body) + expect(requests).toEqual([1, 2].map(() => ({ + body: payload, + authorization: `Bearer ${process.env.HACKMD_ACCESS_TOKEN}`, + }))) +}) + test('should not retry non-idempotent requests when wrapping errors', async () => { let requestCount = 0 const clientWithRetryAndWrap = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, { diff --git a/nodejs/tests/e2e/api.e2e.spec.ts b/nodejs/tests/e2e/api.e2e.spec.ts index ffe5726..d342cd3 100644 --- a/nodejs/tests/e2e/api.e2e.spec.ts +++ b/nodejs/tests/e2e/api.e2e.spec.ts @@ -110,6 +110,10 @@ describe('HackMD API (live e2e)', () => { tags: ['e2e'], }) + if (created.status === 207) { + noteId = created.note.id + throw new Error(`Note created, but folder placement failed: ${created.error}`) + } expect(created.id).toEqual(expect.any(String)) expect(created.title).toBe(title) expect(created.tags).toContain('e2e') diff --git a/nodejs/tests/etag.spec.ts b/nodejs/tests/etag.spec.ts index faadbdc..475b98a 100644 --- a/nodejs/tests/etag.spec.ts +++ b/nodejs/tests/etag.spec.ts @@ -270,6 +270,7 @@ describe('Etag support', () => { title: 'New Test Note' }, { + status: 201, headers: { 'ETag': mockEtag } @@ -283,6 +284,8 @@ describe('Etag support', () => { // Verify response has etag property expect(response).toHaveProperty('etag', mockEtag) + expect(response.status).toBe(201) + if (response.status !== 201) throw new Error('Expected 201') // Verify data properties still exist expect(response).toHaveProperty('id', 'new-note-id') diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 9d51c60..04c34e4 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -78,6 +78,16 @@ async function exploreNoteMutations (client: API): Promise { void deleted } +async function exploreNoteCreation (client: API): Promise { + const personal = await client.createNote({ title: 'New note' }) + const personalId = personal.status === 201 ? personal.id : personal.note.id + const raw = await client.createNote({ title: 'New note' }, { unwrapData: false }) + const rawId = raw.status === 201 ? raw.data.id : raw.data.note.id + const team = await client.createTeamNote('TEAM_PATH', { title: 'New note' }) + const teamId = 'note' in team ? team.note.id : team.id + return [personalId, rawId, teamId].join(',') +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -92,4 +102,5 @@ void exploreTeamFolders void exploreFolderReads void exploreFolderWrites void exploreNoteMutations +void exploreNoteCreation void exploreRawNote diff --git a/nodejs/typedoc.json b/nodejs/typedoc.json index 55005a8..d7f0253 100644 --- a/nodejs/typedoc.json +++ b/nodejs/typedoc.json @@ -21,6 +21,8 @@ "TDataShape", "OptionReturnType", "UpdateNoteReturnType", + "CreateNoteReturnType", + "CreateTeamNoteReturnType", "ClientMeta", "StreamEvent" ], From 0ff2912ecaddb8c316cd69088270c93bcc41a5ed Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 05:31:23 +0800 Subject: [PATCH 16/31] refactor: delegate legacy profile and teams reads to generated client --- nodejs/src/index.ts | 6 ++-- nodejs/src/type.ts | 24 +++----------- nodejs/tests/api.spec.ts | 53 +++++++++++++++++++++++++++++++ nodejs/tests/mock/index.ts | 3 +- nodejs/tests/types/playground.mts | 12 +++++++ 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 284a364..6c07a71 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -11,6 +11,7 @@ import { deleteNote as generatedDeleteNote, deleteTeamFolder as generatedDeleteTeamFolder, deleteTeamNote as generatedDeleteTeamNote, + getCurrentUser, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, @@ -19,6 +20,7 @@ import { getTeamFolderOrder as generatedGetTeamFolderOrder, listFolders, listNotes, + listTeams, listTeamFolders, listTeamNotes, updateFolder as generatedUpdateFolder, @@ -230,7 +232,7 @@ export class API { ) } async getMe (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get("me"), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(getCurrentUser({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getHistory (options = defaultOption as Opt): Promise> { @@ -304,7 +306,7 @@ export class API { } async getTeams (options = defaultOption as Opt): Promise> { - return this.unwrapData(this.axios.get("teams"), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(listTeams({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 574a673..9c12652 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -7,7 +7,9 @@ import type { NoteType as GeneratedNote, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, + Team as GeneratedTeam, UpdateNoteData as GeneratedUpdateNoteData, + User as GeneratedUser, } from './generated/types.gen.js' export enum TeamVisibilityType { @@ -33,26 +35,8 @@ export enum CommentPermissionType { export type CreateNoteOptions = Exclude, string> export type CreateNoteMultiStatusResponse = GeneratedCreateNoteMultiStatusResponse -export type Team = { - id: string - ownerId: string - name: string - logo: string - path: string - description: string - hardBreaks: boolean - visibility: TeamVisibilityType - createdAt: Date -} - -export type User = { - id: string - email: string | null - name: string - userPath: string - photo: string - teams: Team[] -} +export type Team = GeneratedTeam +export type User = GeneratedUser export type SimpleUserProfile = GeneratedSimpleUserProfile diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 43a66aa..9939573 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -37,6 +37,59 @@ test('getMe unwrapped', async () => { expect(response).toHaveProperty('email') expect(response).toHaveProperty('userPath') expect(response).toHaveProperty('photo') + expect(response).toHaveProperty('upgraded', false) +}) + +test('getMe keeps the generated profile shape and authorization', async () => { + const team = { + id: 'team-1', ownerId: 'user-1', name: 'Research', logo: '', path: 'research', + description: null, visibility: 'private', upgraded: false, createdAt: 1700000000000, + } + const profile = { + id: 'user-1', email: null, name: 'Researcher', userPath: 'researcher', + photo: '', teams: [team], upgraded: true, + } + let authorization: string | null = null + server.use(http.get('https://api.hackmd.io/v1/me', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json(profile) + })) + + expect(await client.getMe()).toEqual(profile) + const raw = await client.getMe({ unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(profile) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getTeams keeps the generated team shape and legacy response forms', async () => { + const teams = [{ + id: 'team-1', ownerId: 'user-1', name: 'Research', logo: '', path: 'research', + description: null, visibility: 'private', upgraded: false, createdAt: 1700000000000, + }] + let authorization: string | null = null + server.use(http.get('https://api.hackmd.io/v1/teams', ({ request }) => { + authorization = request.headers.get('Authorization') + return HttpResponse.json(teams) + })) + + expect(await client.getTeams()).toEqual(teams) + const raw = await client.getTeams({ unwrapData: false }) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(teams) + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) +}) + +test('getTeams keeps legacy error wrapping', async () => { + server.use(http.get('https://api.hackmd.io/v1/teams', () => + HttpResponse.json({ error: 'Forbidden' }, { status: 403 }) + )) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.getTeams()).rejects.toBeInstanceOf(HttpResponseError) }) test('getNoteList keeps the legacy response shapes and authorization', async () => { diff --git a/nodejs/tests/mock/index.ts b/nodejs/tests/mock/index.ts index 7a4f6ee..9f7dce2 100644 --- a/nodejs/tests/mock/index.ts +++ b/nodejs/tests/mock/index.ts @@ -27,7 +27,8 @@ export const server = setupServer( email: faker.internet.email(), userPath: faker.internet.userName(), photo: faker.image.avatar(), - teams: [] + teams: [], + upgraded: false, }) }), ) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 04c34e4..75cbb5c 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -88,6 +88,17 @@ async function exploreNoteCreation (client: API): Promise { return [personalId, rawId, teamId].join(',') } +async function exploreProfileAndTeams (client: API): Promise { + const profile = await client.getMe() + const teams = await client.getTeams() + const createdAt: number = teams[0].createdAt + const description: string | null = profile.teams[0].description + const upgraded: boolean = profile.upgraded + void description + void upgraded + return createdAt +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -103,4 +114,5 @@ void exploreFolderReads void exploreFolderWrites void exploreNoteMutations void exploreNoteCreation +void exploreProfileAndTeams void exploreRawNote From 6316aabb70e02a5577be1266e9f9c64b2b14efd0 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 05:45:33 +0800 Subject: [PATCH 17/31] refactor: delegate legacy image upload to generated client --- nodejs/src/index.ts | 9 ++++++++- nodejs/src/type.ts | 7 ++----- nodejs/tests/api.spec.ts | 25 ++++++++++++++++++++++++- nodejs/tests/types/playground.mts | 10 ++++++++++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 6c07a71..ce44c22 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -29,6 +29,7 @@ import { updateTeamFolder as generatedUpdateTeamFolder, updateTeamFolderOrder as generatedUpdateTeamFolderOrder, updateTeamNote as generatedUpdateTeamNote, + uploadNoteImage as generatedUploadNoteImage, } from './generated/sdk.gen.js' import { CreateNoteOptions, @@ -300,7 +301,13 @@ export class API { formData.append('image', image, options.filename ?? undefined) return this.unwrapData( - this.axios.post(`notes/${noteId}/images`, formData), + generatedUploadNoteImage({ + client: this.generatedClient, + path: { noteId }, + body: { image }, + bodySerializer: () => formData, + throwOnError: true, + }), options.unwrapData, ) as unknown as OptionReturnType } diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 9c12652..78d1389 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -5,6 +5,7 @@ import type { FolderPath as GeneratedFolderPath, GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, + NoteImageUploadResponse as GeneratedNoteImageUploadResponse, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, Team as GeneratedTeam, @@ -64,11 +65,7 @@ export type CreateUserNote = SingleNote | CreateNoteMultiStatusResponse export type UpdateUserNote = void export type DeleteUserNote = void -export type UploadNoteImageResponse = { - data: { - link: string - } -} +export type UploadNoteImageResponse = GeneratedNoteImageUploadResponse export type UploadNoteImageOptions = { unwrapData?: boolean diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 9939573..38593bb 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -504,10 +504,12 @@ test('folder writes keep legacy error wrapping', async () => { test('uploadNoteImage sends image as multipart form data', async () => { let uploaded: FormDataEntryValue | null = null let contentType: string | null = null + let authorization: string | null = null server.use( http.post('https://api.hackmd.io/v1/notes/test-note-id/images', async ({ request }) => { contentType = request.headers.get('content-type') + authorization = request.headers.get('Authorization') const formData = await request.formData() uploaded = formData.get('image') @@ -515,7 +517,7 @@ test('uploadNoteImage sends image as multipart form data', async () => { data: { link: 'https://hackmd.io/_uploads/test-image.png', }, - }) + }, { status: 201 }) }), ) @@ -529,11 +531,32 @@ test('uploadNoteImage sends image as multipart form data', async () => { expect(uploaded).toBeInstanceOf(Blob) const uploadedBlob = uploaded as unknown as Blob expect(uploadedBlob.type).toBe('image/png') + expect((uploaded as unknown as File).name).toBe('test-image.png') + expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) expect(response).toEqual({ data: { link: 'https://hackmd.io/_uploads/test-image.png', }, }) + + const raw = await client.uploadNoteImage('test-note-id', new Blob(['test image'], { type: 'image/png' }), { + filename: 'test-image.png', + unwrapData: false, + }) + expect(raw.status).toBe(201) + expect(raw.data).toEqual(response) +}) + +test('uploadNoteImage keeps legacy error wrapping', async () => { + server.use(http.post('https://api.hackmd.io/v1/notes/missing/images', () => + HttpResponse.json({ message: 'Forbidden' }, { status: 403 }) + )) + const customClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + + await expect(customClient.uploadNoteImage('missing', new Blob(['image'], { type: 'image/png' }))).rejects.toBeInstanceOf(HttpResponseError) }) test('should support updating team note title and tags metadata', async () => { diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 75cbb5c..ab048f6 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -99,6 +99,15 @@ async function exploreProfileAndTeams (client: API): Promise { return createdAt } +async function exploreImageUpload (client: API): Promise { + const image = new Blob(['image'], { type: 'image/png' }) + const uploaded = await client.uploadNoteImage('NOTE_ID', image, { filename: 'image.png' }) + const raw = await client.uploadNoteImage('NOTE_ID', image, { unwrapData: false }) + const status: number = raw.status + void status + return uploaded.data.link +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -115,4 +124,5 @@ void exploreFolderWrites void exploreNoteMutations void exploreNoteCreation void exploreProfileAndTeams +void exploreImageUpload void exploreRawNote From 2832309e65108280f7c20a9a2f160f349c1e5346 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 17:20:47 +0800 Subject: [PATCH 18/31] fix: emit CommonJS shared chunks with cjs extension --- nodejs/rollup.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/nodejs/rollup.config.js b/nodejs/rollup.config.js index cf7e01b..cbfb425 100644 --- a/nodejs/rollup.config.js +++ b/nodejs/rollup.config.js @@ -38,6 +38,7 @@ export default [ output: { dir: 'dist', entryFileNames: '[name].cjs', + chunkFileNames: '[name]-[hash].cjs', format: 'cjs', sourcemap: true, exports: 'named' From 043e9ff46bb2a86be62684406025f7a2989147f3 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 17:44:39 +0800 Subject: [PATCH 19/31] feat: add team note detail to API client --- nodejs/README.md | 14 +++++- nodejs/src/index.ts | 19 ++++++++ nodejs/tests/etag.spec.ts | 73 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 15 +++++++ 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index 1b4491a..8c69152 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -119,6 +119,16 @@ const response = await client.getNote('note-id', { const note = response.status === 304 ? first.data : response.data ``` +Team notes work the same way through the existing client: + +```javascript +const teamFirst = await client.getTeamNote('team-path', 'note-id') +const teamResponse = await client.getTeamNote('team-path', 'note-id', { + etag: teamFirst.etag, +}) +const teamNote = teamResponse.status === 304 ? teamFirst : teamResponse +``` + ### Image Upload Upload an image to a note with `uploadNoteImage`. The API returns the uploaded image link in `data.link`. @@ -164,8 +174,8 @@ be edited manually. ## API -The [API reference](https://hackmdio.github.io/api-client/) covers the SDK, -compatibility API, and every raw operation and DTO. To explore autocomplete +The [API reference](https://hackmdio.github.io/api-client/) covers the existing +`API` class and every raw operation and DTO. To explore autocomplete without a real token, open the [type-only StackBlitz example](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts). The Pages site is deployed from `master` only. diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index ce44c22..3338c56 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -16,6 +16,7 @@ import { getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, getNote as generatedGetNote, + getTeamNote as generatedGetTeamNote, getTeamFolder as generatedGetTeamFolder, getTeamFolderOrder as generatedGetTeamFolderOrder, listFolders, @@ -316,6 +317,24 @@ export class API { return this.unwrapData(listTeams({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } + async getTeamNote (teamPath: string, noteId: string): Promise + async getTeamNote (teamPath: string, noteId: string, options: { unwrapData: false; etag?: undefined }): Promise & { status: 200 }> + async getTeamNote (teamPath: string, noteId: string, options: { unwrapData: false; etag: string }): Promise + async getTeamNote (teamPath: string, noteId: string, options: { unwrapData?: true; etag: string }): Promise + async getTeamNote (teamPath: string, noteId: string, options: { unwrapData?: true; etag?: undefined }): Promise + async getTeamNote (teamPath: string, noteId: string, options: RequestOptions): Promise + async getTeamNote (teamPath: string, noteId: string, options: RequestOptions = defaultOption): Promise { + const request = generatedGetTeamNote({ + client: this.generatedClient, + path: { teampath: teamPath, noteId }, + headers: options.etag ? { 'If-None-Match': options.etag } : undefined, + validateStatus: (status: number) => + (status >= 200 && status < 300) || Boolean(options.etag && status === 304), + throwOnError: true, + }) + return this.unwrapData(request, options.unwrapData, true) as unknown as Promise + } + async getTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { return this.unwrapData(listTeamNotes({ client: this.generatedClient, diff --git a/nodejs/tests/etag.spec.ts b/nodejs/tests/etag.spec.ts index 475b98a..a44c4a7 100644 --- a/nodejs/tests/etag.spec.ts +++ b/nodejs/tests/etag.spec.ts @@ -256,6 +256,79 @@ describe('Etag support', () => { expect(response.content).toBe('retried') }) }) + + describe('getTeamNote', () => { + test('preserves 200/304 ETags, raw responses, authorization and custom base URL', async () => { + const etag = 'W/"team-note-v1"' + const conditionalHeaders: Array = [] + const authorizationHeaders: Array = [] + server.use( + http.get('https://custom.hackmd.test/v1/teams/my-team/notes/team-note', ({ request }) => { + const ifNoneMatch = request.headers.get('If-None-Match') + conditionalHeaders.push(ifNoneMatch) + authorizationHeaders.push(request.headers.get('Authorization')) + if (ifNoneMatch === etag) { + return new HttpResponse(null, { status: 304, headers: { ETag: etag } }) + } + return HttpResponse.json( + { id: 'team-note', content: 'Team content' }, + { headers: { ETag: etag } } + ) + }) + ) + + const teamClient = new API('custom-token', 'https://custom.hackmd.test/v1/') + const first = await teamClient.getTeamNote('my-team', 'team-note') + expect(first).toEqual({ id: 'team-note', content: 'Team content', status: 200, etag }) + + const raw200 = await teamClient.getTeamNote('my-team', 'team-note', { unwrapData: false }) + expect(raw200.status).toBe(200) + expect(raw200.data.content).toBe('Team content') + expect(raw200.headers.etag).toBe(etag) + + const notModified = await teamClient.getTeamNote('my-team', 'team-note', { etag }) + expect(notModified).toEqual({ status: 304, etag }) + + const raw304 = await teamClient.getTeamNote('my-team', 'team-note', { etag, unwrapData: false }) + expect(raw304.status).toBe(304) + expect(raw304.headers.etag).toBe(etag) + expect(conditionalHeaders).toEqual([null, null, etag, etag]) + expect(authorizationHeaders).toEqual(Array(4).fill('Bearer custom-token')) + }) + + test('keeps legacy error wrapping for generated team requests', async () => { + server.use( + http.get('https://api.hackmd.io/v1/teams/my-team/notes/missing-note', () => + HttpResponse.json({ error: 'Note not found' }, { status: 404 }) + ) + ) + + const teamClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: undefined, + }) + await expect(teamClient.getTeamNote('my-team', 'missing-note')).rejects.toBeInstanceOf(HttpResponseError) + }) + + test('uses the existing retry interceptor', async () => { + let attempts = 0 + server.use( + http.get('https://api.hackmd.io/v1/teams/my-team/notes/retry-note', () => { + attempts++ + if (attempts === 1) return HttpResponse.json({}, { status: 503 }) + return HttpResponse.json({ id: 'retry-note', content: 'retried' }) + }) + ) + + const teamClient = new API('custom-token', undefined, { + wrapResponseErrors: true, + retryConfig: { maxRetries: 1, baseDelay: 0 }, + }) + const note = await teamClient.getTeamNote('my-team', 'retry-note') + expect(attempts).toBe(2) + expect(note.content).toBe('retried') + }) + }) describe('createNote', () => { test('should include etag property in response when creating a note', async () => { diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index ab048f6..d08b482 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -18,6 +18,20 @@ async function exploreConditionalNote (client: API): Promise return response.content } +async function exploreConditionalTeamNote (client: API): Promise { + const response = await client.getTeamNote('TEAM_PATH', 'NOTE_ID', { etag: 'W/"cached"' }) + if (response.status === 304) { + // @ts-expect-error A 304 response has no note content. + response.content + return undefined + } + const content: string = response.content + const raw = await client.getTeamNote('TEAM_PATH', 'NOTE_ID', { etag: 'W/"cached"', unwrapData: false }) + const status: 200 | 304 = raw.status + void status + return content +} + async function exploreNoteList (client: API): Promise { const notes = await client.getNoteList() return notes[0].createdAt @@ -114,6 +128,7 @@ function exploreRawNote (note: RawNote): string { void exploreClient void exploreConditionalNote +void exploreConditionalTeamNote void exploreNoteList void exploreHistory void exploreTeamNotes From 422507eca50c654f94c575740b57351b25d8d8f0 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 18:17:09 +0800 Subject: [PATCH 20/31] feat: add webhook CRUD to API client --- nodejs/README.md | 15 +++++ nodejs/src/index.ts | 93 ++++++++++++++++++++++++++++++ nodejs/src/type.ts | 10 ++++ nodejs/tests/types/playground.mts | 17 ++++++ nodejs/tests/webhooks.spec.ts | 94 +++++++++++++++++++++++++++++++ 5 files changed, 229 insertions(+) create mode 100644 nodejs/tests/webhooks.spec.ts diff --git a/nodejs/README.md b/nodejs/README.md index 8c69152..5005e1a 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -146,6 +146,21 @@ const uploadedFromNode = await client.uploadNoteImage('note-id', image, { console.log(uploadedFromNode.data.link) ``` +### Webhooks + +Manage personal or team webhooks through the same `API` client. Save the secret +returned by `createWebhook` when you create a webhook; later reads do not return it. + +```javascript +const webhook = await client.createWebhook({ + scope: { type: 'workspace' }, + url: 'https://example.com/webhook', +}) +console.log(webhook.secret) + +const teamWebhooks = await client.listTeamWebhooks('team-path') +``` + ### Generated Raw API The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 3338c56..330431c 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -7,10 +7,14 @@ import { createNote as generatedCreateNote, createTeamFolder as generatedCreateTeamFolder, createTeamNote as generatedCreateTeamNote, + createTeamWebhook as generatedCreateTeamWebhook, + createWebhook as generatedCreateWebhook, deleteFolder as generatedDeleteFolder, deleteNote as generatedDeleteNote, deleteTeamFolder as generatedDeleteTeamFolder, deleteTeamNote as generatedDeleteTeamNote, + deleteTeamWebhook as generatedDeleteTeamWebhook, + deleteWebhook as generatedDeleteWebhook, getCurrentUser, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, @@ -19,17 +23,23 @@ import { getTeamNote as generatedGetTeamNote, getTeamFolder as generatedGetTeamFolder, getTeamFolderOrder as generatedGetTeamFolderOrder, + getTeamWebhook as generatedGetTeamWebhook, + getWebhook as generatedGetWebhook, listFolders, listNotes, listTeams, listTeamFolders, listTeamNotes, + listTeamWebhooks as generatedListTeamWebhooks, + listWebhooks as generatedListWebhooks, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, updateNote as generatedUpdateNote, updateTeamFolder as generatedUpdateTeamFolder, updateTeamFolderOrder as generatedUpdateTeamFolderOrder, updateTeamNote as generatedUpdateTeamNote, + updateTeamWebhook as generatedUpdateTeamWebhook, + updateWebhook as generatedUpdateWebhook, uploadNoteImage as generatedUploadNoteImage, } from './generated/sdk.gen.js' import { @@ -61,6 +71,10 @@ import { UpdateUserFolderBody, UploadNoteImageOptions, UploadNoteImageResponse, + ApiWebhook, + CreateWebhookBody, + CreateWebhookResult, + UpdateWebhookBody, } from './type.js' import * as HackMDErrors from './error' @@ -478,6 +492,85 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } + async listWebhooks (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListWebhooks({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + } + + async getWebhook (hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetWebhook({ + client: this.generatedClient, + path: { hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async createWebhook (body: CreateWebhookBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCreateWebhook({ + client: this.generatedClient, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async updateWebhook (hookId: string, body: UpdateWebhookBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUpdateWebhook({ + client: this.generatedClient, + path: { hookId }, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async deleteWebhook (hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedDeleteWebhook({ + client: this.generatedClient, + path: { hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async listTeamWebhooks (teamPath: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeamWebhooks({ + client: this.generatedClient, + path: { teampath: teamPath }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async getTeamWebhook (teamPath: string, hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetTeamWebhook({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async createTeamWebhook (teamPath: string, body: CreateWebhookBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCreateTeamWebhook({ + client: this.generatedClient, + path: { teampath: teamPath }, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async updateTeamWebhook (teamPath: string, hookId: string, body: UpdateWebhookBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUpdateTeamWebhook({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async deleteTeamWebhook (teamPath: string, hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedDeleteTeamWebhook({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { if (!unwrap) { // For raw responses, etag is available via response.headers diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 78d1389..6d9baa3 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,7 +1,10 @@ import type { ApiFolder as GeneratedApiFolder, + ApiWebhook as GeneratedApiWebhook, + CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, + CreateWebhookResponse as GeneratedCreateWebhookResponse, FolderPath as GeneratedFolderPath, GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, @@ -10,6 +13,7 @@ import type { SingleNote as GeneratedSingleNote, Team as GeneratedTeam, UpdateNoteData as GeneratedUpdateNoteData, + UpdateWebhookData as GeneratedUpdateWebhookData, User as GeneratedUser, } from './generated/types.gen.js' @@ -123,3 +127,9 @@ export type DeleteFolderResult = void export type DeleteTeamFolderResult = void export type GetFolderOrder = ApiFolderOrder export type GetTeamFolderOrder = ApiFolderOrder + +// Webhooks (personal & team workspaces) +export type ApiWebhook = GeneratedApiWebhook +export type CreateWebhookBody = GeneratedCreateApiWebhookBody +export type CreateWebhookResult = GeneratedCreateWebhookResponse +export type UpdateWebhookBody = GeneratedUpdateWebhookData diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index d08b482..f04e6e2 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -122,6 +122,22 @@ async function exploreImageUpload (client: API): Promise { return uploaded.data.link } +async function exploreWebhooks (client: API): Promise { + const personal = await client.listWebhooks() + const team = await client.listTeamWebhooks('TEAM_PATH') + const created = await client.createWebhook({ + scope: { type: 'workspace' }, + url: 'https://example.test/webhook', + }) + const secret: string = created.secret + const updated = await client.updateTeamWebhook('TEAM_PATH', team[0].id, { active: false }) + const raw = await client.deleteWebhook(personal[0].id, { unwrapData: false }) + const status: number = raw.status + void secret + void status + return updated.id +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -140,4 +156,5 @@ void exploreNoteMutations void exploreNoteCreation void exploreProfileAndTeams void exploreImageUpload +void exploreWebhooks void exploreRawNote diff --git a/nodejs/tests/webhooks.spec.ts b/nodejs/tests/webhooks.spec.ts new file mode 100644 index 0000000..2914898 --- /dev/null +++ b/nodejs/tests/webhooks.spec.ts @@ -0,0 +1,94 @@ +import { http, HttpResponse } from 'msw' + +import { API } from '../src' +import { HttpResponseError } from '../src/error' +import { server } from './mock' + +const webhook = { + id: 'hook-1', + name: 'Updates', + url: 'https://example.test/webhook', + scope: { type: 'workspace' as const }, + configuredStatus: 'active' as const, + configuredStatusReason: null, + deliveryPolicyStatus: 'normal' as const, + deliveryPolicyReason: null, + eventDeliveryMode: 'all_supported_events' as const, + createdAt: 1700000000000, + updatedAt: 1700000000000, +} +const createBody = { scope: { type: 'workspace' as const }, url: webhook.url, name: webhook.name } +const updateBody = { name: 'Renamed' } + +const cases = [ + { name: 'listWebhooks', method: 'GET', path: '/webhooks', status: 200, body: undefined, result: [webhook], run: (api: API, raw: boolean) => api.listWebhooks({ unwrapData: !raw }) }, + { name: 'getWebhook', method: 'GET', path: '/webhooks/hook-1', status: 200, body: undefined, result: webhook, run: (api: API, raw: boolean) => api.getWebhook('hook-1', { unwrapData: !raw }) }, + { name: 'createWebhook', method: 'POST', path: '/webhooks', status: 201, body: createBody, result: { ...webhook, secret: 'one-time-secret' }, run: (api: API, raw: boolean) => api.createWebhook(createBody, { unwrapData: !raw }) }, + { name: 'updateWebhook', method: 'PATCH', path: '/webhooks/hook-1', status: 200, body: updateBody, result: { ...webhook, name: 'Renamed' }, run: (api: API, raw: boolean) => api.updateWebhook('hook-1', updateBody, { unwrapData: !raw }) }, + { name: 'deleteWebhook', method: 'DELETE', path: '/webhooks/hook-1', status: 204, body: undefined, result: undefined, run: (api: API, raw: boolean) => api.deleteWebhook('hook-1', { unwrapData: !raw }) }, + { name: 'listTeamWebhooks', method: 'GET', path: '/teams/my-team/webhooks', status: 200, body: undefined, result: [webhook], run: (api: API, raw: boolean) => api.listTeamWebhooks('my-team', { unwrapData: !raw }) }, + { name: 'getTeamWebhook', method: 'GET', path: '/teams/my-team/webhooks/hook-1', status: 200, body: undefined, result: webhook, run: (api: API, raw: boolean) => api.getTeamWebhook('my-team', 'hook-1', { unwrapData: !raw }) }, + { name: 'createTeamWebhook', method: 'POST', path: '/teams/my-team/webhooks', status: 201, body: createBody, result: { ...webhook, secret: 'one-time-secret' }, run: (api: API, raw: boolean) => api.createTeamWebhook('my-team', createBody, { unwrapData: !raw }) }, + { name: 'updateTeamWebhook', method: 'PATCH', path: '/teams/my-team/webhooks/hook-1', status: 200, body: updateBody, result: { ...webhook, name: 'Renamed' }, run: (api: API, raw: boolean) => api.updateTeamWebhook('my-team', 'hook-1', updateBody, { unwrapData: !raw }) }, + { name: 'deleteTeamWebhook', method: 'DELETE', path: '/teams/my-team/webhooks/hook-1', status: 204, body: undefined, result: undefined, run: (api: API, raw: boolean) => api.deleteTeamWebhook('my-team', 'hook-1', { unwrapData: !raw }) }, +] + +beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +test.each(cases)('$name preserves path, method, body, authentication and response shape', async ({ method, path, status, body, result, run }) => { + const requests: Array<{ method: string, body: string, authorization: string | null }> = [] + server.use(http.all(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ + method: request.method, + body: await request.text(), + authorization: request.headers.get('Authorization'), + }) + return status === 204 ? new HttpResponse(null, { status }) : HttpResponse.json(result, { status }) + })) + + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const unwrapped = await run(api, false) + const raw = await run(api, true) + if (typeof raw !== 'object' || raw === null || !('status' in raw) || !('data' in raw)) { + throw new Error('Expected a raw Axios response') + } + + expect(raw.status).toBe(status) + if (status === 204) { + expect([undefined, '']).toContain(unwrapped) + expect([undefined, '']).toContain(raw.data) + } else { + expect(unwrapped).toEqual(result) + expect(raw.data).toEqual(result) + } + expect(requests).toEqual([1, 2].map(() => ({ + method, + body: body === undefined ? '' : JSON.stringify(body), + authorization: 'Bearer test-token', + }))) +}) + +test('uses a custom base URL and keeps legacy error wrapping', async () => { + server.use(http.get('https://custom.hackmd.test/v1/webhooks/missing', () => + HttpResponse.json({ error: { code: 'WEBHOOK_NOT_FOUND', message: 'Webhook not found' } }, { status: 404 }) + )) + const api = new API('test-token', 'https://custom.hackmd.test/v1/', { + wrapResponseErrors: true, + retryConfig: undefined, + }) + await expect(api.getWebhook('missing')).rejects.toBeInstanceOf(HttpResponseError) +}) + +test.each(['createWebhook', 'createTeamWebhook'] as const)('%s keeps validation errors', async name => { + const path = name === 'createWebhook' ? '/webhooks' : '/teams/my-team/webhooks' + server.use(http.post(`https://api.hackmd.io/v1${path}`, () => + HttpResponse.json({ message: 'Validation Failed', details: {} }, { status: 422 }) + )) + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const request = name === 'createWebhook' + ? api.createWebhook(createBody) + : api.createTeamWebhook('my-team', createBody) + await expect(request).rejects.toBeInstanceOf(HttpResponseError) +}) From a5439702c966251d3815739ab35c8c3223f1bb2a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 18:31:18 +0800 Subject: [PATCH 21/31] feat: add webhook delivery operations to API client --- nodejs/README.md | 4 ++ nodejs/src/index.ts | 79 +++++++++++++++++++++++++++++++ nodejs/src/type.ts | 4 ++ nodejs/tests/types/playground.mts | 14 ++++++ nodejs/tests/webhooks.spec.ts | 60 +++++++++++++++++++++++ 5 files changed, 161 insertions(+) diff --git a/nodejs/README.md b/nodejs/README.md index 5005e1a..7cafb12 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -161,6 +161,10 @@ console.log(webhook.secret) const teamWebhooks = await client.listTeamWebhooks('team-path') ``` +`listWebhookDeliveries('hook-id', { page: 1, limit: 20 })` returns delivery data +and pagination metadata. `exportWebhookDeliveries('hook-id')` returns +newline-delimited JSON as a string; parse the lines yourself if needed. + ### Generated Raw API The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 330431c..739be66 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -15,6 +15,8 @@ import { deleteTeamNote as generatedDeleteTeamNote, deleteTeamWebhook as generatedDeleteTeamWebhook, deleteWebhook as generatedDeleteWebhook, + exportTeamWebhookDeliveries as generatedExportTeamWebhookDeliveries, + exportWebhookDeliveries as generatedExportWebhookDeliveries, getCurrentUser, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, @@ -24,14 +26,20 @@ import { getTeamFolder as generatedGetTeamFolder, getTeamFolderOrder as generatedGetTeamFolderOrder, getTeamWebhook as generatedGetTeamWebhook, + getTeamWebhookDelivery as generatedGetTeamWebhookDelivery, getWebhook as generatedGetWebhook, + getWebhookDelivery as generatedGetWebhookDelivery, listFolders, listNotes, listTeams, listTeamFolders, listTeamNotes, listTeamWebhooks as generatedListTeamWebhooks, + listTeamWebhookDeliveries as generatedListTeamWebhookDeliveries, listWebhooks as generatedListWebhooks, + listWebhookDeliveries as generatedListWebhookDeliveries, + pingTeamWebhook as generatedPingTeamWebhook, + pingWebhook as generatedPingWebhook, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, updateNote as generatedUpdateNote, @@ -75,6 +83,8 @@ import { CreateWebhookBody, CreateWebhookResult, UpdateWebhookBody, + ApiWebhookDelivery, + WebhookDeliveryPage, } from './type.js' import * as HackMDErrors from './error' @@ -84,6 +94,7 @@ export type RequestOptions = { } export type GetHistoryOptions = RequestOptions & { limit?: number } +export type WebhookDeliveryListOptions = RequestOptions & { page?: number; limit?: number } const defaultOption: RequestOptions = { unwrapData: true, @@ -571,6 +582,74 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } + async pingWebhook (hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedPingWebhook({ + client: this.generatedClient, + path: { hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async listWebhookDeliveries (hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListWebhookDeliveries({ + client: this.generatedClient, + path: { hookId }, + query: { page: options.page, limit: options.limit }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async getWebhookDelivery (hookId: string, deliveryId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetWebhookDelivery({ + client: this.generatedClient, + path: { hookId, deliveryId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async exportWebhookDeliveries (hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedExportWebhookDeliveries({ + client: this.generatedClient, + path: { hookId }, + responseType: 'text', + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async pingTeamWebhook (teamPath: string, hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedPingTeamWebhook({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async listTeamWebhookDeliveries (teamPath: string, hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeamWebhookDeliveries({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + query: { page: options.page, limit: options.limit }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async getTeamWebhookDelivery (teamPath: string, hookId: string, deliveryId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetTeamWebhookDelivery({ + client: this.generatedClient, + path: { teampath: teamPath, hookId, deliveryId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async exportTeamWebhookDeliveries (teamPath: string, hookId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedExportTeamWebhookDeliveries({ + client: this.generatedClient, + path: { teampath: teamPath, hookId }, + responseType: 'text', + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { if (!unwrap) { // For raw responses, etag is available via response.headers diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 6d9baa3..e67e145 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,6 +1,7 @@ import type { ApiFolder as GeneratedApiFolder, ApiWebhook as GeneratedApiWebhook, + ApiWebhookDelivery as GeneratedApiWebhookDelivery, CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, @@ -9,6 +10,7 @@ import type { GetUserHistory as GeneratedUserHistory, NoteType as GeneratedNote, NoteImageUploadResponse as GeneratedNoteImageUploadResponse, + PaginatedResponseApiWebhookDelivery as GeneratedWebhookDeliveryPage, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, Team as GeneratedTeam, @@ -133,3 +135,5 @@ export type ApiWebhook = GeneratedApiWebhook export type CreateWebhookBody = GeneratedCreateApiWebhookBody export type CreateWebhookResult = GeneratedCreateWebhookResponse export type UpdateWebhookBody = GeneratedUpdateWebhookData +export type ApiWebhookDelivery = GeneratedApiWebhookDelivery +export type WebhookDeliveryPage = GeneratedWebhookDeliveryPage diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index f04e6e2..0b5ea0c 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -138,6 +138,19 @@ async function exploreWebhooks (client: API): Promise { return updated.id } +async function exploreWebhookDeliveries (client: API): Promise { + const page = await client.listWebhookDeliveries('HOOK_ID', { page: 2, limit: 5 }) + const delivery = await client.getTeamWebhookDelivery('TEAM_PATH', 'HOOK_ID', page.data[0].id) + const jsonl: string = await client.exportWebhookDeliveries('HOOK_ID') + const raw = await client.exportTeamWebhookDeliveries('TEAM_PATH', 'HOOK_ID', { unwrapData: false }) + const rawJsonl: string = raw.data + const pinged: void = await client.pingWebhook('HOOK_ID') + void jsonl + void rawJsonl + void pinged + return delivery.id +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -157,4 +170,5 @@ void exploreNoteCreation void exploreProfileAndTeams void exploreImageUpload void exploreWebhooks +void exploreWebhookDeliveries void exploreRawNote diff --git a/nodejs/tests/webhooks.spec.ts b/nodejs/tests/webhooks.spec.ts index 2914898..c732056 100644 --- a/nodejs/tests/webhooks.spec.ts +++ b/nodejs/tests/webhooks.spec.ts @@ -92,3 +92,63 @@ test.each(['createWebhook', 'createTeamWebhook'] as const)('%s keeps validation : api.createTeamWebhook('my-team', createBody) await expect(request).rejects.toBeInstanceOf(HttpResponseError) }) + +const delivery = { + id: 'delivery-1', eventType: 'note.created', attemptKind: 'event', status: 'success', + statusCategory: 'delivered', httpStatus: 200, errorCategory: null, errorMessage: null, + latencyMs: 42, createdAt: 1700000000000, deliveredAt: 1700000000042, +} +const deliveryPage = { data: [delivery], meta: { page: 2, limit: 5, total: 1 } } +const ndjson = `${JSON.stringify(delivery)}\n` + +const deliveryCases = [ + { name: 'pingWebhook', method: 'POST', path: '/webhooks/hook-1/ping', status: 204, query: '', result: undefined, run: (api: API, raw: boolean) => api.pingWebhook('hook-1', { unwrapData: !raw }) }, + { name: 'listWebhookDeliveries', method: 'GET', path: '/webhooks/hook-1/deliveries', status: 200, query: '?page=2&limit=5', result: deliveryPage, run: (api: API, raw: boolean) => api.listWebhookDeliveries('hook-1', { page: 2, limit: 5, unwrapData: !raw }) }, + { name: 'getWebhookDelivery', method: 'GET', path: '/webhooks/hook-1/deliveries/delivery-1', status: 200, query: '', result: delivery, run: (api: API, raw: boolean) => api.getWebhookDelivery('hook-1', 'delivery-1', { unwrapData: !raw }) }, + { name: 'exportWebhookDeliveries', method: 'GET', path: '/webhooks/hook-1/deliveries/export', status: 200, query: '', result: ndjson, run: (api: API, raw: boolean) => api.exportWebhookDeliveries('hook-1', { unwrapData: !raw }) }, + { name: 'pingTeamWebhook', method: 'POST', path: '/teams/my-team/webhooks/hook-1/ping', status: 204, query: '', result: undefined, run: (api: API, raw: boolean) => api.pingTeamWebhook('my-team', 'hook-1', { unwrapData: !raw }) }, + { name: 'listTeamWebhookDeliveries', method: 'GET', path: '/teams/my-team/webhooks/hook-1/deliveries', status: 200, query: '?page=2&limit=5', result: deliveryPage, run: (api: API, raw: boolean) => api.listTeamWebhookDeliveries('my-team', 'hook-1', { page: 2, limit: 5, unwrapData: !raw }) }, + { name: 'getTeamWebhookDelivery', method: 'GET', path: '/teams/my-team/webhooks/hook-1/deliveries/delivery-1', status: 200, query: '', result: delivery, run: (api: API, raw: boolean) => api.getTeamWebhookDelivery('my-team', 'hook-1', 'delivery-1', { unwrapData: !raw }) }, + { name: 'exportTeamWebhookDeliveries', method: 'GET', path: '/teams/my-team/webhooks/hook-1/deliveries/export', status: 200, query: '', result: ndjson, run: (api: API, raw: boolean) => api.exportTeamWebhookDeliveries('my-team', 'hook-1', { unwrapData: !raw }) }, +] + +test.each(deliveryCases)('$name preserves path, query, status and response shape', async ({ method, path, status, query, result, run }) => { + const requests: Array<{ method: string, url: string, authorization: string | null }> = [] + server.use(http.all(`https://api.hackmd.io/v1${path}`, ({ request }) => { + requests.push({ method: request.method, url: request.url, authorization: request.headers.get('Authorization') }) + if (status === 204) return new HttpResponse(null, { status }) + if (typeof result === 'string') { + return HttpResponse.text(result, { status, headers: { 'Content-Type': 'application/x-ndjson' } }) + } + return HttpResponse.json(result, { status }) + })) + + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const unwrapped = await run(api, false) + const raw = await run(api, true) + if (typeof raw !== 'object' || raw === null || !('status' in raw) || !('data' in raw)) { + throw new Error('Expected a raw Axios response') + } + + expect(raw.status).toBe(status) + if (status === 204) { + expect([undefined, '']).toContain(unwrapped) + expect([undefined, '']).toContain(raw.data) + } else { + expect(unwrapped).toEqual(result) + expect(raw.data).toEqual(result) + } + expect(requests).toEqual([1, 2].map(() => ({ + method, + url: `https://api.hackmd.io/v1${path}${query}`, + authorization: 'Bearer test-token', + }))) +}) + +test('ping errors keep legacy wrapping', async () => { + server.use(http.post('https://api.hackmd.io/v1/webhooks/hook-1/ping', () => + HttpResponse.json({ error: { code: 'RATE_LIMITED', message: 'Too many ping requests' } }, { status: 429 }) + )) + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + await expect(api.pingWebhook('hook-1')).rejects.toMatchObject({ code: 429 }) +}) From 6d1962aded34f31bd9087a9d934347c0c6f306d0 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 23:07:10 +0800 Subject: [PATCH 22/31] feat: add trash operations to API client --- nodejs/README.md | 7 +++ nodejs/spec/hackmd-openapi.json | 4 +- nodejs/src/generated/index.ts | 4 +- nodejs/src/generated/sdk.gen.ts | 4 +- nodejs/src/generated/types.gen.ts | 6 +-- nodejs/src/index.ts | 35 +++++++++++++ nodejs/src/type.ts | 8 +++ nodejs/tests/generated.spec.ts | 2 +- nodejs/tests/trash.spec.ts | 86 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 15 ++++++ 10 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 nodejs/tests/trash.spec.ts diff --git a/nodejs/README.md b/nodejs/README.md index 7cafb12..3e6b3ae 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -165,6 +165,13 @@ const teamWebhooks = await client.listTeamWebhooks('team-path') and pagination metadata. `exportWebhookDeliveries('hook-id')` returns newline-delimited JSON as a string; parse the lines yourself if needed. +### Trash + +Use `listTrash()` or `listTeamTrash('team-path')` to find deleted notes. +`restoreNote('note-id')` restores one note; `batchRestore({ noteIds })` reports +success or failure for each note. Pass `{ unwrapData: false }` to inspect the +batch HTTP status (200 or 207). + ### Generated Raw API The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, diff --git a/nodejs/spec/hackmd-openapi.json b/nodejs/spec/hackmd-openapi.json index f866513..e498ae7 100644 --- a/nodejs/spec/hackmd-openapi.json +++ b/nodejs/spec/hackmd-openapi.json @@ -4452,7 +4452,7 @@ }, "/trash": { "get": { - "operationId": "ListPersonalTrash", + "operationId": "ListTrash", "responses": { "200": { "description": "Ok", @@ -6718,4 +6718,4 @@ "description": "Operations for managing webhooks in a team workspace." } ] -} \ No newline at end of file +} diff --git a/nodejs/src/generated/index.ts b/nodejs/src/generated/index.ts index eae4884..5db128d 100644 --- a/nodejs/src/generated/index.ts +++ b/nodejs/src/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { batchRestore, compareVersions, createFolder, createNote, createTeamFolder, createTeamNote, createTeamWebhook, createVersion, createWebhook, deleteFolder, deleteNote, deleteTeamFolder, deleteTeamNote, deleteTeamWebhook, deleteWebhook, exportTeamWebhookDeliveries, exportWebhookDeliveries, getCurrentUser, getFolder, getFolderOrder, getHistory, getNote, getNoteComment, getTeamFolder, getTeamFolderOrder, getTeamNote, getTeamWebhook, getTeamWebhookDelivery, getVersion, getWebhook, getWebhookDelivery, listFolders, listNoteComments, listNotes, listPersonalTrash, listTeamFolders, listTeamNotes, listTeams, listTeamTrash, listTeamWebhookDeliveries, listTeamWebhooks, listVersions, listWebhookDeliveries, listWebhooks, type Options, pingTeamWebhook, pingWebhook, resolveNoteComment, restoreNote, unresolveNoteComment, updateFolder, updateFolderOrder, updateNote, updateTeamFolder, updateTeamFolderOrder, updateTeamNote, updateTeamWebhook, updateVersion, updateWebhook, uploadNoteImage } from './sdk.gen.js'; -export type { ApiBadRequestResponse, ApiComment, ApiCommentActor, ApiCommentAnchor, ApiCommentDeletedUserActor, ApiCommentDetail, ApiCommentDetailReply, ApiCommentError, ApiCommentGuestActor, ApiCommentHead, ApiCommentId, ApiCommentLimit, ApiCommentListReply, ApiCommentPage, ApiCommentPagination, ApiCommentReactionShortcode, ApiCommentReactionSummary, ApiCommentResolutionCascade, ApiCommentResolutionConflictError, ApiCommentResolutionResponse, ApiCommentState, ApiCommentStateActor, ApiCommentStateDeletedUserActor, ApiCommentStateReason, ApiCommentStateUserActor, ApiCommentStatus, ApiCommentTeamActor, ApiCommentThreadId, ApiCommentThreadState, ApiCommentUserActor, ApiError, ApiErrorResponse, ApiFolder, ApiFolderOrder, ApiNoteFeatures, ApiRequestErrorResponse, ApiTrashNote, ApiValidationError, ApiValidationFieldError, ApiWebhook, ApiWebhookDelivery, ApiWebhookLimit, ApiWebhookPage, ApiWebhookScope, BatchOperationError, BatchRestoreData, BatchRestoreError, BatchRestoreErrors, BatchRestoreResponse, BatchRestoreResponses, BatchRestoreTrashBody, ClientOptions, CommentPermissionType, CompareNoteVersions, CompareVersionsData, CompareVersionsError, CompareVersionsErrors, CompareVersionsResponse, CompareVersionsResponses, CreateApiWebhookBody, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteMultiStatusResponse, CreateNoteResponse, CreateNoteResponses, CreateNoteVersionBody, CreateNoteVersionConflictError, CreateTeamFolderBody, CreateTeamFolderData, CreateTeamFolderError, CreateTeamFolderErrors, CreateTeamFolderResponse, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteError, CreateTeamNoteErrors, CreateTeamNoteResponse, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookError, CreateTeamWebhookErrors, CreateTeamWebhookResponse, CreateTeamWebhookResponses, CreateUserFolderBody, CreateVersionData, CreateVersionError, CreateVersionErrors, CreateVersionResponse, CreateVersionResponses, CreateWebhookData, CreateWebhookData2, CreateWebhookError, CreateWebhookErrors, CreateWebhookResponse, CreateWebhookResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderError, DeleteTeamFolderErrors, DeleteTeamFolderResponse, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteError, DeleteTeamNoteErrors, DeleteTeamNoteResponse, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookError, DeleteTeamWebhookErrors, DeleteTeamWebhookResponse, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesError, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponse, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesError, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponse, ExportWebhookDeliveriesResponses, FolderColorHex, FolderIconCodepoint, FolderName, FolderPath, GetCurrentUserData, GetCurrentUserResponse, GetCurrentUserResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponse, GetFolderOrderResponses, GetFolderResponse, GetFolderResponses, GetHistoryData, GetHistoryResponse, GetHistoryResponses, GetNoteCommentData, GetNoteCommentError, GetNoteCommentErrors, GetNoteCommentResponse, GetNoteCommentResponses, GetNoteComments, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetNoteVersions, GetTeamFolderData, GetTeamFolderError, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponse, GetTeamFolderOrderResponses, GetTeamFolderResponse, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteError, GetTeamNoteErrors, GetTeamNoteResponse, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryError, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponse, GetTeamWebhookDeliveryResponses, GetTeamWebhookError, GetTeamWebhookErrors, GetTeamWebhookResponse, GetTeamWebhookResponses, GetUserHistory, GetVersionData, GetVersionError, GetVersionErrors, GetVersionResponse, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryError, GetWebhookDeliveryErrors, GetWebhookDeliveryResponse, GetWebhookDeliveryResponses, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, ListFoldersData, ListFoldersResponse, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsError, ListNoteCommentsErrors, ListNoteCommentsResponse, ListNoteCommentsResponses, ListNotesData, ListNotesResponse, ListNotesResponses, ListPersonalTrashData, ListPersonalTrashResponse, ListPersonalTrashResponses, ListTeamFoldersData, ListTeamFoldersResponse, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponse, ListTeamNotesResponses, ListTeamsData, ListTeamsResponse, ListTeamsResponses, ListTeamTrashData, ListTeamTrashError, ListTeamTrashErrors, ListTeamTrashResponse, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesError, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponse, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponse, ListTeamWebhooksResponses, ListVersionsData, ListVersionsError, ListVersionsErrors, ListVersionsResponse, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesError, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponse, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponse, ListWebhooksResponses, NoContentChangeError, Note, NoteImageUploadResponse, NotePermissionRole, NotePublishType, NoteType, NoteVersion, NoteVersionMetadata, NoteVersionUser, PaginatedQuery, PaginatedResponseApiWebhookDelivery, PingTeamWebhookData, PingTeamWebhookError, PingTeamWebhookErrors, PingTeamWebhookResponse, PingTeamWebhookResponses, PingWebhookData, PingWebhookError, PingWebhookErrors, PingWebhookResponse, PingWebhookResponses, RecordStringApiValidationFieldError, ResolveNoteCommentData, ResolveNoteCommentError, ResolveNoteCommentErrors, ResolveNoteCommentResponse, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteError, RestoreNoteErrors, RestoreNoteResponse, RestoreNoteResponses, SimpleUserProfile, SingleNote, SuggestEditPermissionType, Team, TeamVisibilityType, TrashBatchOperationResponse, UnresolveNoteCommentData, UnresolveNoteCommentError, UnresolveNoteCommentErrors, UnresolveNoteCommentResponse, UnresolveNoteCommentResponses, UpdateApiWebhookBody, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderOrderBody, UpdateFolderOrderData, UpdateFolderOrderError, UpdateFolderOrderErrors, UpdateFolderOrderResponse, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponses, UpdateNoteVersionBody, UpdateTeamFolderBody, UpdateTeamFolderData, UpdateTeamFolderError, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderError, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponse, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteError, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookError, UpdateTeamWebhookErrors, UpdateTeamWebhookResponse, UpdateTeamWebhookResponses, UpdateUserFolderBody, UpdateVersionData, UpdateVersionError, UpdateVersionErrors, UpdateVersionResponse, UpdateVersionResponses, UpdateWebhookData, UpdateWebhookData2, UpdateWebhookError, UpdateWebhookErrors, UpdateWebhookResponse, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageError, UploadNoteImageErrors, UploadNoteImageResponse, UploadNoteImageResponses, User, VersionAlreadyNamedError, VersionNotNamedError, WebhookApiErrorResponse, WebhookDeliveryStatusCategory } from './types.gen.js'; +export { batchRestore, compareVersions, createFolder, createNote, createTeamFolder, createTeamNote, createTeamWebhook, createVersion, createWebhook, deleteFolder, deleteNote, deleteTeamFolder, deleteTeamNote, deleteTeamWebhook, deleteWebhook, exportTeamWebhookDeliveries, exportWebhookDeliveries, getCurrentUser, getFolder, getFolderOrder, getHistory, getNote, getNoteComment, getTeamFolder, getTeamFolderOrder, getTeamNote, getTeamWebhook, getTeamWebhookDelivery, getVersion, getWebhook, getWebhookDelivery, listFolders, listNoteComments, listNotes, listTeamFolders, listTeamNotes, listTeams, listTeamTrash, listTeamWebhookDeliveries, listTeamWebhooks, listTrash, listVersions, listWebhookDeliveries, listWebhooks, type Options, pingTeamWebhook, pingWebhook, resolveNoteComment, restoreNote, unresolveNoteComment, updateFolder, updateFolderOrder, updateNote, updateTeamFolder, updateTeamFolderOrder, updateTeamNote, updateTeamWebhook, updateVersion, updateWebhook, uploadNoteImage } from './sdk.gen.js'; +export type { ApiBadRequestResponse, ApiComment, ApiCommentActor, ApiCommentAnchor, ApiCommentDeletedUserActor, ApiCommentDetail, ApiCommentDetailReply, ApiCommentError, ApiCommentGuestActor, ApiCommentHead, ApiCommentId, ApiCommentLimit, ApiCommentListReply, ApiCommentPage, ApiCommentPagination, ApiCommentReactionShortcode, ApiCommentReactionSummary, ApiCommentResolutionCascade, ApiCommentResolutionConflictError, ApiCommentResolutionResponse, ApiCommentState, ApiCommentStateActor, ApiCommentStateDeletedUserActor, ApiCommentStateReason, ApiCommentStateUserActor, ApiCommentStatus, ApiCommentTeamActor, ApiCommentThreadId, ApiCommentThreadState, ApiCommentUserActor, ApiError, ApiErrorResponse, ApiFolder, ApiFolderOrder, ApiNoteFeatures, ApiRequestErrorResponse, ApiTrashNote, ApiValidationError, ApiValidationFieldError, ApiWebhook, ApiWebhookDelivery, ApiWebhookLimit, ApiWebhookPage, ApiWebhookScope, BatchOperationError, BatchRestoreData, BatchRestoreError, BatchRestoreErrors, BatchRestoreResponse, BatchRestoreResponses, BatchRestoreTrashBody, ClientOptions, CommentPermissionType, CompareNoteVersions, CompareVersionsData, CompareVersionsError, CompareVersionsErrors, CompareVersionsResponse, CompareVersionsResponses, CreateApiWebhookBody, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteMultiStatusResponse, CreateNoteResponse, CreateNoteResponses, CreateNoteVersionBody, CreateNoteVersionConflictError, CreateTeamFolderBody, CreateTeamFolderData, CreateTeamFolderError, CreateTeamFolderErrors, CreateTeamFolderResponse, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteError, CreateTeamNoteErrors, CreateTeamNoteResponse, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookError, CreateTeamWebhookErrors, CreateTeamWebhookResponse, CreateTeamWebhookResponses, CreateUserFolderBody, CreateVersionData, CreateVersionError, CreateVersionErrors, CreateVersionResponse, CreateVersionResponses, CreateWebhookData, CreateWebhookData2, CreateWebhookError, CreateWebhookErrors, CreateWebhookResponse, CreateWebhookResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderError, DeleteTeamFolderErrors, DeleteTeamFolderResponse, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteError, DeleteTeamNoteErrors, DeleteTeamNoteResponse, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookError, DeleteTeamWebhookErrors, DeleteTeamWebhookResponse, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesError, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponse, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesError, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponse, ExportWebhookDeliveriesResponses, FolderColorHex, FolderIconCodepoint, FolderName, FolderPath, GetCurrentUserData, GetCurrentUserResponse, GetCurrentUserResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponse, GetFolderOrderResponses, GetFolderResponse, GetFolderResponses, GetHistoryData, GetHistoryResponse, GetHistoryResponses, GetNoteCommentData, GetNoteCommentError, GetNoteCommentErrors, GetNoteCommentResponse, GetNoteCommentResponses, GetNoteComments, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetNoteVersions, GetTeamFolderData, GetTeamFolderError, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponse, GetTeamFolderOrderResponses, GetTeamFolderResponse, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteError, GetTeamNoteErrors, GetTeamNoteResponse, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryError, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponse, GetTeamWebhookDeliveryResponses, GetTeamWebhookError, GetTeamWebhookErrors, GetTeamWebhookResponse, GetTeamWebhookResponses, GetUserHistory, GetVersionData, GetVersionError, GetVersionErrors, GetVersionResponse, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryError, GetWebhookDeliveryErrors, GetWebhookDeliveryResponse, GetWebhookDeliveryResponses, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, ListFoldersData, ListFoldersResponse, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsError, ListNoteCommentsErrors, ListNoteCommentsResponse, ListNoteCommentsResponses, ListNotesData, ListNotesResponse, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponse, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponse, ListTeamNotesResponses, ListTeamsData, ListTeamsResponse, ListTeamsResponses, ListTeamTrashData, ListTeamTrashError, ListTeamTrashErrors, ListTeamTrashResponse, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesError, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponse, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponse, ListTeamWebhooksResponses, ListTrashData, ListTrashResponse, ListTrashResponses, ListVersionsData, ListVersionsError, ListVersionsErrors, ListVersionsResponse, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesError, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponse, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponse, ListWebhooksResponses, NoContentChangeError, Note, NoteImageUploadResponse, NotePermissionRole, NotePublishType, NoteType, NoteVersion, NoteVersionMetadata, NoteVersionUser, PaginatedQuery, PaginatedResponseApiWebhookDelivery, PingTeamWebhookData, PingTeamWebhookError, PingTeamWebhookErrors, PingTeamWebhookResponse, PingTeamWebhookResponses, PingWebhookData, PingWebhookError, PingWebhookErrors, PingWebhookResponse, PingWebhookResponses, RecordStringApiValidationFieldError, ResolveNoteCommentData, ResolveNoteCommentError, ResolveNoteCommentErrors, ResolveNoteCommentResponse, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteError, RestoreNoteErrors, RestoreNoteResponse, RestoreNoteResponses, SimpleUserProfile, SingleNote, SuggestEditPermissionType, Team, TeamVisibilityType, TrashBatchOperationResponse, UnresolveNoteCommentData, UnresolveNoteCommentError, UnresolveNoteCommentErrors, UnresolveNoteCommentResponse, UnresolveNoteCommentResponses, UpdateApiWebhookBody, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderOrderBody, UpdateFolderOrderData, UpdateFolderOrderError, UpdateFolderOrderErrors, UpdateFolderOrderResponse, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponses, UpdateNoteVersionBody, UpdateTeamFolderBody, UpdateTeamFolderData, UpdateTeamFolderError, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderError, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponse, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteError, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookError, UpdateTeamWebhookErrors, UpdateTeamWebhookResponse, UpdateTeamWebhookResponses, UpdateUserFolderBody, UpdateVersionData, UpdateVersionError, UpdateVersionErrors, UpdateVersionResponse, UpdateVersionResponses, UpdateWebhookData, UpdateWebhookData2, UpdateWebhookError, UpdateWebhookErrors, UpdateWebhookResponse, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageError, UploadNoteImageErrors, UploadNoteImageResponse, UploadNoteImageResponses, User, VersionAlreadyNamedError, VersionNotNamedError, WebhookApiErrorResponse, WebhookDeliveryStatusCategory } from './types.gen.js'; diff --git a/nodejs/src/generated/sdk.gen.ts b/nodejs/src/generated/sdk.gen.ts index e26d5dd..36f1694 100644 --- a/nodejs/src/generated/sdk.gen.ts +++ b/nodejs/src/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import { client } from './client.gen.js'; import { type Client, type ClientMeta, formDataBodySerializer, type Options as Options2, type RequestResult, type TDataShape } from './client/index.js'; -import type { BatchRestoreData, BatchRestoreErrors, BatchRestoreResponses, CompareVersionsData, CompareVersionsErrors, CompareVersionsResponses, CreateFolderData, CreateFolderErrors, CreateFolderResponses, CreateNoteData, CreateNoteErrors, CreateNoteResponses, CreateTeamFolderData, CreateTeamFolderErrors, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteErrors, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookErrors, CreateTeamWebhookResponses, CreateVersionData, CreateVersionErrors, CreateVersionResponses, CreateWebhookData2, CreateWebhookErrors, CreateWebhookResponses, DeleteFolderData, DeleteFolderErrors, DeleteFolderResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderErrors, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteErrors, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookErrors, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponses, GetCurrentUserData, GetCurrentUserResponses, GetFolderData, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponses, GetFolderResponses, GetHistoryData, GetHistoryResponses, GetNoteCommentData, GetNoteCommentErrors, GetNoteCommentResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetTeamFolderData, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponses, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteErrors, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponses, GetTeamWebhookErrors, GetTeamWebhookResponses, GetVersionData, GetVersionErrors, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryErrors, GetWebhookDeliveryResponses, GetWebhookErrors, GetWebhookResponses, ListFoldersData, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsErrors, ListNoteCommentsResponses, ListNotesData, ListNotesResponses, ListPersonalTrashData, ListPersonalTrashResponses, ListTeamFoldersData, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponses, ListTeamsData, ListTeamsResponses, ListTeamTrashData, ListTeamTrashErrors, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponses, ListVersionsData, ListVersionsErrors, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponses, PingTeamWebhookData, PingTeamWebhookErrors, PingTeamWebhookResponses, PingWebhookData, PingWebhookErrors, PingWebhookResponses, ResolveNoteCommentData, ResolveNoteCommentErrors, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteErrors, RestoreNoteResponses, UnresolveNoteCommentData, UnresolveNoteCommentErrors, UnresolveNoteCommentResponses, UpdateFolderData, UpdateFolderErrors, UpdateFolderOrderData, UpdateFolderOrderErrors, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteErrors, UpdateNoteResponses, UpdateTeamFolderData, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookErrors, UpdateTeamWebhookResponses, UpdateVersionData, UpdateVersionErrors, UpdateVersionResponses, UpdateWebhookData2, UpdateWebhookErrors, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageErrors, UploadNoteImageResponses } from './types.gen.js'; +import type { BatchRestoreData, BatchRestoreErrors, BatchRestoreResponses, CompareVersionsData, CompareVersionsErrors, CompareVersionsResponses, CreateFolderData, CreateFolderErrors, CreateFolderResponses, CreateNoteData, CreateNoteErrors, CreateNoteResponses, CreateTeamFolderData, CreateTeamFolderErrors, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteErrors, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookErrors, CreateTeamWebhookResponses, CreateVersionData, CreateVersionErrors, CreateVersionResponses, CreateWebhookData2, CreateWebhookErrors, CreateWebhookResponses, DeleteFolderData, DeleteFolderErrors, DeleteFolderResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderErrors, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteErrors, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookErrors, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponses, GetCurrentUserData, GetCurrentUserResponses, GetFolderData, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponses, GetFolderResponses, GetHistoryData, GetHistoryResponses, GetNoteCommentData, GetNoteCommentErrors, GetNoteCommentResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetTeamFolderData, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponses, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteErrors, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponses, GetTeamWebhookErrors, GetTeamWebhookResponses, GetVersionData, GetVersionErrors, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryErrors, GetWebhookDeliveryResponses, GetWebhookErrors, GetWebhookResponses, ListFoldersData, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsErrors, ListNoteCommentsResponses, ListNotesData, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponses, ListTeamsData, ListTeamsResponses, ListTeamTrashData, ListTeamTrashErrors, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponses, ListTrashData, ListTrashResponses, ListVersionsData, ListVersionsErrors, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponses, PingTeamWebhookData, PingTeamWebhookErrors, PingTeamWebhookResponses, PingWebhookData, PingWebhookErrors, PingWebhookResponses, ResolveNoteCommentData, ResolveNoteCommentErrors, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteErrors, RestoreNoteResponses, UnresolveNoteCommentData, UnresolveNoteCommentErrors, UnresolveNoteCommentResponses, UpdateFolderData, UpdateFolderErrors, UpdateFolderOrderData, UpdateFolderOrderErrors, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteErrors, UpdateNoteResponses, UpdateTeamFolderData, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookErrors, UpdateTeamWebhookResponses, UpdateVersionData, UpdateVersionErrors, UpdateVersionResponses, UpdateWebhookData2, UpdateWebhookErrors, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageErrors, UploadNoteImageResponses } from './types.gen.js'; export type Options = Options2 & { /** @@ -353,7 +353,7 @@ export const updateFolder = (options: Opti /** * List trashed notes in your personal workspace (same data as the internal trash API). */ -export const listPersonalTrash = (options?: Options): RequestResult => (options?.client ?? client).get({ +export const listTrash = (options?: Options): RequestResult => (options?.client ?? client).get({ responseType: 'json', security: [{ scheme: 'bearer', type: 'http' }], url: '/trash', diff --git a/nodejs/src/generated/types.gen.ts b/nodejs/src/generated/types.gen.ts index 4ca67f6..1158e6d 100644 --- a/nodejs/src/generated/types.gen.ts +++ b/nodejs/src/generated/types.gen.ts @@ -1534,21 +1534,21 @@ export type UpdateFolderResponses = { 202: unknown; }; -export type ListPersonalTrashData = { +export type ListTrashData = { body?: never; path?: never; query?: never; url: '/trash'; }; -export type ListPersonalTrashResponses = { +export type ListTrashResponses = { /** * Ok */ 200: Array; }; -export type ListPersonalTrashResponse = ListPersonalTrashResponses[keyof ListPersonalTrashResponses]; +export type ListTrashResponse = ListTrashResponses[keyof ListTrashResponses]; export type BatchRestoreData = { body: BatchRestoreTrashBody; diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 739be66..d7c6699 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -3,6 +3,7 @@ import axios, { AxiosInstance, AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios' import { createClient, type Client } from './generated/client/index.js' import { + batchRestore as generatedBatchRestore, createFolder as generatedCreateFolder, createNote as generatedCreateNote, createTeamFolder as generatedCreateTeamFolder, @@ -31,15 +32,18 @@ import { getWebhookDelivery as generatedGetWebhookDelivery, listFolders, listNotes, + listTrash as generatedListTrash, listTeams, listTeamFolders, listTeamNotes, + listTeamTrash as generatedListTeamTrash, listTeamWebhooks as generatedListTeamWebhooks, listTeamWebhookDeliveries as generatedListTeamWebhookDeliveries, listWebhooks as generatedListWebhooks, listWebhookDeliveries as generatedListWebhookDeliveries, pingTeamWebhook as generatedPingTeamWebhook, pingWebhook as generatedPingWebhook, + restoreNote as generatedRestoreNote, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, updateNote as generatedUpdateNote, @@ -85,6 +89,9 @@ import { UpdateWebhookBody, ApiWebhookDelivery, WebhookDeliveryPage, + ApiTrashNote, + BatchRestoreTrashBody, + TrashBatchOperationResponse, } from './type.js' import * as HackMDErrors from './error' @@ -650,6 +657,34 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } + async listTrash (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTrash({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + } + + async batchRestore (body: BatchRestoreTrashBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedBatchRestore({ + client: this.generatedClient, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async restoreNote (noteId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedRestoreNote({ + client: this.generatedClient, + path: { noteId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async listTeamTrash (teamPath: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeamTrash({ + client: this.generatedClient, + path: { teampath: teamPath }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { if (!unwrap) { // For raw responses, etag is available via response.headers diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index e67e145..317600b 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -2,6 +2,8 @@ import type { ApiFolder as GeneratedApiFolder, ApiWebhook as GeneratedApiWebhook, ApiWebhookDelivery as GeneratedApiWebhookDelivery, + ApiTrashNote as GeneratedApiTrashNote, + BatchRestoreTrashBody as GeneratedBatchRestoreTrashBody, CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, @@ -14,6 +16,7 @@ import type { SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, Team as GeneratedTeam, + TrashBatchOperationResponse as GeneratedTrashBatchOperationResponse, UpdateNoteData as GeneratedUpdateNoteData, UpdateWebhookData as GeneratedUpdateWebhookData, User as GeneratedUser, @@ -137,3 +140,8 @@ export type CreateWebhookResult = GeneratedCreateWebhookResponse export type UpdateWebhookBody = GeneratedUpdateWebhookData export type ApiWebhookDelivery = GeneratedApiWebhookDelivery export type WebhookDeliveryPage = GeneratedWebhookDeliveryPage + +// Trash (personal & team workspaces) +export type ApiTrashNote = GeneratedApiTrashNote +export type BatchRestoreTrashBody = GeneratedBatchRestoreTrashBody +export type TrashBatchOperationResponse = GeneratedTrashBatchOperationResponse diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index 0725fd6..a5b7a6b 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -40,7 +40,7 @@ const EXPECTED_OPERATIONS = [ 'listFolders', 'listNoteComments', 'listNotes', - 'listPersonalTrash', + 'listTrash', 'listTeamFolders', 'listTeamNotes', 'listTeams', diff --git a/nodejs/tests/trash.spec.ts b/nodejs/tests/trash.spec.ts new file mode 100644 index 0000000..8a5e743 --- /dev/null +++ b/nodejs/tests/trash.spec.ts @@ -0,0 +1,86 @@ +import { http, HttpResponse } from 'msw' + +import { API } from '../src' +import { HttpResponseError } from '../src/error' +import { server } from './mock' + +const trashNote = { + id: 'note-1', title: 'Deleted note', tags: [], deletedAt: 1700000000000, + ownerId: 'user-1', teamId: null, shortId: 'short-1', publishType: 'view', + permanentDeletedAt: null, +} +const restored = { 'note-1': { status: 'success' } } +const partial = { ...restored, 'note-2': { status: 'failure', reason: 'NOT_FOUND' } } + +const cases = [ + { name: 'listTrash', method: 'GET', path: '/trash', status: 200, body: undefined, result: [trashNote], run: (api: API, raw: boolean) => api.listTrash({ unwrapData: !raw }) }, + { name: 'batchRestore 200', method: 'PUT', path: '/trash/batch-restore', status: 200, body: { noteIds: ['note-1'] }, result: restored, run: (api: API, raw: boolean) => api.batchRestore({ noteIds: ['note-1'] }, { unwrapData: !raw }) }, + { name: 'batchRestore 207', method: 'PUT', path: '/trash/batch-restore', status: 207, body: { noteIds: ['note-1', 'note-2'] }, result: partial, run: (api: API, raw: boolean) => api.batchRestore({ noteIds: ['note-1', 'note-2'] }, { unwrapData: !raw }) }, + { name: 'restoreNote', method: 'PUT', path: '/trash/note-1/restore', status: 204, body: undefined, result: undefined, run: (api: API, raw: boolean) => api.restoreNote('note-1', { unwrapData: !raw }) }, + { name: 'listTeamTrash', method: 'GET', path: '/teams/my-team/trash', status: 200, body: undefined, result: [{ ...trashNote, teamId: 'team-1' }], run: (api: API, raw: boolean) => api.listTeamTrash('my-team', { unwrapData: !raw }) }, +] + +beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +test.each(cases)('$name keeps the path, body, status and response shape', async ({ method, path, status, body, result, run }) => { + const requests: Array<{ method: string, body: string, authorization: string | null }> = [] + server.use(http.all(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ + method: request.method, + body: await request.text(), + authorization: request.headers.get('Authorization'), + }) + return status === 204 ? new HttpResponse(null, { status }) : HttpResponse.json(result, { status }) + })) + + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const unwrapped = await run(api, false) + const raw = await run(api, true) + if (typeof raw !== 'object' || raw === null || !('status' in raw) || !('data' in raw)) { + throw new Error('Expected a raw Axios response') + } + + expect(raw.status).toBe(status) + if (status === 204) { + expect([undefined, '']).toContain(unwrapped) + expect([undefined, '']).toContain(raw.data) + } else { + expect(unwrapped).toEqual(result) + expect(raw.data).toEqual(result) + } + expect(requests).toEqual([1, 2].map(() => ({ + method, + body: body === undefined ? '' : JSON.stringify(body), + authorization: 'Bearer test-token', + }))) +}) + +test('batch restore keeps legacy error wrapping for 404', async () => { + server.use(http.put('https://api.hackmd.io/v1/trash/batch-restore', () => + HttpResponse.json({ error: 'No trashed notes found' }, { status: 404 }) + )) + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + await expect(api.batchRestore({ noteIds: ['missing'] })).rejects.toBeInstanceOf(HttpResponseError) +}) + +test('team trash keeps legacy error wrapping for plain-text middleware errors', async () => { + server.use(http.get('https://api.hackmd.io/v1/teams/my-team/trash', () => + HttpResponse.text('Forbidden', { status: 403 }) + )) + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + await expect(api.listTeamTrash('my-team')).rejects.toBeInstanceOf(HttpResponseError) +}) + +test('restoreNote uses a custom base URL and authorization', async () => { + let authorization: string | null = null + server.use(http.put('https://custom.hackmd.test/v1/trash/note-1/restore', ({ request }) => { + authorization = request.headers.get('Authorization') + return new HttpResponse(null, { status: 204 }) + })) + const api = new API('custom-token', 'https://custom.hackmd.test/v1/') + const response = await api.restoreNote('note-1', { unwrapData: false }) + expect(response.status).toBe(204) + expect(authorization).toBe('Bearer custom-token') +}) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 0b5ea0c..9c826d5 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -151,6 +151,20 @@ async function exploreWebhookDeliveries (client: API): Promise { return delivery.id } +async function exploreTrash (client: API): Promise { + const personal = await client.listTrash() + const team = await client.listTeamTrash('TEAM_PATH') + const results = await client.batchRestore({ noteIds: [personal[0].id] }) + const outcome: 'success' | 'failure' = results[personal[0].id].status + const raw = await client.batchRestore({ noteIds: [personal[0].id] }, { unwrapData: false }) + const status: number = raw.status + const restored: void = await client.restoreNote(team[0].id) + void outcome + void status + void restored + return personal[0].id +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -171,4 +185,5 @@ void exploreProfileAndTeams void exploreImageUpload void exploreWebhooks void exploreWebhookDeliveries +void exploreTrash void exploreRawNote From 4c20a1f1edab612c34a2d7b418d11f5798221af2 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 24 Sep 2026 23:22:14 +0800 Subject: [PATCH 23/31] feat: add note version operations to API client --- nodejs/README.md | 8 ++++ nodejs/src/index.ts | 59 +++++++++++++++++++++++++++ nodejs/src/type.ts | 18 ++++++++ nodejs/tests/types/playground.mts | 16 ++++++++ nodejs/tests/versions.spec.ts | 68 +++++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+) create mode 100644 nodejs/tests/versions.spec.ts diff --git a/nodejs/README.md b/nodejs/README.md index 3e6b3ae..5868d23 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -172,6 +172,14 @@ Use `listTrash()` or `listTeamTrash('team-path')` to find deleted notes. success or failure for each note. Pass `{ unwrapData: false }` to inspect the batch HTTP status (200 or 207). +### Versions + +`listVersions('note-id', { named_only: true, page: 1, limit: 20 })` lists saved +versions. Use `getVersion('note-id', versionId)` for content, or +`compareVersions('note-id', { base: 'version:', target: 'note_content' })` +to compare it with the live note. `createVersion` and `updateVersion` accept the +corresponding OpenAPI request bodies. + ### Generated Raw API The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index d7c6699..43713b2 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -6,6 +6,7 @@ import { batchRestore as generatedBatchRestore, createFolder as generatedCreateFolder, createNote as generatedCreateNote, + createVersion as generatedCreateVersion, createTeamFolder as generatedCreateTeamFolder, createTeamNote as generatedCreateTeamNote, createTeamWebhook as generatedCreateTeamWebhook, @@ -23,6 +24,7 @@ import { getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, getNote as generatedGetNote, + getVersion as generatedGetVersion, getTeamNote as generatedGetTeamNote, getTeamFolder as generatedGetTeamFolder, getTeamFolderOrder as generatedGetTeamFolderOrder, @@ -32,6 +34,7 @@ import { getWebhookDelivery as generatedGetWebhookDelivery, listFolders, listNotes, + listVersions as generatedListVersions, listTrash as generatedListTrash, listTeams, listTeamFolders, @@ -44,9 +47,11 @@ import { pingTeamWebhook as generatedPingTeamWebhook, pingWebhook as generatedPingWebhook, restoreNote as generatedRestoreNote, + compareVersions as generatedCompareVersions, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, updateNote as generatedUpdateNote, + updateVersion as generatedUpdateVersion, updateTeamFolder as generatedUpdateTeamFolder, updateTeamFolderOrder as generatedUpdateTeamFolderOrder, updateTeamNote as generatedUpdateTeamNote, @@ -92,6 +97,14 @@ import { ApiTrashNote, BatchRestoreTrashBody, TrashBatchOperationResponse, + CompareNoteVersions, + CompareVersionsQuery, + CreateNoteVersionBody, + GetNoteVersions, + ListVersionsQuery, + NoteVersion, + NoteVersionMetadata, + UpdateNoteVersionBody, } from './type.js' import * as HackMDErrors from './error' @@ -102,6 +115,7 @@ export type RequestOptions = { export type GetHistoryOptions = RequestOptions & { limit?: number } export type WebhookDeliveryListOptions = RequestOptions & { page?: number; limit?: number } +export type ListVersionsOptions = RequestOptions & ListVersionsQuery const defaultOption: RequestOptions = { unwrapData: true, @@ -685,6 +699,51 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } + async listVersions (noteId: string, options = defaultOption as Opt): Promise> { + const { named_only, q, created_by, created_after, created_before, page, limit } = options + return this.unwrapData(generatedListVersions({ + client: this.generatedClient, + path: { noteId }, + query: { named_only, q, created_by, created_after, created_before, page, limit }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async getVersion (noteId: string, versionId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetVersion({ + client: this.generatedClient, + path: { noteId, versionId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async createVersion (noteId: string, body: CreateNoteVersionBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCreateVersion({ + client: this.generatedClient, + path: { noteId }, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async updateVersion (noteId: string, body: UpdateNoteVersionBody, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUpdateVersion({ + client: this.generatedClient, + path: { noteId }, + body, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async compareVersions (noteId: string, query: CompareVersionsQuery, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedCompareVersions({ + client: this.generatedClient, + path: { noteId }, + query, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { if (!unwrap) { // For raw responses, etag is available via response.headers diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 317600b..5fb100b 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -4,12 +4,19 @@ import type { ApiWebhookDelivery as GeneratedApiWebhookDelivery, ApiTrashNote as GeneratedApiTrashNote, BatchRestoreTrashBody as GeneratedBatchRestoreTrashBody, + CompareNoteVersions as GeneratedCompareNoteVersions, + CompareVersionsData as GeneratedCompareVersionsData, + CreateNoteVersionBody as GeneratedCreateNoteVersionBody, CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, CreateWebhookResponse as GeneratedCreateWebhookResponse, FolderPath as GeneratedFolderPath, GetUserHistory as GeneratedUserHistory, + GetNoteVersions as GeneratedGetNoteVersions, + ListVersionsData as GeneratedListVersionsData, + NoteVersion as GeneratedNoteVersion, + NoteVersionMetadata as GeneratedNoteVersionMetadata, NoteType as GeneratedNote, NoteImageUploadResponse as GeneratedNoteImageUploadResponse, PaginatedResponseApiWebhookDelivery as GeneratedWebhookDeliveryPage, @@ -18,6 +25,7 @@ import type { Team as GeneratedTeam, TrashBatchOperationResponse as GeneratedTrashBatchOperationResponse, UpdateNoteData as GeneratedUpdateNoteData, + UpdateNoteVersionBody as GeneratedUpdateNoteVersionBody, UpdateWebhookData as GeneratedUpdateWebhookData, User as GeneratedUser, } from './generated/types.gen.js' @@ -145,3 +153,13 @@ export type WebhookDeliveryPage = GeneratedWebhookDeliveryPage export type ApiTrashNote = GeneratedApiTrashNote export type BatchRestoreTrashBody = GeneratedBatchRestoreTrashBody export type TrashBatchOperationResponse = GeneratedTrashBatchOperationResponse + +// Versions +export type NoteVersion = GeneratedNoteVersion +export type NoteVersionMetadata = GeneratedNoteVersionMetadata +export type GetNoteVersions = GeneratedGetNoteVersions +export type CreateNoteVersionBody = GeneratedCreateNoteVersionBody +export type UpdateNoteVersionBody = GeneratedUpdateNoteVersionBody +export type CompareNoteVersions = GeneratedCompareNoteVersions +export type CompareVersionsQuery = GeneratedCompareVersionsData['query'] +export type ListVersionsQuery = NonNullable diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 9c826d5..da4aaa7 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -165,6 +165,21 @@ async function exploreTrash (client: API): Promise { return personal[0].id } +async function exploreVersions (client: API): Promise { + const page = await client.listVersions('NOTE_ID', { named_only: true, page: 1, limit: 20 }) + const version = await client.getVersion('NOTE_ID', page.data[0].id) + const created = await client.createVersion('NOTE_ID', { name: 'Milestone', base: 'note_content' }) + const updated = await client.updateVersion('NOTE_ID', { base: `version:${created.id}`, name: 'Renamed' }) + const comparison = await client.compareVersions('NOTE_ID', { base: `version:${version.id}`, target: 'note_content' }) + const diff: string = comparison.unified_diff + const raw = await client.listVersions('NOTE_ID', { unwrapData: false }) + const status: number = raw.status + void updated + void diff + void status + return created.id +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -186,4 +201,5 @@ void exploreImageUpload void exploreWebhooks void exploreWebhookDeliveries void exploreTrash +void exploreVersions void exploreRawNote diff --git a/nodejs/tests/versions.spec.ts b/nodejs/tests/versions.spec.ts new file mode 100644 index 0000000..8dba55a --- /dev/null +++ b/nodejs/tests/versions.spec.ts @@ -0,0 +1,68 @@ +import { http, HttpResponse } from 'msw' + +import { API } from '../src' +import { HttpResponseError } from '../src/error' +import { server } from './mock' + +const metadata = { + id: 'version-1', name: 'Milestone', name_source: 'user' as const, + created_at: '2026-01-01T00:00:00.000Z', created_by: null, authorship: [], + content_available: true, +} +const page = { data: [metadata], meta: { total_pages: 1, total: 1, limit: 20, page: 1 } } +const createBody = { base: 'note_content', name: 'Milestone' } +const updateBody = { base: 'version:version-1', name: 'Renamed' } +const comparison = { base: 'version:version-1', target: 'note_content' } + +const cases = [ + { name: 'listVersions', method: 'GET', path: '/notes/note-1/versions', status: 200, query: { named_only: 'true', q: 'milestone', created_by: 'user-1', created_after: '2026-01-01T00:00:00Z', created_before: '2026-02-01T00:00:00Z', page: '1', limit: '20' }, body: undefined, result: page, run: (api: API, raw: boolean) => api.listVersions('note-1', { named_only: true, q: 'milestone', created_by: 'user-1', created_after: '2026-01-01T00:00:00Z', created_before: '2026-02-01T00:00:00Z', page: 1, limit: 20, unwrapData: !raw }) }, + { name: 'getVersion', method: 'GET', path: '/notes/note-1/versions/version-1', status: 200, query: {}, body: undefined, result: { ...metadata, content: '# Note' }, run: (api: API, raw: boolean) => api.getVersion('note-1', 'version-1', { unwrapData: !raw }) }, + { name: 'createVersion', method: 'POST', path: '/notes/note-1/versions', status: 201, query: {}, body: createBody, result: metadata, run: (api: API, raw: boolean) => api.createVersion('note-1', createBody, { unwrapData: !raw }) }, + { name: 'updateVersion', method: 'PATCH', path: '/notes/note-1/versions', status: 200, query: {}, body: updateBody, result: { ...metadata, name: 'Renamed' }, run: (api: API, raw: boolean) => api.updateVersion('note-1', updateBody, { unwrapData: !raw }) }, + { name: 'compareVersions', method: 'GET', path: '/notes/note-1/versions/compare', status: 200, query: comparison, body: undefined, result: { unified_diff: '@@ -1 +1 @@' }, run: (api: API, raw: boolean) => api.compareVersions('note-1', comparison, { unwrapData: !raw }) }, +] + +beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +test.each(cases)('$name preserves the method, path, query, body and response', async ({ method, path, status, query, body, result, run }) => { + const requests: Array<{ method: string, query: Record, body: string, authorization: string | null }> = [] + server.use(http.all(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ + method: request.method, + query: Object.fromEntries(new URL(request.url).searchParams), + body: await request.text(), + authorization: request.headers.get('Authorization'), + }) + return HttpResponse.json(result, { status }) + })) + + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const unwrapped = await run(api, false) + const raw = await run(api, true) + if (typeof raw !== 'object' || raw === null || !('status' in raw) || !('data' in raw)) { + throw new Error('Expected a raw Axios response') + } + + expect(unwrapped).toEqual(result) + expect(raw.status).toBe(status) + expect(raw.data).toEqual(result) + expect(requests).toEqual([1, 2].map(() => ({ + method, + query, + body: body === undefined ? '' : JSON.stringify(body), + authorization: 'Bearer test-token', + }))) +}) + +test('uses a custom base URL and keeps legacy error wrapping', async () => { + server.use(http.get('https://custom.hackmd.test/v1/notes/note-1/versions/missing', () => + HttpResponse.json({ error_code: 'not_found', message: 'Version not found' }, { status: 404 }) + )) + const api = new API('test-token', 'https://custom.hackmd.test/v1/', { + wrapResponseErrors: true, + retryConfig: undefined, + }) + await expect(api.getVersion('note-1', 'missing')).rejects.toBeInstanceOf(HttpResponseError) +}) From b5bebe8a8cdf5d1d726beb562b0a17fccd04438a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 00:04:49 +0800 Subject: [PATCH 24/31] feat: add note comment operations to API client --- nodejs/README.md | 7 +++ nodejs/src/index.ts | 43 ++++++++++++++++++ nodejs/src/type.ts | 12 +++++ nodejs/tests/comments.spec.ts | 75 +++++++++++++++++++++++++++++++ nodejs/tests/types/playground.mts | 14 ++++++ 5 files changed, 151 insertions(+) create mode 100644 nodejs/tests/comments.spec.ts diff --git a/nodejs/README.md b/nodejs/README.md index 5868d23..a6ec0a1 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -180,6 +180,13 @@ versions. Use `getVersion('note-id', versionId)` for content, or to compare it with the live note. `createVersion` and `updateVersion` accept the corresponding OpenAPI request bodies. +### Comments + +Use `listNoteComments('note-id', { page: 1, limit: 20, threadStatus: 'open' })` +to browse comments. `getNoteComment('note-id', commentId)` reads one comment; +`resolveNoteComment` and `unresolveNoteComment` return the updated comment and +any affected thread. + ### Generated Raw API The `@hackmd/api/raw` entry point exposes every OpenAPI operation as a generated, diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 43713b2..59bc39b 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -24,6 +24,7 @@ import { getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, getNote as generatedGetNote, + getNoteComment as generatedGetNoteComment, getVersion as generatedGetVersion, getTeamNote as generatedGetTeamNote, getTeamFolder as generatedGetTeamFolder, @@ -34,6 +35,7 @@ import { getWebhookDelivery as generatedGetWebhookDelivery, listFolders, listNotes, + listNoteComments as generatedListNoteComments, listVersions as generatedListVersions, listTrash as generatedListTrash, listTeams, @@ -47,6 +49,7 @@ import { pingTeamWebhook as generatedPingTeamWebhook, pingWebhook as generatedPingWebhook, restoreNote as generatedRestoreNote, + resolveNoteComment as generatedResolveNoteComment, compareVersions as generatedCompareVersions, updateFolder as generatedUpdateFolder, updateFolderOrder as generatedUpdateFolderOrder, @@ -57,6 +60,7 @@ import { updateTeamNote as generatedUpdateTeamNote, updateTeamWebhook as generatedUpdateTeamWebhook, updateWebhook as generatedUpdateWebhook, + unresolveNoteComment as generatedUnresolveNoteComment, uploadNoteImage as generatedUploadNoteImage, } from './generated/sdk.gen.js' import { @@ -105,6 +109,10 @@ import { NoteVersion, NoteVersionMetadata, UpdateNoteVersionBody, + ApiCommentDetail, + ApiCommentResolutionResponse, + GetNoteComments, + ListNoteCommentsQuery, } from './type.js' import * as HackMDErrors from './error' @@ -116,6 +124,7 @@ export type RequestOptions = { export type GetHistoryOptions = RequestOptions & { limit?: number } export type WebhookDeliveryListOptions = RequestOptions & { page?: number; limit?: number } export type ListVersionsOptions = RequestOptions & ListVersionsQuery +export type ListNoteCommentsOptions = RequestOptions & ListNoteCommentsQuery const defaultOption: RequestOptions = { unwrapData: true, @@ -744,6 +753,40 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } + async listNoteComments (noteId: string, options = defaultOption as Opt): Promise> { + const { page, limit, sort, threadId, commentStatus, threadStatus, isThreadHead } = options + return this.unwrapData(generatedListNoteComments({ + client: this.generatedClient, + path: { noteId }, + query: { page, limit, sort, threadId, commentStatus, threadStatus, isThreadHead }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async getNoteComment (noteId: string, commentId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedGetNoteComment({ + client: this.generatedClient, + path: { noteId, commentId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async resolveNoteComment (noteId: string, commentId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedResolveNoteComment({ + client: this.generatedClient, + path: { noteId, commentId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + + async unresolveNoteComment (noteId: string, commentId: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedUnresolveNoteComment({ + client: this.generatedClient, + path: { noteId, commentId }, + throwOnError: true, + }), options.unwrapData) as unknown as OptionReturnType + } + private unwrapData (reqP: Promise>, unwrap = true, includeEtag = false) { if (!unwrap) { // For raw responses, etag is available via response.headers diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 5fb100b..3acbd6b 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -1,4 +1,7 @@ import type { + ApiComment as GeneratedApiComment, + ApiCommentDetail as GeneratedApiCommentDetail, + ApiCommentResolutionResponse as GeneratedApiCommentResolutionResponse, ApiFolder as GeneratedApiFolder, ApiWebhook as GeneratedApiWebhook, ApiWebhookDelivery as GeneratedApiWebhookDelivery, @@ -12,9 +15,11 @@ import type { CreateNoteMultiStatusResponse as GeneratedCreateNoteMultiStatusResponse, CreateWebhookResponse as GeneratedCreateWebhookResponse, FolderPath as GeneratedFolderPath, + GetNoteComments as GeneratedGetNoteComments, GetUserHistory as GeneratedUserHistory, GetNoteVersions as GeneratedGetNoteVersions, ListVersionsData as GeneratedListVersionsData, + ListNoteCommentsData as GeneratedListNoteCommentsData, NoteVersion as GeneratedNoteVersion, NoteVersionMetadata as GeneratedNoteVersionMetadata, NoteType as GeneratedNote, @@ -163,3 +168,10 @@ export type UpdateNoteVersionBody = GeneratedUpdateNoteVersionBody export type CompareNoteVersions = GeneratedCompareNoteVersions export type CompareVersionsQuery = GeneratedCompareVersionsData['query'] export type ListVersionsQuery = NonNullable + +// Comments +export type ApiComment = GeneratedApiComment +export type ApiCommentDetail = GeneratedApiCommentDetail +export type ApiCommentResolutionResponse = GeneratedApiCommentResolutionResponse +export type GetNoteComments = GeneratedGetNoteComments +export type ListNoteCommentsQuery = NonNullable diff --git a/nodejs/tests/comments.spec.ts b/nodejs/tests/comments.spec.ts new file mode 100644 index 0000000..1d564b4 --- /dev/null +++ b/nodejs/tests/comments.spec.ts @@ -0,0 +1,75 @@ +import { http, HttpResponse } from 'msw' + +import { API } from '../src' +import { HttpResponseError } from '../src/error' +import { server } from './mock' + +const comment = { + id: 'comment-1', threadId: 'thread-1', + actor: { kind: 'user', userId: 'user-1', displayName: 'Alice' }, + body: 'Hello', contentType: 'plain_text', + commentState: { status: 'open', reason: null, actedBy: null, actedAt: null }, + isPinned: false, anchor: null, currentAnchor: null, excerpt: null, + reactions: [], createdAt: 1700000000000, updatedAt: 1700000000000, + isThreadHead: true, replyCount: 0, + threadState: { status: 'open', reason: null, actedBy: null, actedAt: null, sourceCommentId: 'comment-1' }, +} +const page = { comments: [comment], pagination: { page: 2, limit: 10, hasNext: false } } +const resolved = { comment: { ...comment, threadState: { ...comment.threadState, status: 'hidden', reason: 'resolved' } }, cascade: { threadId: 'thread-1' } } +const reopened = { comment, cascade: null } +const query = { page: '2', limit: '10', sort: 'desc', threadId: '00000000-0000-4000-8000-000000000001', commentStatus: 'open', threadStatus: 'hidden', isThreadHead: 'true' } + +const cases = [ + { name: 'listNoteComments', method: 'GET', path: '/notes/note-1/comments', query, result: page, run: (api: API, raw: boolean) => api.listNoteComments('note-1', { page: 2, limit: 10, sort: 'desc', threadId: query.threadId, commentStatus: 'open', threadStatus: 'hidden', isThreadHead: true, unwrapData: !raw }) }, + { name: 'getNoteComment', method: 'GET', path: '/notes/note-1/comments/comment-1', query: {}, result: comment, run: (api: API, raw: boolean) => api.getNoteComment('note-1', 'comment-1', { unwrapData: !raw }) }, + { name: 'resolveNoteComment', method: 'PUT', path: '/notes/note-1/comments/comment-1/resolution', query: {}, result: resolved, run: (api: API, raw: boolean) => api.resolveNoteComment('note-1', 'comment-1', { unwrapData: !raw }) }, + { name: 'unresolveNoteComment', method: 'DELETE', path: '/notes/note-1/comments/comment-1/resolution', query: {}, result: reopened, run: (api: API, raw: boolean) => api.unresolveNoteComment('note-1', 'comment-1', { unwrapData: !raw }) }, +] + +beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +test.each(cases)('$name preserves the method, path, query, authentication and response', async ({ method, path, query, result, run }) => { + const requests: Array<{ method: string, query: Record, body: string, authorization: string | null }> = [] + server.use(http.all(`https://api.hackmd.io/v1${path}`, async ({ request }) => { + requests.push({ + method: request.method, + query: Object.fromEntries(new URL(request.url).searchParams), + body: await request.text(), + authorization: request.headers.get('Authorization'), + }) + return HttpResponse.json(result) + })) + + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + const unwrapped = await run(api, false) + const raw = await run(api, true) + if (typeof raw !== 'object' || raw === null || !('status' in raw) || !('data' in raw)) { + throw new Error('Expected a raw Axios response') + } + + expect(unwrapped).toEqual(result) + expect(raw.status).toBe(200) + expect(raw.data).toEqual(result) + expect(requests).toEqual([1, 2].map(() => ({ method, query, body: '', authorization: 'Bearer test-token' }))) +}) + +test('uses a custom base URL and keeps legacy error wrapping', async () => { + server.use(http.get('https://custom.hackmd.test/v1/notes/note-1/comments/missing', () => + HttpResponse.json({ errorCode: 'not_found', message: 'Comment not found' }, { status: 404 }) + )) + const api = new API('test-token', 'https://custom.hackmd.test/v1/', { + wrapResponseErrors: true, + retryConfig: undefined, + }) + await expect(api.getNoteComment('note-1', 'missing')).rejects.toBeInstanceOf(HttpResponseError) +}) + +test('keeps resolution conflict errors wrapped', async () => { + server.use(http.put('https://api.hackmd.io/v1/notes/note-1/comments/comment-1/resolution', () => + HttpResponse.json({ errorCode: 'resolution_conflict', message: 'Conflict', commentState: comment.commentState }, { status: 409 }) + )) + const api = new API('test-token', undefined, { wrapResponseErrors: true, retryConfig: undefined }) + await expect(api.resolveNoteComment('note-1', 'comment-1')).rejects.toBeInstanceOf(HttpResponseError) +}) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index da4aaa7..91653ea 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -180,6 +180,19 @@ async function exploreVersions (client: API): Promise { return created.id } +async function exploreComments (client: API): Promise { + const page = await client.listNoteComments('NOTE_ID', { page: 1, limit: 20, isThreadHead: true }) + const comment = await client.getNoteComment('NOTE_ID', page.comments[0].id) + const resolved = await client.resolveNoteComment('NOTE_ID', comment.id) + const reopened = await client.unresolveNoteComment('NOTE_ID', comment.id) + const raw = await client.listNoteComments('NOTE_ID', { unwrapData: false }) + const status: number = raw.status + const threadId: string = resolved.comment.threadId + void reopened + void status + return threadId +} + function exploreRawNote (note: RawNote): string { return note.content } @@ -202,4 +215,5 @@ void exploreWebhooks void exploreWebhookDeliveries void exploreTrash void exploreVersions +void exploreComments void exploreRawNote From f9e5bc9355eba37f5968be87aff7b5465614e3df Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 01:06:49 +0800 Subject: [PATCH 25/31] feat: align API method names with OpenAPI operations --- nodejs/README.md | 5 +++ nodejs/spec/hackmd-openapi.json | 2 +- nodejs/src/generated/index.ts | 4 +-- nodejs/src/generated/sdk.gen.ts | 4 +-- nodejs/src/generated/types.gen.ts | 6 ++-- nodejs/src/index.ts | 59 ++++++++++++++++++++++--------- nodejs/tests/api.spec.ts | 10 ++++++ nodejs/tests/generated.spec.ts | 13 +++++-- nodejs/tests/types/playground.mts | 18 +++++++--- 9 files changed, 88 insertions(+), 33 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index a6ec0a1..e92792d 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -213,6 +213,11 @@ The package root retains the existing `API` class. Files under `src/generated` are generated from the vendored OpenAPI document and must not be edited manually. +On `API`, `listNotes`, `listFolders`, `listTeamNotes`, `listTeamFolders`, and +`listTeams` match the OpenAPI operation names. Existing `getNoteList`, +`getFolderList`, `getTeamNotes`, `getTeamFolderList`, and `getTeams` remain +compatible aliases. + ## API The [API reference](https://hackmdio.github.io/api-client/) covers the existing diff --git a/nodejs/spec/hackmd-openapi.json b/nodejs/spec/hackmd-openapi.json index e498ae7..e6796dc 100644 --- a/nodejs/spec/hackmd-openapi.json +++ b/nodejs/spec/hackmd-openapi.json @@ -6599,7 +6599,7 @@ }, "/me": { "get": { - "operationId": "GetCurrentUser", + "operationId": "GetMe", "responses": { "200": { "description": "Ok", diff --git a/nodejs/src/generated/index.ts b/nodejs/src/generated/index.ts index 5db128d..2a0b489 100644 --- a/nodejs/src/generated/index.ts +++ b/nodejs/src/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { batchRestore, compareVersions, createFolder, createNote, createTeamFolder, createTeamNote, createTeamWebhook, createVersion, createWebhook, deleteFolder, deleteNote, deleteTeamFolder, deleteTeamNote, deleteTeamWebhook, deleteWebhook, exportTeamWebhookDeliveries, exportWebhookDeliveries, getCurrentUser, getFolder, getFolderOrder, getHistory, getNote, getNoteComment, getTeamFolder, getTeamFolderOrder, getTeamNote, getTeamWebhook, getTeamWebhookDelivery, getVersion, getWebhook, getWebhookDelivery, listFolders, listNoteComments, listNotes, listTeamFolders, listTeamNotes, listTeams, listTeamTrash, listTeamWebhookDeliveries, listTeamWebhooks, listTrash, listVersions, listWebhookDeliveries, listWebhooks, type Options, pingTeamWebhook, pingWebhook, resolveNoteComment, restoreNote, unresolveNoteComment, updateFolder, updateFolderOrder, updateNote, updateTeamFolder, updateTeamFolderOrder, updateTeamNote, updateTeamWebhook, updateVersion, updateWebhook, uploadNoteImage } from './sdk.gen.js'; -export type { ApiBadRequestResponse, ApiComment, ApiCommentActor, ApiCommentAnchor, ApiCommentDeletedUserActor, ApiCommentDetail, ApiCommentDetailReply, ApiCommentError, ApiCommentGuestActor, ApiCommentHead, ApiCommentId, ApiCommentLimit, ApiCommentListReply, ApiCommentPage, ApiCommentPagination, ApiCommentReactionShortcode, ApiCommentReactionSummary, ApiCommentResolutionCascade, ApiCommentResolutionConflictError, ApiCommentResolutionResponse, ApiCommentState, ApiCommentStateActor, ApiCommentStateDeletedUserActor, ApiCommentStateReason, ApiCommentStateUserActor, ApiCommentStatus, ApiCommentTeamActor, ApiCommentThreadId, ApiCommentThreadState, ApiCommentUserActor, ApiError, ApiErrorResponse, ApiFolder, ApiFolderOrder, ApiNoteFeatures, ApiRequestErrorResponse, ApiTrashNote, ApiValidationError, ApiValidationFieldError, ApiWebhook, ApiWebhookDelivery, ApiWebhookLimit, ApiWebhookPage, ApiWebhookScope, BatchOperationError, BatchRestoreData, BatchRestoreError, BatchRestoreErrors, BatchRestoreResponse, BatchRestoreResponses, BatchRestoreTrashBody, ClientOptions, CommentPermissionType, CompareNoteVersions, CompareVersionsData, CompareVersionsError, CompareVersionsErrors, CompareVersionsResponse, CompareVersionsResponses, CreateApiWebhookBody, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteMultiStatusResponse, CreateNoteResponse, CreateNoteResponses, CreateNoteVersionBody, CreateNoteVersionConflictError, CreateTeamFolderBody, CreateTeamFolderData, CreateTeamFolderError, CreateTeamFolderErrors, CreateTeamFolderResponse, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteError, CreateTeamNoteErrors, CreateTeamNoteResponse, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookError, CreateTeamWebhookErrors, CreateTeamWebhookResponse, CreateTeamWebhookResponses, CreateUserFolderBody, CreateVersionData, CreateVersionError, CreateVersionErrors, CreateVersionResponse, CreateVersionResponses, CreateWebhookData, CreateWebhookData2, CreateWebhookError, CreateWebhookErrors, CreateWebhookResponse, CreateWebhookResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderError, DeleteTeamFolderErrors, DeleteTeamFolderResponse, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteError, DeleteTeamNoteErrors, DeleteTeamNoteResponse, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookError, DeleteTeamWebhookErrors, DeleteTeamWebhookResponse, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesError, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponse, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesError, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponse, ExportWebhookDeliveriesResponses, FolderColorHex, FolderIconCodepoint, FolderName, FolderPath, GetCurrentUserData, GetCurrentUserResponse, GetCurrentUserResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponse, GetFolderOrderResponses, GetFolderResponse, GetFolderResponses, GetHistoryData, GetHistoryResponse, GetHistoryResponses, GetNoteCommentData, GetNoteCommentError, GetNoteCommentErrors, GetNoteCommentResponse, GetNoteCommentResponses, GetNoteComments, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetNoteVersions, GetTeamFolderData, GetTeamFolderError, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponse, GetTeamFolderOrderResponses, GetTeamFolderResponse, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteError, GetTeamNoteErrors, GetTeamNoteResponse, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryError, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponse, GetTeamWebhookDeliveryResponses, GetTeamWebhookError, GetTeamWebhookErrors, GetTeamWebhookResponse, GetTeamWebhookResponses, GetUserHistory, GetVersionData, GetVersionError, GetVersionErrors, GetVersionResponse, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryError, GetWebhookDeliveryErrors, GetWebhookDeliveryResponse, GetWebhookDeliveryResponses, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, ListFoldersData, ListFoldersResponse, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsError, ListNoteCommentsErrors, ListNoteCommentsResponse, ListNoteCommentsResponses, ListNotesData, ListNotesResponse, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponse, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponse, ListTeamNotesResponses, ListTeamsData, ListTeamsResponse, ListTeamsResponses, ListTeamTrashData, ListTeamTrashError, ListTeamTrashErrors, ListTeamTrashResponse, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesError, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponse, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponse, ListTeamWebhooksResponses, ListTrashData, ListTrashResponse, ListTrashResponses, ListVersionsData, ListVersionsError, ListVersionsErrors, ListVersionsResponse, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesError, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponse, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponse, ListWebhooksResponses, NoContentChangeError, Note, NoteImageUploadResponse, NotePermissionRole, NotePublishType, NoteType, NoteVersion, NoteVersionMetadata, NoteVersionUser, PaginatedQuery, PaginatedResponseApiWebhookDelivery, PingTeamWebhookData, PingTeamWebhookError, PingTeamWebhookErrors, PingTeamWebhookResponse, PingTeamWebhookResponses, PingWebhookData, PingWebhookError, PingWebhookErrors, PingWebhookResponse, PingWebhookResponses, RecordStringApiValidationFieldError, ResolveNoteCommentData, ResolveNoteCommentError, ResolveNoteCommentErrors, ResolveNoteCommentResponse, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteError, RestoreNoteErrors, RestoreNoteResponse, RestoreNoteResponses, SimpleUserProfile, SingleNote, SuggestEditPermissionType, Team, TeamVisibilityType, TrashBatchOperationResponse, UnresolveNoteCommentData, UnresolveNoteCommentError, UnresolveNoteCommentErrors, UnresolveNoteCommentResponse, UnresolveNoteCommentResponses, UpdateApiWebhookBody, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderOrderBody, UpdateFolderOrderData, UpdateFolderOrderError, UpdateFolderOrderErrors, UpdateFolderOrderResponse, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponses, UpdateNoteVersionBody, UpdateTeamFolderBody, UpdateTeamFolderData, UpdateTeamFolderError, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderError, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponse, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteError, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookError, UpdateTeamWebhookErrors, UpdateTeamWebhookResponse, UpdateTeamWebhookResponses, UpdateUserFolderBody, UpdateVersionData, UpdateVersionError, UpdateVersionErrors, UpdateVersionResponse, UpdateVersionResponses, UpdateWebhookData, UpdateWebhookData2, UpdateWebhookError, UpdateWebhookErrors, UpdateWebhookResponse, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageError, UploadNoteImageErrors, UploadNoteImageResponse, UploadNoteImageResponses, User, VersionAlreadyNamedError, VersionNotNamedError, WebhookApiErrorResponse, WebhookDeliveryStatusCategory } from './types.gen.js'; +export { batchRestore, compareVersions, createFolder, createNote, createTeamFolder, createTeamNote, createTeamWebhook, createVersion, createWebhook, deleteFolder, deleteNote, deleteTeamFolder, deleteTeamNote, deleteTeamWebhook, deleteWebhook, exportTeamWebhookDeliveries, exportWebhookDeliveries, getFolder, getFolderOrder, getHistory, getMe, getNote, getNoteComment, getTeamFolder, getTeamFolderOrder, getTeamNote, getTeamWebhook, getTeamWebhookDelivery, getVersion, getWebhook, getWebhookDelivery, listFolders, listNoteComments, listNotes, listTeamFolders, listTeamNotes, listTeams, listTeamTrash, listTeamWebhookDeliveries, listTeamWebhooks, listTrash, listVersions, listWebhookDeliveries, listWebhooks, type Options, pingTeamWebhook, pingWebhook, resolveNoteComment, restoreNote, unresolveNoteComment, updateFolder, updateFolderOrder, updateNote, updateTeamFolder, updateTeamFolderOrder, updateTeamNote, updateTeamWebhook, updateVersion, updateWebhook, uploadNoteImage } from './sdk.gen.js'; +export type { ApiBadRequestResponse, ApiComment, ApiCommentActor, ApiCommentAnchor, ApiCommentDeletedUserActor, ApiCommentDetail, ApiCommentDetailReply, ApiCommentError, ApiCommentGuestActor, ApiCommentHead, ApiCommentId, ApiCommentLimit, ApiCommentListReply, ApiCommentPage, ApiCommentPagination, ApiCommentReactionShortcode, ApiCommentReactionSummary, ApiCommentResolutionCascade, ApiCommentResolutionConflictError, ApiCommentResolutionResponse, ApiCommentState, ApiCommentStateActor, ApiCommentStateDeletedUserActor, ApiCommentStateReason, ApiCommentStateUserActor, ApiCommentStatus, ApiCommentTeamActor, ApiCommentThreadId, ApiCommentThreadState, ApiCommentUserActor, ApiError, ApiErrorResponse, ApiFolder, ApiFolderOrder, ApiNoteFeatures, ApiRequestErrorResponse, ApiTrashNote, ApiValidationError, ApiValidationFieldError, ApiWebhook, ApiWebhookDelivery, ApiWebhookLimit, ApiWebhookPage, ApiWebhookScope, BatchOperationError, BatchRestoreData, BatchRestoreError, BatchRestoreErrors, BatchRestoreResponse, BatchRestoreResponses, BatchRestoreTrashBody, ClientOptions, CommentPermissionType, CompareNoteVersions, CompareVersionsData, CompareVersionsError, CompareVersionsErrors, CompareVersionsResponse, CompareVersionsResponses, CreateApiWebhookBody, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteMultiStatusResponse, CreateNoteResponse, CreateNoteResponses, CreateNoteVersionBody, CreateNoteVersionConflictError, CreateTeamFolderBody, CreateTeamFolderData, CreateTeamFolderError, CreateTeamFolderErrors, CreateTeamFolderResponse, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteError, CreateTeamNoteErrors, CreateTeamNoteResponse, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookError, CreateTeamWebhookErrors, CreateTeamWebhookResponse, CreateTeamWebhookResponses, CreateUserFolderBody, CreateVersionData, CreateVersionError, CreateVersionErrors, CreateVersionResponse, CreateVersionResponses, CreateWebhookData, CreateWebhookData2, CreateWebhookError, CreateWebhookErrors, CreateWebhookResponse, CreateWebhookResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderError, DeleteTeamFolderErrors, DeleteTeamFolderResponse, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteError, DeleteTeamNoteErrors, DeleteTeamNoteResponse, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookError, DeleteTeamWebhookErrors, DeleteTeamWebhookResponse, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookError, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesError, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponse, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesError, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponse, ExportWebhookDeliveriesResponses, FolderColorHex, FolderIconCodepoint, FolderName, FolderPath, GetFolderData, GetFolderError, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponse, GetFolderOrderResponses, GetFolderResponse, GetFolderResponses, GetHistoryData, GetHistoryResponse, GetHistoryResponses, GetMeData, GetMeResponse, GetMeResponses, GetNoteCommentData, GetNoteCommentError, GetNoteCommentErrors, GetNoteCommentResponse, GetNoteCommentResponses, GetNoteComments, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetNoteVersions, GetTeamFolderData, GetTeamFolderError, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponse, GetTeamFolderOrderResponses, GetTeamFolderResponse, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteError, GetTeamNoteErrors, GetTeamNoteResponse, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryError, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponse, GetTeamWebhookDeliveryResponses, GetTeamWebhookError, GetTeamWebhookErrors, GetTeamWebhookResponse, GetTeamWebhookResponses, GetUserHistory, GetVersionData, GetVersionError, GetVersionErrors, GetVersionResponse, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryError, GetWebhookDeliveryErrors, GetWebhookDeliveryResponse, GetWebhookDeliveryResponses, GetWebhookError, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, ListFoldersData, ListFoldersResponse, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsError, ListNoteCommentsErrors, ListNoteCommentsResponse, ListNoteCommentsResponses, ListNotesData, ListNotesResponse, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponse, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponse, ListTeamNotesResponses, ListTeamsData, ListTeamsResponse, ListTeamsResponses, ListTeamTrashData, ListTeamTrashError, ListTeamTrashErrors, ListTeamTrashResponse, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesError, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponse, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponse, ListTeamWebhooksResponses, ListTrashData, ListTrashResponse, ListTrashResponses, ListVersionsData, ListVersionsError, ListVersionsErrors, ListVersionsResponse, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesError, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponse, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponse, ListWebhooksResponses, NoContentChangeError, Note, NoteImageUploadResponse, NotePermissionRole, NotePublishType, NoteType, NoteVersion, NoteVersionMetadata, NoteVersionUser, PaginatedQuery, PaginatedResponseApiWebhookDelivery, PingTeamWebhookData, PingTeamWebhookError, PingTeamWebhookErrors, PingTeamWebhookResponse, PingTeamWebhookResponses, PingWebhookData, PingWebhookError, PingWebhookErrors, PingWebhookResponse, PingWebhookResponses, RecordStringApiValidationFieldError, ResolveNoteCommentData, ResolveNoteCommentError, ResolveNoteCommentErrors, ResolveNoteCommentResponse, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteError, RestoreNoteErrors, RestoreNoteResponse, RestoreNoteResponses, SimpleUserProfile, SingleNote, SuggestEditPermissionType, Team, TeamVisibilityType, TrashBatchOperationResponse, UnresolveNoteCommentData, UnresolveNoteCommentError, UnresolveNoteCommentErrors, UnresolveNoteCommentResponse, UnresolveNoteCommentResponses, UpdateApiWebhookBody, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderOrderBody, UpdateFolderOrderData, UpdateFolderOrderError, UpdateFolderOrderErrors, UpdateFolderOrderResponse, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponses, UpdateNoteVersionBody, UpdateTeamFolderBody, UpdateTeamFolderData, UpdateTeamFolderError, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderError, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponse, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteError, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookError, UpdateTeamWebhookErrors, UpdateTeamWebhookResponse, UpdateTeamWebhookResponses, UpdateUserFolderBody, UpdateVersionData, UpdateVersionError, UpdateVersionErrors, UpdateVersionResponse, UpdateVersionResponses, UpdateWebhookData, UpdateWebhookData2, UpdateWebhookError, UpdateWebhookErrors, UpdateWebhookResponse, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageError, UploadNoteImageErrors, UploadNoteImageResponse, UploadNoteImageResponses, User, VersionAlreadyNamedError, VersionNotNamedError, WebhookApiErrorResponse, WebhookDeliveryStatusCategory } from './types.gen.js'; diff --git a/nodejs/src/generated/sdk.gen.ts b/nodejs/src/generated/sdk.gen.ts index 36f1694..27c4df4 100644 --- a/nodejs/src/generated/sdk.gen.ts +++ b/nodejs/src/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import { client } from './client.gen.js'; import { type Client, type ClientMeta, formDataBodySerializer, type Options as Options2, type RequestResult, type TDataShape } from './client/index.js'; -import type { BatchRestoreData, BatchRestoreErrors, BatchRestoreResponses, CompareVersionsData, CompareVersionsErrors, CompareVersionsResponses, CreateFolderData, CreateFolderErrors, CreateFolderResponses, CreateNoteData, CreateNoteErrors, CreateNoteResponses, CreateTeamFolderData, CreateTeamFolderErrors, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteErrors, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookErrors, CreateTeamWebhookResponses, CreateVersionData, CreateVersionErrors, CreateVersionResponses, CreateWebhookData2, CreateWebhookErrors, CreateWebhookResponses, DeleteFolderData, DeleteFolderErrors, DeleteFolderResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderErrors, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteErrors, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookErrors, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponses, GetCurrentUserData, GetCurrentUserResponses, GetFolderData, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponses, GetFolderResponses, GetHistoryData, GetHistoryResponses, GetNoteCommentData, GetNoteCommentErrors, GetNoteCommentResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetTeamFolderData, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponses, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteErrors, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponses, GetTeamWebhookErrors, GetTeamWebhookResponses, GetVersionData, GetVersionErrors, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryErrors, GetWebhookDeliveryResponses, GetWebhookErrors, GetWebhookResponses, ListFoldersData, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsErrors, ListNoteCommentsResponses, ListNotesData, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponses, ListTeamsData, ListTeamsResponses, ListTeamTrashData, ListTeamTrashErrors, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponses, ListTrashData, ListTrashResponses, ListVersionsData, ListVersionsErrors, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponses, PingTeamWebhookData, PingTeamWebhookErrors, PingTeamWebhookResponses, PingWebhookData, PingWebhookErrors, PingWebhookResponses, ResolveNoteCommentData, ResolveNoteCommentErrors, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteErrors, RestoreNoteResponses, UnresolveNoteCommentData, UnresolveNoteCommentErrors, UnresolveNoteCommentResponses, UpdateFolderData, UpdateFolderErrors, UpdateFolderOrderData, UpdateFolderOrderErrors, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteErrors, UpdateNoteResponses, UpdateTeamFolderData, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookErrors, UpdateTeamWebhookResponses, UpdateVersionData, UpdateVersionErrors, UpdateVersionResponses, UpdateWebhookData2, UpdateWebhookErrors, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageErrors, UploadNoteImageResponses } from './types.gen.js'; +import type { BatchRestoreData, BatchRestoreErrors, BatchRestoreResponses, CompareVersionsData, CompareVersionsErrors, CompareVersionsResponses, CreateFolderData, CreateFolderErrors, CreateFolderResponses, CreateNoteData, CreateNoteErrors, CreateNoteResponses, CreateTeamFolderData, CreateTeamFolderErrors, CreateTeamFolderResponses, CreateTeamNoteData, CreateTeamNoteErrors, CreateTeamNoteResponses, CreateTeamWebhookData, CreateTeamWebhookErrors, CreateTeamWebhookResponses, CreateVersionData, CreateVersionErrors, CreateVersionResponses, CreateWebhookData2, CreateWebhookErrors, CreateWebhookResponses, DeleteFolderData, DeleteFolderErrors, DeleteFolderResponses, DeleteNoteData, DeleteNoteErrors, DeleteNoteResponses, DeleteTeamFolderData, DeleteTeamFolderErrors, DeleteTeamFolderResponses, DeleteTeamNoteData, DeleteTeamNoteErrors, DeleteTeamNoteResponses, DeleteTeamWebhookData, DeleteTeamWebhookErrors, DeleteTeamWebhookResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, ExportTeamWebhookDeliveriesData, ExportTeamWebhookDeliveriesErrors, ExportTeamWebhookDeliveriesResponses, ExportWebhookDeliveriesData, ExportWebhookDeliveriesErrors, ExportWebhookDeliveriesResponses, GetFolderData, GetFolderErrors, GetFolderOrderData, GetFolderOrderResponses, GetFolderResponses, GetHistoryData, GetHistoryResponses, GetMeData, GetMeResponses, GetNoteCommentData, GetNoteCommentErrors, GetNoteCommentResponses, GetNoteData, GetNoteErrors, GetNoteResponses, GetTeamFolderData, GetTeamFolderErrors, GetTeamFolderOrderData, GetTeamFolderOrderResponses, GetTeamFolderResponses, GetTeamNoteData, GetTeamNoteErrors, GetTeamNoteResponses, GetTeamWebhookData, GetTeamWebhookDeliveryData, GetTeamWebhookDeliveryErrors, GetTeamWebhookDeliveryResponses, GetTeamWebhookErrors, GetTeamWebhookResponses, GetVersionData, GetVersionErrors, GetVersionResponses, GetWebhookData, GetWebhookDeliveryData, GetWebhookDeliveryErrors, GetWebhookDeliveryResponses, GetWebhookErrors, GetWebhookResponses, ListFoldersData, ListFoldersResponses, ListNoteCommentsData, ListNoteCommentsErrors, ListNoteCommentsResponses, ListNotesData, ListNotesResponses, ListTeamFoldersData, ListTeamFoldersResponses, ListTeamNotesData, ListTeamNotesResponses, ListTeamsData, ListTeamsResponses, ListTeamTrashData, ListTeamTrashErrors, ListTeamTrashResponses, ListTeamWebhookDeliveriesData, ListTeamWebhookDeliveriesErrors, ListTeamWebhookDeliveriesResponses, ListTeamWebhooksData, ListTeamWebhooksResponses, ListTrashData, ListTrashResponses, ListVersionsData, ListVersionsErrors, ListVersionsResponses, ListWebhookDeliveriesData, ListWebhookDeliveriesErrors, ListWebhookDeliveriesResponses, ListWebhooksData, ListWebhooksResponses, PingTeamWebhookData, PingTeamWebhookErrors, PingTeamWebhookResponses, PingWebhookData, PingWebhookErrors, PingWebhookResponses, ResolveNoteCommentData, ResolveNoteCommentErrors, ResolveNoteCommentResponses, RestoreNoteData, RestoreNoteErrors, RestoreNoteResponses, UnresolveNoteCommentData, UnresolveNoteCommentErrors, UnresolveNoteCommentResponses, UpdateFolderData, UpdateFolderErrors, UpdateFolderOrderData, UpdateFolderOrderErrors, UpdateFolderOrderResponses, UpdateFolderResponses, UpdateNoteData, UpdateNoteErrors, UpdateNoteResponses, UpdateTeamFolderData, UpdateTeamFolderErrors, UpdateTeamFolderOrderData, UpdateTeamFolderOrderErrors, UpdateTeamFolderOrderResponses, UpdateTeamFolderResponses, UpdateTeamNoteData, UpdateTeamNoteErrors, UpdateTeamNoteResponses, UpdateTeamWebhookData, UpdateTeamWebhookErrors, UpdateTeamWebhookResponses, UpdateVersionData, UpdateVersionErrors, UpdateVersionResponses, UpdateWebhookData2, UpdateWebhookErrors, UpdateWebhookResponses, UploadNoteImageData, UploadNoteImageErrors, UploadNoteImageResponses } from './types.gen.js'; export type Options = Options2 & { /** @@ -672,7 +672,7 @@ export const resolveNoteComment = (options /** * Get the current user's profile */ -export const getCurrentUser = (options?: Options): RequestResult => (options?.client ?? client).get({ +export const getMe = (options?: Options): RequestResult => (options?.client ?? client).get({ responseType: 'json', security: [{ scheme: 'bearer', type: 'http' }], url: '/me', diff --git a/nodejs/src/generated/types.gen.ts b/nodejs/src/generated/types.gen.ts index 1158e6d..0b44208 100644 --- a/nodejs/src/generated/types.gen.ts +++ b/nodejs/src/generated/types.gen.ts @@ -2410,21 +2410,21 @@ export type ResolveNoteCommentResponses = { export type ResolveNoteCommentResponse = ResolveNoteCommentResponses[keyof ResolveNoteCommentResponses]; -export type GetCurrentUserData = { +export type GetMeData = { body?: never; path?: never; query?: never; url: '/me'; }; -export type GetCurrentUserResponses = { +export type GetMeResponses = { /** * Ok */ 200: User; }; -export type GetCurrentUserResponse = GetCurrentUserResponses[keyof GetCurrentUserResponses]; +export type GetMeResponse = GetMeResponses[keyof GetMeResponses]; export type GetHistoryData = { body?: never; diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 59bc39b..3400f0a 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -19,7 +19,7 @@ import { deleteWebhook as generatedDeleteWebhook, exportTeamWebhookDeliveries as generatedExportTeamWebhookDeliveries, exportWebhookDeliveries as generatedExportWebhookDeliveries, - getCurrentUser, + getMe as generatedGetMe, getFolder as generatedGetFolder, getFolderOrder as generatedGetFolderOrder, getHistory as generatedGetHistory, @@ -33,14 +33,14 @@ import { getTeamWebhookDelivery as generatedGetTeamWebhookDelivery, getWebhook as generatedGetWebhook, getWebhookDelivery as generatedGetWebhookDelivery, - listFolders, - listNotes, + listFolders as generatedListFolders, + listNotes as generatedListNotes, listNoteComments as generatedListNoteComments, listVersions as generatedListVersions, listTrash as generatedListTrash, - listTeams, - listTeamFolders, - listTeamNotes, + listTeams as generatedListTeams, + listTeamFolders as generatedListTeamFolders, + listTeamNotes as generatedListTeamNotes, listTeamTrash as generatedListTeamTrash, listTeamWebhooks as generatedListTeamWebhooks, listTeamWebhookDeliveries as generatedListTeamWebhookDeliveries, @@ -289,7 +289,7 @@ export class API { ) } async getMe (options = defaultOption as Opt): Promise> { - return this.unwrapData(getCurrentUser({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + return this.unwrapData(generatedGetMe({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } async getHistory (options = defaultOption as Opt): Promise> { @@ -300,8 +300,13 @@ export class API { }), options.unwrapData) as unknown as OptionReturnType } - async getNoteList (options = defaultOption as Opt): Promise> { - return this.unwrapData(listNotes({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + async listNotes (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListNotes({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + } + + /** Alias of listNotes; kept for backward compatibility. */ + getNoteList (options = defaultOption as Opt): Promise> { + return this.listNotes(options) } async getNote (noteId: string): Promise @@ -368,8 +373,13 @@ export class API { ) as unknown as OptionReturnType } - async getTeams (options = defaultOption as Opt): Promise> { - return this.unwrapData(listTeams({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + async listTeams (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeams({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + } + + /** Alias of listTeams; kept for backward compatibility. */ + getTeams (options = defaultOption as Opt): Promise> { + return this.listTeams(options) } async getTeamNote (teamPath: string, noteId: string): Promise @@ -390,14 +400,19 @@ export class API { return this.unwrapData(request, options.unwrapData, true) as unknown as Promise } - async getTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(listTeamNotes({ + async listTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeamNotes({ client: this.generatedClient, path: { teampath: teamPath }, throwOnError: true, }), options.unwrapData) as unknown as OptionReturnType } + /** Alias of listTeamNotes; kept for backward compatibility. */ + getTeamNotes (teamPath: string, options = defaultOption as Opt): Promise> { + return this.listTeamNotes(teamPath, options) + } + async createTeamNote (teamPath: string, payload: CreateNoteOptions, options = defaultOption as Opt): Promise> { return this.unwrapData(generatedCreateTeamNote({ client: this.generatedClient, @@ -433,8 +448,13 @@ export class API { }) } - async getFolderList (options = defaultOption as Opt): Promise> { - return this.unwrapData(listFolders({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + async listFolders (options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListFolders({ client: this.generatedClient, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType + } + + /** Alias of listFolders; kept for backward compatibility. */ + getFolderList (options = defaultOption as Opt): Promise> { + return this.listFolders(options) } async createFolder (payload: CreateUserFolderBody, options = defaultOption as Opt): Promise> { @@ -474,14 +494,19 @@ export class API { return this.unwrapData(generatedUpdateFolderOrder({ client: this.generatedClient, body: payload, throwOnError: true }), options.unwrapData) as unknown as OptionReturnType } - async getTeamFolderList (teamPath: string, options = defaultOption as Opt): Promise> { - return this.unwrapData(listTeamFolders({ + async listTeamFolders (teamPath: string, options = defaultOption as Opt): Promise> { + return this.unwrapData(generatedListTeamFolders({ client: this.generatedClient, path: { teampath: teamPath }, throwOnError: true, }), options.unwrapData) as unknown as OptionReturnType } + /** Alias of listTeamFolders; kept for backward compatibility. */ + getTeamFolderList (teamPath: string, options = defaultOption as Opt): Promise> { + return this.listTeamFolders(teamPath, options) + } + async createTeamFolder (teamPath: string, payload: CreateTeamFolderBody, options = defaultOption as Opt): Promise> { return this.unwrapData(generatedCreateTeamFolder({ client: this.generatedClient, diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index 38593bb..ee1c43e 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -74,9 +74,11 @@ test('getTeams keeps the generated team shape and legacy response forms', async })) expect(await client.getTeams()).toEqual(teams) + expect(await client.listTeams()).toEqual(teams) const raw = await client.getTeams({ unwrapData: false }) expect(raw.status).toBe(200) expect(raw.data).toEqual(teams) + expect((await client.listTeams({ unwrapData: false })).data).toEqual(teams) expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) }) @@ -103,9 +105,11 @@ test('getNoteList keeps the legacy response shapes and authorization', async () ) expect(await client.getNoteList()).toEqual([note]) + expect(await client.listNotes()).toEqual([note]) const raw = await client.getNoteList({ unwrapData: false }) expect(raw.status).toBe(200) expect(raw.data).toEqual([note]) + expect((await client.listNotes({ unwrapData: false })).data).toEqual([note]) expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) }) @@ -168,9 +172,11 @@ test('getTeamNotes keeps the legacy response shapes and team path', async () => ) expect(await client.getTeamNotes('test-team')).toEqual(notes) + expect(await client.listTeamNotes('test-team')).toEqual(notes) const raw = await client.getTeamNotes('test-team', { unwrapData: false }) expect(raw.status).toBe(200) expect(raw.data).toEqual(notes) + expect((await client.listTeamNotes('test-team', { unwrapData: false })).data).toEqual(notes) expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) }) @@ -269,12 +275,14 @@ test('getFolderList returns folders from /folders', async () => { ) const folders = await client.getFolderList() + expect(await client.listFolders()).toEqual(folders) expect(folders).toHaveLength(1) expect(folders[0]).toMatchObject({ id: 'folder-1', name: 'Research' }) const raw = await client.getFolderList({ unwrapData: false }) expect(raw.status).toBe(200) expect(raw.data).toEqual(folders) + expect((await client.listFolders({ unwrapData: false })).data).toEqual(folders) expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) }) @@ -312,9 +320,11 @@ test('getTeamFolderList keeps the legacy response shapes and team path', async ( ) expect(await client.getTeamFolderList('test-team')).toEqual(folders) + expect(await client.listTeamFolders('test-team')).toEqual(folders) const raw = await client.getTeamFolderList('test-team', { unwrapData: false }) expect(raw.status).toBe(200) expect(raw.data).toEqual(folders) + expect((await client.listTeamFolders('test-team', { unwrapData: false })).data).toEqual(folders) expect(authorization).toBe(`Bearer ${process.env.HACKMD_ACCESS_TOKEN}`) }) diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index a5b7a6b..54d07f8 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -1,7 +1,8 @@ import { http, HttpResponse } from 'msw' +import { API } from '../src' import * as generatedSdk from '../src/generated/sdk.gen' -import { createClient, getCurrentUser } from '../src/raw' +import { createClient, getMe } from '../src/raw' import * as openApiDocument from '../spec/hackmd-openapi.json' import { server } from './mock' @@ -23,7 +24,7 @@ const EXPECTED_OPERATIONS = [ 'deleteWebhook', 'exportTeamWebhookDeliveries', 'exportWebhookDeliveries', - 'getCurrentUser', + 'getMe', 'getFolder', 'getFolderOrder', 'getHistory', @@ -99,6 +100,12 @@ describe('generated raw API', () => { expect(Object.keys(generatedSdk).sort()).toEqual(expected) }) + test('the compatibility API covers every raw operation', () => { + const apiMethods = new Set(Object.getOwnPropertyNames(API.prototype)) + const missing = operationNamesFromSpec().filter(name => !apiMethods.has(name)) + expect(missing).toEqual([]) + }) + test('detail GETs have only the documented 200 success response', () => { const paths = openApiDocument.paths as Record for (const path of [ @@ -139,7 +146,7 @@ describe('generated raw API', () => { baseURL: 'https://api.hackmd.io/v1', }) - const response = await getCurrentUser({ client, throwOnError: true }) + const response = await getMe({ client, throwOnError: true }) expect(authorization).toBe('Bearer test-token') expect(response.status).toBe(200) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 91653ea..1f8265f 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -33,7 +33,11 @@ async function exploreConditionalTeamNote (client: API): Promise { - const notes = await client.getNoteList() + const notes = await client.listNotes() + const legacy = await client.getNoteList() + const raw = await client.listNotes({ unwrapData: false }) + void legacy + void raw.status return notes[0].createdAt } @@ -43,17 +47,20 @@ async function exploreHistory (client: API): Promise { } async function exploreTeamNotes (client: API): Promise { - const notes = await client.getTeamNotes('TEAM_PATH') + const notes = await client.listTeamNotes('TEAM_PATH') + void await client.getTeamNotes('TEAM_PATH') return notes[0].createdAt } async function exploreFolders (client: API): Promise { - const folders = await client.getFolderList() + const folders = await client.listFolders() + void await client.getFolderList() return folders[0].createdAt } async function exploreTeamFolders (client: API): Promise { - const folders = await client.getTeamFolderList('TEAM_PATH') + const folders = await client.listTeamFolders('TEAM_PATH') + void await client.getTeamFolderList('TEAM_PATH') return folders[0].updatedAt } @@ -104,7 +111,8 @@ async function exploreNoteCreation (client: API): Promise { async function exploreProfileAndTeams (client: API): Promise { const profile = await client.getMe() - const teams = await client.getTeams() + const teams = await client.listTeams() + void await client.getTeams() const createdAt: number = teams[0].createdAt const description: string | null = profile.teams[0].description const upgraded: boolean = profile.upgraded From fb0ea22825f38aee967dbea7400f31975976e266 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 01:32:01 +0800 Subject: [PATCH 26/31] fix: derive public enum types from OpenAPI contract --- nodejs/src/index.ts | 12 +++---- nodejs/src/type.ts | 57 ++++++++++++++++++------------- nodejs/tests/generated.spec.ts | 12 ++++++- nodejs/tests/types/playground.mts | 11 ++++++ 4 files changed, 61 insertions(+), 31 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 3400f0a..e5919d4 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -422,30 +422,30 @@ export class API { }), options.unwrapData) as unknown as CreateTeamNoteReturnType } - async updateTeamNoteContent (teamPath: string, noteId: string, content?: string): Promise { + async updateTeamNoteContent (teamPath: string, noteId: string, content?: string): Promise & { status: 202 }> { return generatedUpdateTeamNote({ client: this.generatedClient, path: { teampath: teamPath, noteId }, body: { content }, throwOnError: true, - }) + }) as Promise & { status: 202 }> } - async updateTeamNote (teamPath: string, noteId: string, options: UpdateNoteOptions): Promise { + async updateTeamNote (teamPath: string, noteId: string, options: UpdateNoteOptions): Promise & { status: 202 }> { return generatedUpdateTeamNote({ client: this.generatedClient, path: { teampath: teamPath, noteId }, body: options, throwOnError: true, - }) + }) as Promise & { status: 202 }> } - async deleteTeamNote (teamPath: string, noteId: string): Promise { + async deleteTeamNote (teamPath: string, noteId: string): Promise & { status: 204 }> { return generatedDeleteTeamNote({ client: this.generatedClient, path: { teampath: teamPath, noteId }, throwOnError: true, - }) + }) as Promise & { status: 204 }> } async listFolders (options = defaultOption as Opt): Promise> { diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 3acbd6b..36b7c72 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -9,6 +9,7 @@ import type { BatchRestoreTrashBody as GeneratedBatchRestoreTrashBody, CompareNoteVersions as GeneratedCompareNoteVersions, CompareVersionsData as GeneratedCompareVersionsData, + CommentPermissionType as GeneratedCommentPermissionType, CreateNoteVersionBody as GeneratedCreateNoteVersionBody, CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, @@ -23,11 +24,14 @@ import type { NoteVersion as GeneratedNoteVersion, NoteVersionMetadata as GeneratedNoteVersionMetadata, NoteType as GeneratedNote, + NotePermissionRole as GeneratedNotePermissionRole, + NotePublishType as GeneratedNotePublishType, NoteImageUploadResponse as GeneratedNoteImageUploadResponse, PaginatedResponseApiWebhookDelivery as GeneratedWebhookDeliveryPage, SimpleUserProfile as GeneratedSimpleUserProfile, SingleNote as GeneratedSingleNote, Team as GeneratedTeam, + TeamVisibilityType as GeneratedTeamVisibilityType, TrashBatchOperationResponse as GeneratedTrashBatchOperationResponse, UpdateNoteData as GeneratedUpdateNoteData, UpdateNoteVersionBody as GeneratedUpdateNoteVersionBody, @@ -35,25 +39,29 @@ import type { User as GeneratedUser, } from './generated/types.gen.js' -export enum TeamVisibilityType { - PUBLIC = 'public', - PRIVATE = 'private', -} - -export enum NotePublishType { - EDIT = 'edit', - VIEW = 'view', - SLIDE = 'slide', - BOOK = 'book' -} - -export enum CommentPermissionType { - DISABLED = 'disabled', - FORBIDDEN = 'forbidden', - OWNERS = 'owners', - SIGNED_IN_USERS = 'signed_in_users', - EVERYONE = 'everyone' -} +/** Preserve the runtime enum-like values while deriving their types from OpenAPI. */ +export const TeamVisibilityType = { + PUBLIC: 'public', + PRIVATE: 'private', +} as const satisfies Record, GeneratedTeamVisibilityType> +export type TeamVisibilityType = GeneratedTeamVisibilityType + +export const NotePublishType = { + EDIT: 'edit', + VIEW: 'view', + SLIDE: 'slide', + BOOK: 'book', +} as const satisfies Record, GeneratedNotePublishType> +export type NotePublishType = GeneratedNotePublishType + +export const CommentPermissionType = { + DISABLED: 'disabled', + FORBIDDEN: 'forbidden', + OWNERS: 'owners', + SIGNED_IN_USERS: 'signed_in_users', + EVERYONE: 'everyone', +} as const satisfies Record, GeneratedCommentPermissionType> +export type CommentPermissionType = GeneratedCommentPermissionType export type CreateNoteOptions = Exclude, string> export type CreateNoteMultiStatusResponse = GeneratedCreateNoteMultiStatusResponse @@ -63,11 +71,12 @@ export type User = GeneratedUser export type SimpleUserProfile = GeneratedSimpleUserProfile -export enum NotePermissionRole { - OWNER = 'owner', - SIGNED_IN = 'signed_in', - GUEST = 'guest' -} +export const NotePermissionRole = { + OWNER: 'owner', + SIGNED_IN: 'signed_in', + GUEST: 'guest', +} as const satisfies Record, GeneratedNotePermissionRole> +export type NotePermissionRole = GeneratedNotePermissionRole /** Note response types are generated from the v1 OpenAPI contract. */ export type FolderPath = GeneratedFolderPath diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index 54d07f8..4b27fe7 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -1,6 +1,6 @@ import { http, HttpResponse } from 'msw' -import { API } from '../src' +import { API, CommentPermissionType, NotePermissionRole, NotePublishType, TeamVisibilityType } from '../src' import * as generatedSdk from '../src/generated/sdk.gen' import { createClient, getMe } from '../src/raw' import * as openApiDocument from '../spec/hackmd-openapi.json' @@ -100,6 +100,16 @@ describe('generated raw API', () => { expect(Object.keys(generatedSdk).sort()).toEqual(expected) }) + test('keeps the existing runtime enum values', () => { + expect(TeamVisibilityType).toEqual({ PUBLIC: 'public', PRIVATE: 'private' }) + expect(NotePublishType).toEqual({ EDIT: 'edit', VIEW: 'view', SLIDE: 'slide', BOOK: 'book' }) + expect(CommentPermissionType).toEqual({ + DISABLED: 'disabled', FORBIDDEN: 'forbidden', OWNERS: 'owners', + SIGNED_IN_USERS: 'signed_in_users', EVERYONE: 'everyone', + }) + expect(NotePermissionRole).toEqual({ OWNER: 'owner', SIGNED_IN: 'signed_in', GUEST: 'guest' }) + }) + test('the compatibility API covers every raw operation', () => { const apiMethods = new Set(Object.getOwnPropertyNames(API.prototype)) const missing = operationNamesFromSpec().filter(name => !apiMethods.has(name)) diff --git a/nodejs/tests/types/playground.mts b/nodejs/tests/types/playground.mts index 1f8265f..437e250 100644 --- a/nodejs/tests/types/playground.mts +++ b/nodejs/tests/types/playground.mts @@ -94,9 +94,20 @@ async function exploreNoteMutations (client: API): Promise { const raw = await client.updateNoteContent('NOTE_ID', 'Updated', { unwrapData: false }) const rawStatus: 202 = raw.status const deleted: void = await client.deleteNote('NOTE_ID') + const teamUpdated = await client.updateTeamNote('TEAM_PATH', 'NOTE_ID', { title: 'Updated' }) + const teamContentUpdated = await client.updateTeamNoteContent('TEAM_PATH', 'NOTE_ID', 'Updated') + const teamDeleted = await client.deleteTeamNote('TEAM_PATH', 'NOTE_ID') + const teamUpdateStatus: 202 = teamUpdated.status + const teamContentStatus: 202 = teamContentUpdated.status + const teamDeleteStatus: 204 = teamDeleted.status + // @ts-expect-error A successful team note update has no documented body. + teamUpdated.data.content void status void rawStatus void deleted + void teamUpdateStatus + void teamContentStatus + void teamDeleteStatus } async function exploreNoteCreation (client: API): Promise { From 734bc84a7bda32985422679669a8fbd68fb927ec Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 02:08:30 +0800 Subject: [PATCH 27/31] fix: isolate retries per API request --- nodejs/src/index.ts | 17 +++++----- nodejs/tests/api.spec.ts | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index e5919d4..0068142 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -266,24 +266,23 @@ export class API { } private createRetryInterceptor (axiosInstance: AxiosInstance, maxRetries: number, baseDelay: number): void { - let retryCount = 0 - axiosInstance.interceptors.response.use( response => response, - async error => { - if (retryCount < maxRetries && this.isRetryableError(error)) { + async (error: AxiosError) => { + const config = error.config as (InternalAxiosRequestConfig & { __hackmdRetryCount?: number }) | undefined + const retryCount = config?.__hackmdRetryCount ?? 0 + if (config && retryCount < maxRetries && this.isRetryableError(error)) { const remainingCredits = parseInt(error.response?.headers['x-ratelimit-userremaining'], 10) if (isNaN(remainingCredits) || remainingCredits > 0) { - retryCount++ - const delay = this.exponentialBackoff(retryCount, baseDelay) - console.warn(`Retrying request... attempt #${retryCount} after delay of ${delay}ms`) + config.__hackmdRetryCount = retryCount + 1 + const delay = this.exponentialBackoff(config.__hackmdRetryCount, baseDelay) + console.warn(`Retrying request... attempt #${config.__hackmdRetryCount} after delay of ${delay}ms`) await new Promise(resolve => setTimeout(resolve, delay)) - return axiosInstance(error.config) + return axiosInstance(config) } } - retryCount = 0 // Reset retry count after a successful request or when not retrying return Promise.reject(error) } ) diff --git a/nodejs/tests/api.spec.ts b/nodejs/tests/api.spec.ts index ee1c43e..0e90cd4 100644 --- a/nodejs/tests/api.spec.ts +++ b/nodejs/tests/api.spec.ts @@ -692,6 +692,74 @@ test.each([ }))) }) +test('retries separate requests independently on the same client', async () => { + const attempts = new Map() + server.use(http.get('https://api.hackmd.io/v1/notes/:noteId', ({ params }) => { + const noteId = String(params.noteId) + const count = (attempts.get(noteId) ?? 0) + 1 + attempts.set(noteId, count) + if (count === 1) return HttpResponse.json({}, { status: 503 }) + return HttpResponse.json({ id: noteId }) + })) + + const retryClient = new API('test-token', undefined, { + wrapResponseErrors: true, + retryConfig: { maxRetries: 1, baseDelay: 0 }, + }) + + expect((await retryClient.getNote('first')).id).toBe('first') + expect((await retryClient.getNote('second')).id).toBe('second') + expect([...attempts]).toEqual([['first', 2], ['second', 2]]) +}) + +test('retries concurrent requests independently', async () => { + const attempts = new Map() + let initialRequests = 0 + let releaseInitialRequests!: () => void + const bothStarted = new Promise(resolve => { releaseInitialRequests = resolve }) + + server.use(http.get('https://api.hackmd.io/v1/notes/:noteId', async ({ params }) => { + const noteId = String(params.noteId) + const count = (attempts.get(noteId) ?? 0) + 1 + attempts.set(noteId, count) + if (count === 1) { + initialRequests++ + if (initialRequests === 2) releaseInitialRequests() + await bothStarted + return HttpResponse.json({}, { status: 503 }) + } + return HttpResponse.json({ id: noteId }) + })) + + const retryClient = new API('test-token', undefined, { + wrapResponseErrors: true, + retryConfig: { maxRetries: 1, baseDelay: 0 }, + }) + + const results = await Promise.allSettled([ + retryClient.getNote('first'), retryClient.getNote('second'), + ]) + expect(results.map(result => result.status)).toEqual(['fulfilled', 'fulfilled']) + expect([...attempts]).toEqual([['first', 2], ['second', 2]]) +}) + +test('stops retrying after the per-request limit', async () => { + let attempts = 0 + server.use(http.get('https://api.hackmd.io/v1/notes/retry-limit', () => { + attempts++ + if (attempts <= 2) return HttpResponse.json({}, { status: 503 }) + return HttpResponse.json({ id: 'retry-limit' }) + })) + + const retryClient = new API('test-token', undefined, { + wrapResponseErrors: true, + retryConfig: { maxRetries: 1, baseDelay: 0 }, + }) + + await expect(retryClient.getNote('retry-limit')).rejects.toBeInstanceOf(InternalServerError) + expect(attempts).toBe(2) +}) + test('should not retry non-idempotent requests when wrapping errors', async () => { let requestCount = 0 const clientWithRetryAndWrap = new API(process.env.HACKMD_ACCESS_TOKEN!, undefined, { From fd6122cb6f8b0beeb9a9f1f41e885b8968cd5d1d Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 02:09:37 +0800 Subject: [PATCH 28/31] refactor: derive folder DTOs from OpenAPI types --- nodejs/src/type.ts | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/nodejs/src/type.ts b/nodejs/src/type.ts index 36b7c72..60f4b7f 100644 --- a/nodejs/src/type.ts +++ b/nodejs/src/type.ts @@ -3,6 +3,7 @@ import type { ApiCommentDetail as GeneratedApiCommentDetail, ApiCommentResolutionResponse as GeneratedApiCommentResolutionResponse, ApiFolder as GeneratedApiFolder, + ApiFolderOrder as GeneratedApiFolderOrder, ApiWebhook as GeneratedApiWebhook, ApiWebhookDelivery as GeneratedApiWebhookDelivery, ApiTrashNote as GeneratedApiTrashNote, @@ -10,6 +11,8 @@ import type { CompareNoteVersions as GeneratedCompareNoteVersions, CompareVersionsData as GeneratedCompareVersionsData, CommentPermissionType as GeneratedCommentPermissionType, + CreateTeamFolderBody as GeneratedCreateTeamFolderBody, + CreateUserFolderBody as GeneratedCreateUserFolderBody, CreateNoteVersionBody as GeneratedCreateNoteVersionBody, CreateApiWebhookBody as GeneratedCreateApiWebhookBody, CreateNoteData as GeneratedCreateNoteData, @@ -35,6 +38,9 @@ import type { TrashBatchOperationResponse as GeneratedTrashBatchOperationResponse, UpdateNoteData as GeneratedUpdateNoteData, UpdateNoteVersionBody as GeneratedUpdateNoteVersionBody, + UpdateFolderOrderBody as GeneratedUpdateFolderOrderBody, + UpdateTeamFolderBody as GeneratedUpdateTeamFolderBody, + UpdateUserFolderBody as GeneratedUpdateUserFolderBody, UpdateWebhookData as GeneratedUpdateWebhookData, User as GeneratedUser, } from './generated/types.gen.js' @@ -116,31 +122,12 @@ export type DeleteTeamNote = void export type ApiFolder = GeneratedApiFolder /** Maps each parent folder id or the literal `root` to ordered child folder ids. */ -export type ApiFolderOrder = Record - -export type CreateUserFolderBody = { - name?: string - description?: string - icon?: string - color?: string - parentFolderId?: string -} - -export type UpdateUserFolderBody = { - name?: string - description?: string | null - icon?: string | null - color?: string | null - parentFolderId?: string | null -} - -export type CreateTeamFolderBody = CreateUserFolderBody - -export type UpdateTeamFolderBody = UpdateUserFolderBody - -export type UpdateFolderOrderBody = { - order: ApiFolderOrder -} +export type ApiFolderOrder = GeneratedApiFolderOrder +export type CreateUserFolderBody = GeneratedCreateUserFolderBody +export type UpdateUserFolderBody = GeneratedUpdateUserFolderBody +export type CreateTeamFolderBody = GeneratedCreateTeamFolderBody +export type UpdateTeamFolderBody = GeneratedUpdateTeamFolderBody +export type UpdateFolderOrderBody = GeneratedUpdateFolderOrderBody export type GetFolders = ApiFolder[] export type GetTeamFolders = ApiFolder[] From 774d7ff64562f7ce997af8b76f59b644a0626536 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Fri, 25 Sep 2026 17:13:28 +0800 Subject: [PATCH 29/31] feat: generate OpenAPI operation registry --- nodejs/README.md | 7 +- nodejs/openapi-ts.config.ts | 2 +- .../scripts/generate-operation-registry.mjs | 80 +++++++++++++++++ nodejs/src/generated/operationRegistry.gen.ts | 66 ++++++++++++++ nodejs/src/raw.ts | 2 + nodejs/tests/generated.spec.ts | 89 +++++-------------- 6 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 nodejs/scripts/generate-operation-registry.mjs create mode 100644 nodejs/src/generated/operationRegistry.gen.ts diff --git a/nodejs/README.md b/nodejs/README.md index e92792d..28ba00b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -87,6 +87,7 @@ const client = new HackMDAPI('YOUR_ACCESS_TOKEN', 'https://api.hackmd.io/v1', { ``` The client will automatically retry requests that fail with: + - 5xx server errors - 429 Too Many Requests errors - Network errors @@ -248,12 +249,12 @@ deterministic. Integration tests call a real HackMD API (staging or production). They are **not** run by `pnpm test` or the default CI job. -**Requirements** +## Requirements - `HACKMD_ACCESS_TOKEN` — a valid personal access token for the environment you target. - Optional: `HACKMD_API_ENDPOINT` — defaults to `https://api.hackmd.io/v1`. For staging, use `https://api-stage.hackmd.io/v1`. -**Read-only (default e2e)** +## Read-only (default e2e) ```bash cd nodejs @@ -262,7 +263,7 @@ export HACKMD_API_ENDPOINT=https://api-stage.hackmd.io/v1 # optional pnpm test:e2e ``` -**With CRUD / mutations** +## With CRUD / mutations Set `HACKMD_E2E_MUTATIONS=1` to run write tests against your account: diff --git a/nodejs/openapi-ts.config.ts b/nodejs/openapi-ts.config.ts index 50d2f30..abcb3c3 100644 --- a/nodejs/openapi-ts.config.ts +++ b/nodejs/openapi-ts.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ output: { path: './src/generated', module: { extension: '.js' }, - postProcess: [], + postProcess: [{ command: 'node', args: ['scripts/generate-operation-registry.mjs'] }], }, plugins: [ '@hey-api/client-axios', diff --git a/nodejs/scripts/generate-operation-registry.mjs b/nodejs/scripts/generate-operation-registry.mjs new file mode 100644 index 0000000..5bcd1c1 --- /dev/null +++ b/nodejs/scripts/generate-operation-registry.mjs @@ -0,0 +1,80 @@ +import { readFile, writeFile } from 'node:fs/promises' + +const root = new URL('../', import.meta.url) +const spec = JSON.parse(await readFile(new URL('spec/hackmd-openapi.json', root), 'utf8')) +const raw = await readFile(new URL('src/generated/sdk.gen.ts', root), 'utf8') +const rawFunctions = new Set([...raw.matchAll(/^export const (\w+)\s*=/gm)].map(match => match[1])) +const methods = new Set(['get', 'post', 'put', 'patch', 'delete']) +const operations = [] +const ids = new Set() + +function resolve (value) { + if (!value?.$ref) return value + const prefix = '#/' + if (!value.$ref.startsWith(prefix)) throw new Error(`Unsupported reference: ${value.$ref}`) + return value.$ref.slice(prefix.length).split('/').reduce((object, key) => object?.[key.replace(/~1/g, '/').replace(/~0/g, '~')], spec) +} + +function parameter (value) { + const entry = resolve(value) + const schema = resolve(entry.schema) + return { + name: entry.name, + in: entry.in, + required: entry.required === true || entry.in === 'path', + type: schema?.type ?? (schema?.enum ? 'string' : 'unknown'), + ...(schema?.enum ? { enum: schema.enum } : {}), + ...(entry.description ? { description: entry.description } : {}), + } +} + +for (const [path, item] of Object.entries(spec.paths)) { + for (const [method, operation] of Object.entries(item)) { + if (!methods.has(method)) continue + const id = operation.operationId + if (!id || ids.has(id)) throw new Error(`Missing or duplicate operationId: ${id} (${method} ${path})`) + ids.add(id) + const functionName = id[0].toLowerCase() + id.slice(1) + if (!rawFunctions.has(functionName)) throw new Error(`No generated raw function for ${id}: ${functionName}`) + const parameters = [...(item.parameters ?? []), ...(operation.parameters ?? [])].map(parameter) + const body = resolve(operation.requestBody) + const bodyContent = body?.content ?? {} + const requestBody = body ? { + required: body.required === true, + contentTypes: Object.keys(bodyContent), + binaryFields: Object.values(bodyContent).flatMap(media => { + const schema = resolve(media.schema) + return Object.entries(schema?.properties ?? {}) + .filter(([, field]) => { const resolved = resolve(field); return resolved?.type === 'string' && resolved?.format === 'binary' }) + .map(([name]) => ({ name, required: schema.required?.includes(name) === true })) + }), + } : undefined + const responses = Object.fromEntries(Object.entries(operation.responses ?? {}).map(([status, response]) => [ + status, + Object.keys(resolve(response)?.content ?? {}), + ])) + operations.push({ id, functionName, metadata: { + method: method.toUpperCase(), + path, + description: operation.description ?? operation.summary ?? '', + parameters, + ...(requestBody ? { requestBody } : {}), + responses, + } }) + } +} + +operations.sort((a, b) => a.id.localeCompare(b.id)) +const lines = [ + '// Generated from spec/hackmd-openapi.json. Do not edit by hand.', + "import * as sdk from './sdk.gen.js'", + '', + 'export const operationRegistry = {', + ...operations.map(({ id, functionName, metadata }) => + ` ${JSON.stringify(id)}: { ...${JSON.stringify(metadata)}, call: sdk.${functionName} },`), + '} as const', + '', + 'export type OperationId = keyof typeof operationRegistry', + '', +] +await writeFile(new URL('src/generated/operationRegistry.gen.ts', root), lines.join('\n')) diff --git a/nodejs/src/generated/operationRegistry.gen.ts b/nodejs/src/generated/operationRegistry.gen.ts new file mode 100644 index 0000000..45e417a --- /dev/null +++ b/nodejs/src/generated/operationRegistry.gen.ts @@ -0,0 +1,66 @@ +// Generated from spec/hackmd-openapi.json. Do not edit by hand. +import * as sdk from './sdk.gen.js' + +export const operationRegistry = { + "BatchRestore": { ...{"method":"PUT","path":"/trash/batch-restore","description":"Restore multiple notes from trash in one request.","parameters":[],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"200":["application/json"],"207":["application/json"],"404":["application/json"]}}, call: sdk.batchRestore }, + "CompareVersions": { ...{"method":"GET","path":"/notes/{noteId}/versions/compare","description":"Compare two saved versions or a saved version with live note content.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"base","in":"query","required":true,"type":"string","description":"Required comparison source. Use note_content or version:{id}."},{"name":"target","in":"query","required":true,"type":"string","description":"Required comparison target. Use note_content or version:{id}."}],"responses":{"200":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"],"422":["application/json"]}}, call: sdk.compareVersions }, + "CreateFolder": { ...{"method":"POST","path":"/folders","description":"Create a new folder in the current user's workspace","parameters":[],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.createFolder }, + "CreateNote": { ...{"method":"POST","path":"/notes","description":"Create a new note for the current user","parameters":[],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"207":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"],"422":["application/json"]}}, call: sdk.createNote }, + "CreateTeamFolder": { ...{"method":"POST","path":"/teams/{teampath}/folders","description":"Create a new folder in a team workspace","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."}],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.createTeamFolder }, + "CreateTeamNote": { ...{"method":"POST","path":"/teams/{teampath}/notes","description":"Create a new note for a team","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."}],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"207":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"],"422":["application/json"]}}, call: sdk.createTeamNote }, + "CreateTeamWebhook": { ...{"method":"POST","path":"/teams/{teampath}/webhooks","description":"Create a webhook in a team workspace.\nThe signing secret is returned only once and cannot be retrieved later.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"],"429":["application/json"]}}, call: sdk.createTeamWebhook }, + "CreateVersion": { ...{"method":"POST","path":"/notes/{noteId}/versions","description":"Create a named version from live note content or an existing saved version. When\nlive note content has changed, this creates and names a new saved version. When\nlive content matches the latest system version, that version is named in place and\nreturned with 201. When it matches the latest user-named version, the response is\n409 `no_content_change`. Naming an already named `version:{id}` returns 409\n`version_already_named`.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"]}}, call: sdk.createVersion }, + "CreateWebhook": { ...{"method":"POST","path":"/webhooks","description":"Create a webhook in the current user's workspace.\nThe signing secret is returned only once and cannot be retrieved later.","parameters":[],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"201":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"],"429":["application/json"]}}, call: sdk.createWebhook }, + "DeleteFolder": { ...{"method":"DELETE","path":"/folders/{folderId}","description":"Delete a folder in the current user's workspace","parameters":[{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to delete."}],"responses":{"204":[],"400":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.deleteFolder }, + "DeleteNote": { ...{"method":"DELETE","path":"/notes/{noteId}","description":"Delete a note for the current user","parameters":[{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to delete."}],"responses":{"204":[],"400":["application/json"],"403":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.deleteNote }, + "DeleteTeamFolder": { ...{"method":"DELETE","path":"/teams/{teampath}/folders/{folderId}","description":"Delete a folder in a team workspace","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to delete."}],"responses":{"204":[],"400":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.deleteTeamFolder }, + "DeleteTeamNote": { ...{"method":"DELETE","path":"/teams/{teampath}/notes/{noteId}","description":"Delete a team note","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to delete."}],"responses":{"204":[],"400":["application/json"],"403":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.deleteTeamNote }, + "DeleteTeamWebhook": { ...{"method":"DELETE","path":"/teams/{teampath}/webhooks/{hookId}","description":"Delete a team webhook.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"204":[],"400":["application/json"],"404":["application/json"]}}, call: sdk.deleteTeamWebhook }, + "DeleteWebhook": { ...{"method":"DELETE","path":"/webhooks/{hookId}","description":"Delete a webhook.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"204":[],"400":["application/json"],"404":["application/json"]}}, call: sdk.deleteWebhook }, + "ExportTeamWebhookDeliveries": { ...{"method":"GET","path":"/teams/{teampath}/webhooks/{hookId}/deliveries/export","description":"Download team webhook deliveries as newline-delimited JSON.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/x-ndjson"],"400":["application/json"],"404":["application/json"]}}, call: sdk.exportTeamWebhookDeliveries }, + "ExportWebhookDeliveries": { ...{"method":"GET","path":"/webhooks/{hookId}/deliveries/export","description":"Download webhook deliveries as newline-delimited JSON.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/x-ndjson"],"400":["application/json"],"404":["application/json"]}}, call: sdk.exportWebhookDeliveries }, + "GetFolder": { ...{"method":"GET","path":"/folders/{folderId}","description":"Get a single folder in the current user's workspace","parameters":[{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to retrieve."}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getFolder }, + "GetFolderOrder": { ...{"method":"GET","path":"/folders/folder-order","description":"Get your personal folder ordering for this workspace (parent folder id or `root` → ordered child folder ids).\nUses the same folder UUIDs as (not internal Yjs clientIds).","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.getFolderOrder }, + "GetHistory": { ...{"method":"GET","path":"/history","description":"Get note history for the current user","parameters":[{"name":"limit","in":"query","required":false,"type":"number","description":"The maximum number of history items to return."}],"responses":{"200":["application/json"]}}, call: sdk.getHistory }, + "GetMe": { ...{"method":"GET","path":"/me","description":"Get the current user's profile","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.getMe }, + "GetNote": { ...{"method":"GET","path":"/notes/{noteId}","description":"Get a single note for the current user (or team note if accessible)","parameters":[{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to retrieve."}],"responses":{"200":["application/json"],"304":[],"403":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.getNote }, + "GetNoteComment": { ...{"method":"GET","path":"/notes/{noteId}/comments/{commentId}","description":"Get one comment or reply from a note.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"commentId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"403":["application/json"],"404":["application/json"]}}, call: sdk.getNoteComment }, + "GetTeamFolder": { ...{"method":"GET","path":"/teams/{teampath}/folders/{folderId}","description":"Get a single folder in a team workspace","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to retrieve."}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getTeamFolder }, + "GetTeamFolderOrder": { ...{"method":"GET","path":"/teams/{teampath}/folders/folder-order","description":"Get your personal folder ordering for this team workspace (parent folder id or `root` → ordered child folder ids).\nUses the same folder UUIDs as (not internal Yjs clientIds).","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"]}}, call: sdk.getTeamFolderOrder }, + "GetTeamNote": { ...{"method":"GET","path":"/teams/{teampath}/notes/{noteId}","description":"Get a single note for a team","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to retrieve."}],"responses":{"200":["application/json"],"304":[],"403":["application/json"],"404":["application/json"]}}, call: sdk.getTeamNote }, + "GetTeamWebhook": { ...{"method":"GET","path":"/teams/{teampath}/webhooks/{hookId}","description":"Get a webhook in a team workspace.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getTeamWebhook }, + "GetTeamWebhookDelivery": { ...{"method":"GET","path":"/teams/{teampath}/webhooks/{hookId}/deliveries/{deliveryId}","description":"Get a team webhook delivery within the plan's retention window.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"},{"name":"deliveryId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getTeamWebhookDelivery }, + "GetVersion": { ...{"method":"GET","path":"/notes/{noteId}/versions/{versionId}","description":"Get one saved version with reconstructed content when available.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"versionId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"403":["application/json"],"404":["application/json"]}}, call: sdk.getVersion }, + "GetWebhook": { ...{"method":"GET","path":"/webhooks/{hookId}","description":"Get a webhook in the current user's workspace.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getWebhook }, + "GetWebhookDelivery": { ...{"method":"GET","path":"/webhooks/{hookId}/deliveries/{deliveryId}","description":"Get a webhook delivery within the workspace plan's retention window.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"},{"name":"deliveryId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.getWebhookDelivery }, + "ListFolders": { ...{"method":"GET","path":"/folders","description":"List all folders in the current user's workspace","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.listFolders }, + "ListNoteComments": { ...{"method":"GET","path":"/notes/{noteId}/comments","description":"List comments and replies visible on a note.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"page","in":"query","required":false,"type":"integer"},{"name":"limit","in":"query","required":false,"type":"integer"},{"name":"sort","in":"query","required":false,"type":"string","enum":["asc","desc"]},{"name":"threadId","in":"query","required":false,"type":"string"},{"name":"commentStatus","in":"query","required":false,"type":"string","enum":["open","hidden"]},{"name":"threadStatus","in":"query","required":false,"type":"string","enum":["open","hidden"]},{"name":"isThreadHead","in":"query","required":false,"type":"boolean","enum":[true]}],"responses":{"200":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"]}}, call: sdk.listNoteComments }, + "ListNotes": { ...{"method":"GET","path":"/notes","description":"List all notes for the current user","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.listNotes }, + "ListTeamFolders": { ...{"method":"GET","path":"/teams/{teampath}/folders","description":"List all folders in a team workspace","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."}],"responses":{"200":["application/json"]}}, call: sdk.listTeamFolders }, + "ListTeamNotes": { ...{"method":"GET","path":"/teams/{teampath}/notes","description":"List all notes for a team","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."}],"responses":{"200":["application/json"]}}, call: sdk.listTeamNotes }, + "ListTeams": { ...{"method":"GET","path":"/teams","description":"List the teams for the current user","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.listTeams }, + "ListTeamTrash": { ...{"method":"GET","path":"/teams/{teampath}/trash","description":"List trashed notes in a team workspace (team admin only; same rules as the internal API).","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."}],"responses":{"200":["application/json"],"403":["application/json"],"404":["application/json"]}}, call: sdk.listTeamTrash }, + "ListTeamWebhookDeliveries": { ...{"method":"GET","path":"/teams/{teampath}/webhooks/{hookId}/deliveries","description":"List team webhook deliveries within the plan's retention window.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"},{"name":"page","in":"query","required":false,"type":"integer"},{"name":"limit","in":"query","required":false,"type":"integer"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.listTeamWebhookDeliveries }, + "ListTeamWebhooks": { ...{"method":"GET","path":"/teams/{teampath}/webhooks","description":"List webhooks in a team workspace.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"]}}, call: sdk.listTeamWebhooks }, + "ListTrash": { ...{"method":"GET","path":"/trash","description":"List trashed notes in your personal workspace (same data as the internal trash API).","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.listTrash }, + "ListVersions": { ...{"method":"GET","path":"/notes/{noteId}/versions","description":"List saved versions for a note.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"named_only","in":"query","required":false,"type":"boolean"},{"name":"q","in":"query","required":false,"type":"string"},{"name":"created_by","in":"query","required":false,"type":"string"},{"name":"created_after","in":"query","required":false,"type":"string","description":"Return versions created at or after this ISO-8601 datetime."},{"name":"created_before","in":"query","required":false,"type":"string","description":"Return versions created at or before this ISO-8601 datetime."},{"name":"page","in":"query","required":false,"type":"number"},{"name":"limit","in":"query","required":false,"type":"number"}],"responses":{"200":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"]}}, call: sdk.listVersions }, + "ListWebhookDeliveries": { ...{"method":"GET","path":"/webhooks/{hookId}/deliveries","description":"List webhook deliveries within the workspace plan's retention window.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"},{"name":"page","in":"query","required":false,"type":"integer"},{"name":"limit","in":"query","required":false,"type":"integer"}],"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"]}}, call: sdk.listWebhookDeliveries }, + "ListWebhooks": { ...{"method":"GET","path":"/webhooks","description":"List webhooks in the current user's workspace.","parameters":[],"responses":{"200":["application/json"]}}, call: sdk.listWebhooks }, + "PingTeamWebhook": { ...{"method":"POST","path":"/teams/{teampath}/webhooks/{hookId}/ping","description":"Send a test delivery for a team webhook.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"204":[],"400":["application/json"],"404":["application/json"],"429":["application/json"]}}, call: sdk.pingTeamWebhook }, + "PingWebhook": { ...{"method":"POST","path":"/webhooks/{hookId}/ping","description":"Send a test delivery. Read the result from the deliveries endpoint.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"}],"responses":{"204":[],"400":["application/json"],"404":["application/json"],"429":["application/json"]}}, call: sdk.pingWebhook }, + "ResolveNoteComment": { ...{"method":"PUT","path":"/notes/{noteId}/comments/{commentId}/resolution","description":"Resolve a comment or thread.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"commentId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"]}}, call: sdk.resolveNoteComment }, + "RestoreNote": { ...{"method":"PUT","path":"/trash/{noteId}/restore","description":"Restore a single note from trash.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string","description":"Encoded note id of the trashed note."}],"responses":{"204":[],"403":["application/json"],"404":["application/json"],"500":["application/json"]}}, call: sdk.restoreNote }, + "UnresolveNoteComment": { ...{"method":"DELETE","path":"/notes/{noteId}/comments/{commentId}/resolution","description":"Unresolve a comment or thread.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"},{"name":"commentId","in":"path","required":true,"type":"string"}],"responses":{"200":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"]}}, call: sdk.unresolveNoteComment }, + "UpdateFolder": { ...{"method":"PATCH","path":"/folders/{folderId}","description":"Update a folder in the current user's workspace","parameters":[{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to update."}],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"202":[],"400":["application/json"],"404":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.updateFolder }, + "UpdateFolderOrder": { ...{"method":"PUT","path":"/folders/folder-order","description":"Replace your personal folder ordering for this workspace.","parameters":[],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"204":[],"400":["application/json"],"422":["application/json"]}}, call: sdk.updateFolderOrder }, + "UpdateNote": { ...{"method":"PATCH","path":"/notes/{noteId}","description":"Update a note's content or permissions for the current user","parameters":[{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to update."}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"202":[],"400":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.updateNote }, + "UpdateTeamFolder": { ...{"method":"PATCH","path":"/teams/{teampath}/folders/{folderId}","description":"Update a folder in a team workspace","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"folderId","in":"path","required":true,"type":"string","description":"The ID of the folder to update."}],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"202":[],"400":["application/json"],"404":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.updateTeamFolder }, + "UpdateTeamFolderOrder": { ...{"method":"PUT","path":"/teams/{teampath}/folders/folder-order","description":"Replace your personal folder ordering for this team workspace.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"}],"requestBody":{"required":false,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"204":[],"400":["application/json"],"422":["application/json"]}}, call: sdk.updateTeamFolderOrder }, + "UpdateTeamNote": { ...{"method":"PATCH","path":"/teams/{teampath}/notes/{noteId}","description":"Update a team note","parameters":[{"name":"teampath","in":"path","required":true,"type":"string","description":"The path identifier for the team."},{"name":"noteId","in":"path","required":true,"type":"string","description":"The ID of the note to update."}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"202":[],"400":["application/json"],"403":["application/json"],"404":["application/json"],"409":["application/json"],"422":["application/json"],"500":["application/json"]}}, call: sdk.updateTeamNote }, + "UpdateTeamWebhook": { ...{"method":"PATCH","path":"/teams/{teampath}/webhooks/{hookId}","description":"Update a team webhook. At least one of name, url, or active is required.\nScope and event selection cannot be changed.","parameters":[{"name":"teampath","in":"path","required":true,"type":"string"},{"name":"hookId","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"]}}, call: sdk.updateTeamWebhook }, + "UpdateVersion": { ...{"method":"PATCH","path":"/notes/{noteId}/versions","description":"Update an existing named version's metadata with `PATCH /notes/{noteId}/versions`\nand a `base` body field formatted as `version:{id}`. The item path\n`/notes/{noteId}/versions/{versionId}` does not support PATCH.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"200":["application/json"],"400":["application/json"],"403":["application/json"],"404":["application/json"],"422":["application/json"]}}, call: sdk.updateVersion }, + "UpdateWebhook": { ...{"method":"PATCH","path":"/webhooks/{hookId}","description":"Update a webhook. At least one of name, url, or active is required.\nScope and event selection cannot be changed.","parameters":[{"name":"hookId","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["application/json"],"binaryFields":[]},"responses":{"200":["application/json"],"400":["application/json"],"404":["application/json"],"422":["application/json"]}}, call: sdk.updateWebhook }, + "UploadNoteImage": { ...{"method":"POST","path":"/notes/{noteId}/images","description":"Upload an image for a note.","parameters":[{"name":"noteId","in":"path","required":true,"type":"string"}],"requestBody":{"required":true,"contentTypes":["multipart/form-data"],"binaryFields":[{"name":"image","required":true}]},"responses":{"201":["application/json"],"400":["application/json"],"401":["application/json"],"403":["application/json"],"413":["application/json"],"415":["application/json"],"422":["application/json"],"429":["application/json"]}}, call: sdk.uploadNoteImage }, +} as const + +export type OperationId = keyof typeof operationRegistry diff --git a/nodejs/src/raw.ts b/nodejs/src/raw.ts index 3ab6c40..a799a9f 100644 --- a/nodejs/src/raw.ts +++ b/nodejs/src/raw.ts @@ -19,3 +19,5 @@ export type { RequestOptions as RawRequestOptions, RequestResult as RawRequestResult, } from './generated/client/index.js' +export { operationRegistry } from './generated/operationRegistry.gen.js' +export type { OperationId } from './generated/operationRegistry.gen.js' diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index 4b27fe7..dd82f5e 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -2,72 +2,10 @@ import { http, HttpResponse } from 'msw' import { API, CommentPermissionType, NotePermissionRole, NotePublishType, TeamVisibilityType } from '../src' import * as generatedSdk from '../src/generated/sdk.gen' -import { createClient, getMe } from '../src/raw' +import { createClient, getMe, operationRegistry } from '../src/raw' import * as openApiDocument from '../spec/hackmd-openapi.json' import { server } from './mock' -const EXPECTED_OPERATIONS = [ - 'batchRestore', - 'compareVersions', - 'createFolder', - 'createNote', - 'createTeamFolder', - 'createTeamNote', - 'createTeamWebhook', - 'createVersion', - 'createWebhook', - 'deleteFolder', - 'deleteNote', - 'deleteTeamFolder', - 'deleteTeamNote', - 'deleteTeamWebhook', - 'deleteWebhook', - 'exportTeamWebhookDeliveries', - 'exportWebhookDeliveries', - 'getMe', - 'getFolder', - 'getFolderOrder', - 'getHistory', - 'getNote', - 'getNoteComment', - 'getTeamFolder', - 'getTeamFolderOrder', - 'getTeamNote', - 'getTeamWebhook', - 'getTeamWebhookDelivery', - 'getVersion', - 'getWebhook', - 'getWebhookDelivery', - 'listFolders', - 'listNoteComments', - 'listNotes', - 'listTrash', - 'listTeamFolders', - 'listTeamNotes', - 'listTeams', - 'listTeamTrash', - 'listTeamWebhookDeliveries', - 'listTeamWebhooks', - 'listVersions', - 'listWebhookDeliveries', - 'listWebhooks', - 'pingTeamWebhook', - 'pingWebhook', - 'resolveNoteComment', - 'restoreNote', - 'unresolveNoteComment', - 'updateFolder', - 'updateFolderOrder', - 'updateNote', - 'updateTeamFolder', - 'updateTeamFolderOrder', - 'updateTeamNote', - 'updateTeamWebhook', - 'updateVersion', - 'updateWebhook', - 'uploadNoteImage', -] as const - const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const type OpenApiOperation = { @@ -92,14 +30,29 @@ describe('generated raw API', () => { afterEach(() => server.resetHandlers()) afterAll(() => server.close()) - test('covers every reviewed v1 operation', () => { - const expected = [...EXPECTED_OPERATIONS].sort() - - expect(expected).toHaveLength(59) - expect(operationNamesFromSpec()).toEqual(expected) + test('raw SDK matches every operation in the vendored spec', () => { + const expected = operationNamesFromSpec() + expect(expected.length).toBeGreaterThan(0) expect(Object.keys(generatedSdk).sort()).toEqual(expected) }) + test('registry covers every spec operation and generated raw function', () => { + const operations = Object.entries(openApiDocument.paths).flatMap(([path, item]) => + HTTP_METHODS.flatMap(method => { + const operation = (item as OpenApiPath)[method] + return operation?.operationId ? [{ path, method, operation }] : [] + }), + ) + expect(Object.keys(operationRegistry).sort()).toEqual(operations.map(({ operation }) => operation.operationId).sort()) + for (const { path, method, operation } of operations) { + const id = operation.operationId as keyof typeof operationRegistry + const entry = operationRegistry[id] + expect(entry.method).toBe(method.toUpperCase()) + expect(entry.path).toBe(path) + expect(entry.call).toBe(generatedSdk[id[0].toLowerCase() + id.slice(1) as keyof typeof generatedSdk]) + } + }) + test('keeps the existing runtime enum values', () => { expect(TeamVisibilityType).toEqual({ PUBLIC: 'public', PRIVATE: 'private' }) expect(NotePublishType).toEqual({ EDIT: 'edit', VIEW: 'view', SLIDE: 'slide', BOOK: 'book' }) From e805f59dd29bd73acfe5e249822a38e6b51fdcbd Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Sat, 26 Sep 2026 03:07:12 +0800 Subject: [PATCH 30/31] chore: polish codegen and type playground --- nodejs/README.md | 7 +++++-- nodejs/docs/README.md | 9 +++++---- nodejs/jest.e2e.config.ts | 3 +++ nodejs/package.json | 4 ++-- nodejs/scripts/check-generated.mjs | 2 +- nodejs/tests/types/tsconfig.json | 9 +++++++++ 6 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 nodejs/tests/types/tsconfig.json diff --git a/nodejs/README.md b/nodejs/README.md index 28ba00b..1974914 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -223,7 +223,10 @@ compatible aliases. The [API reference](https://hackmdio.github.io/api-client/) covers the existing `API` class and every raw operation and DTO. To explore autocomplete -without a real token, open the [type-only StackBlitz example](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts). +without a real token, open the type-only example: + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts) + The Pages site is deployed from `master` only. Run `pnpm docs:dev` from `nodejs` and open `http://127.0.0.1:3000` to preview @@ -237,7 +240,7 @@ Generation requires Node.js 22.18 or newer. ```bash pnpm spec:pull -pnpm generate +pnpm codegen pnpm check:generated ``` diff --git a/nodejs/docs/README.md b/nodejs/docs/README.md index 56132af..db51041 100644 --- a/nodejs/docs/README.md +++ b/nodejs/docs/README.md @@ -33,7 +33,8 @@ Use the raw reference for the full OpenAPI operation and DTO catalog. ## Explore the types -[Open the type-only example in StackBlitz](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts). -The example makes no requests and requires no real token. It is useful for -autocomplete and type inspection; run real API calls only with your own token -in a trusted local environment. +The type-only example makes no requests and requires no real token. Open it to +explore autocomplete and type inspection; run real API calls only with your own +token in a trusted local environment. + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/fork/github/hackmdio/api-client/tree/master/nodejs?file=tests/types/playground.mts) diff --git a/nodejs/jest.e2e.config.ts b/nodejs/jest.e2e.config.ts index f1ea72d..ae20590 100644 --- a/nodejs/jest.e2e.config.ts +++ b/nodejs/jest.e2e.config.ts @@ -6,6 +6,9 @@ const e2eJestConfig: JestConfigWithTsJest = { testEnvironment: "node", transformIgnorePatterns: ["/node_modules/"], extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, setupFiles: ["dotenv/config"], testMatch: ["/tests/e2e/**/*.spec.ts"], testTimeout: 60_000, diff --git a/nodejs/package.json b/nodejs/package.json index c443dc0..6852834 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -32,9 +32,9 @@ "scripts": { "clean": "rimraf dist", "build": "pnpm run clean && rollup -c && pnpm check:node-types", - "generate": "openapi-ts", + "codegen": "openapi-ts", "check:generated": "node scripts/check-generated.mjs", - "check:node-types": "tsc --noEmit --strict --module NodeNext tests/types/raw.mts tests/types/raw.cts tests/types/playground.mts", + "check:node-types": "tsc -p tests/types/tsconfig.json", "docs:build": "typedoc", "docs:dev": "pnpm docs:build && serve .docs-dist --listen tcp://127.0.0.1:3000", "typecheck": "tsc --noEmit -p tsconfig.json", diff --git a/nodejs/scripts/check-generated.mjs b/nodejs/scripts/check-generated.mjs index d49a85a..3453f06 100644 --- a/nodejs/scripts/check-generated.mjs +++ b/nodejs/scripts/check-generated.mjs @@ -30,7 +30,7 @@ async function manifest () { } const before = await manifest() -const generation = spawnSync('pnpm', ['run', 'generate'], { +const generation = spawnSync('pnpm', ['run', 'codegen'], { cwd: packageRoot, stdio: 'inherit', }) diff --git a/nodejs/tests/types/tsconfig.json b/nodejs/tests/types/tsconfig.json new file mode 100644 index 0000000..42e480e --- /dev/null +++ b/nodejs/tests/types/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true + }, + "include": ["*.mts", "*.cts"] +} From 859bbd9ae011b8f2925ff1d8c704224ef705bc54 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 28 Sep 2026 01:19:47 +0800 Subject: [PATCH 31/31] fix: sync nullable team owner contract --- nodejs/spec/hackmd-openapi.json | 5 +++-- nodejs/src/generated/types.gen.ts | 2 +- nodejs/tests/generated.spec.ts | 6 ++++++ nodejs/tests/types/raw.mts | 7 +++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/nodejs/spec/hackmd-openapi.json b/nodejs/spec/hackmd-openapi.json index e6796dc..139c316 100644 --- a/nodejs/spec/hackmd-openapi.json +++ b/nodejs/spec/hackmd-openapi.json @@ -1051,7 +1051,8 @@ "type": "string" }, "ownerId": { - "type": "string" + "type": "string", + "nullable": true }, "name": { "type": "string" @@ -6718,4 +6719,4 @@ "description": "Operations for managing webhooks in a team workspace." } ] -} +} \ No newline at end of file diff --git a/nodejs/src/generated/types.gen.ts b/nodejs/src/generated/types.gen.ts index 0b44208..0cff5a5 100644 --- a/nodejs/src/generated/types.gen.ts +++ b/nodejs/src/generated/types.gen.ts @@ -280,7 +280,7 @@ export type TeamVisibilityType = 'public' | 'private'; export type Team = { id: string; - ownerId: string; + ownerId: string | null; name: string; logo: string; path: string; diff --git a/nodejs/tests/generated.spec.ts b/nodejs/tests/generated.spec.ts index dd82f5e..0f3c3aa 100644 --- a/nodejs/tests/generated.spec.ts +++ b/nodejs/tests/generated.spec.ts @@ -53,6 +53,12 @@ describe('generated raw API', () => { } }) + test('team ownerId is required but nullable in the shared spec', () => { + const team = openApiDocument.components.schemas.Team + expect(team.required).toContain('ownerId') + expect(team.properties.ownerId).toEqual({ type: 'string', nullable: true }) + }) + test('keeps the existing runtime enum values', () => { expect(TeamVisibilityType).toEqual({ PUBLIC: 'public', PRIVATE: 'private' }) expect(NotePublishType).toEqual({ EDIT: 'edit', VIEW: 'view', SLIDE: 'slide', BOOK: 'book' }) diff --git a/nodejs/tests/types/raw.mts b/nodejs/tests/types/raw.mts index 8218500..474fd20 100644 --- a/nodejs/tests/types/raw.mts +++ b/nodejs/tests/types/raw.mts @@ -1,4 +1,11 @@ import { createClient, getNote } from '@hackmd/api/raw' +import type { Team } from '@hackmd/api/raw' + +const ownerlessTeam: Pick = { ownerId: null } +const ownedTeam: Pick = { ownerId: 'user-id' } +// @ts-expect-error ownerId is nullable, not optional. +const missingOwner: Pick = {} +void [ownerlessTeam, ownedTeam, missingOwner] const client = createClient({ auth: 'test-token',