snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
853
packages/http-router/CHANGELOG.md
Normal file
853
packages/http-router/CHANGELOG.md
Normal file
@@ -0,0 +1,853 @@
|
||||
# @rocket.chat/http-router
|
||||
|
||||
## 7.9.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [278b84f78360e53792a2e5d7620615039a0e15e9, 24b3671fe61b8b09c6a1b5dc6401b503b3fb92a0, 4e99ff6c1a99ac569cf444f9de9b5fe5ac3262b2, 8c0e16ca29b393cfa50b425520db48ba5a74f678]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.4.0
|
||||
- @rocket.chat/rest-typings@8.4.0
|
||||
</details>
|
||||
|
||||
## 7.9.23-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.4.0-rc.2
|
||||
- @rocket.chat/rest-typings@8.4.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.23-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.4.0-rc.1
|
||||
- @rocket.chat/rest-typings@8.4.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.2
|
||||
- @rocket.chat/rest-typings@8.3.2
|
||||
</details>
|
||||
|
||||
## 7.9.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.1
|
||||
- @rocket.chat/rest-typings@8.3.1
|
||||
</details>
|
||||
|
||||
## 7.9.23-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [278b84f78360e53792a2e5d7620615039a0e15e9, 24b3671fe61b8b09c6a1b5dc6401b503b3fb92a0, 4e99ff6c1a99ac569cf444f9de9b5fe5ac3262b2, 8c0e16ca29b393cfa50b425520db48ba5a74f678]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.4.0-rc.0
|
||||
- @rocket.chat/rest-typings@8.4.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
- <details><summary>Updated dependencies [a4e3c1635d55ec4ce04cbde741426770e43581fb, 539659af22bc19880eda047dfc0b152472ccb65c, 5518503736b72674753e711ba4089d177ab988a5, a4341ec67d1f0413f30bbabfd292d1b0a41728b2, 40253146de8d8f83737e71b0ade7c67e0c295a28, 803b8075514de54c9ff34ba0c9aa3ee5fc3bbe61, 1361a1f4f1e3c0cc3f2a191cef8eccc12a714cde, 2a2701098536b32143003be8d267891978c708c9, 78e37dc3deae4ff05f5e33f9134c7094fd6c1330, 37acece030bc9f39bdaa86ab0130eb818332033e, d8baf395181b70fef9ce448eb509f65b66049615, ddc0ed34b03072362d166f1160104a9332b362e8, d83a1a9753464ee916845b3c88757bbcf76884a5, 4c2e444216efd514ab406fe8e9cd127ef971d566, 722df6f60bc86c51b204e28a39acb3dc8710bdeb, 78b3fe3ef20e3a545b84551ba3f85cb40e862ba7, 98a6c58a38c053c60db2b4d53a9df0e94fecf0ba, 29b453e1def8092a8d78c28736e2bfb24229717b, 39f2e87e1caa6842e69155f033205cfdc4767b9e, c117492ad90d291a361eedc929506f557495caf7, 7c7324184589a15bf3e67b4f0c1cc222f8d48db3]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.3.0
|
||||
- @rocket.chat/core-typings@8.3.0
|
||||
- @rocket.chat/logger@1.0.1
|
||||
</details>
|
||||
|
||||
## 7.9.20-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.0-rc.4
|
||||
- @rocket.chat/rest-typings@8.3.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.9.20-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.0-rc.3
|
||||
- @rocket.chat/rest-typings@8.3.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.9.20-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.0-rc.2
|
||||
- @rocket.chat/rest-typings@8.3.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.20-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.3.0-rc.1
|
||||
- @rocket.chat/rest-typings@8.3.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.20-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38989](https://github.com/RocketChat/Rocket.Chat/pull/38989)) chore(eslint): Upgrades ESLint and its configuration
|
||||
|
||||
- <details><summary>Updated dependencies [a4e3c1635d55ec4ce04cbde741426770e43581fb, 539659af22bc19880eda047dfc0b152472ccb65c, 5518503736b72674753e711ba4089d177ab988a5, a4341ec67d1f0413f30bbabfd292d1b0a41728b2, 40253146de8d8f83737e71b0ade7c67e0c295a28, 803b8075514de54c9ff34ba0c9aa3ee5fc3bbe61, 1361a1f4f1e3c0cc3f2a191cef8eccc12a714cde, 2a2701098536b32143003be8d267891978c708c9, 78e37dc3deae4ff05f5e33f9134c7094fd6c1330, 37acece030bc9f39bdaa86ab0130eb818332033e, d8baf395181b70fef9ce448eb509f65b66049615, ddc0ed34b03072362d166f1160104a9332b362e8, d83a1a9753464ee916845b3c88757bbcf76884a5, 4c2e444216efd514ab406fe8e9cd127ef971d566, 722df6f60bc86c51b204e28a39acb3dc8710bdeb, 78b3fe3ef20e3a545b84551ba3f85cb40e862ba7, 98a6c58a38c053c60db2b4d53a9df0e94fecf0ba, 29b453e1def8092a8d78c28736e2bfb24229717b, 39f2e87e1caa6842e69155f033205cfdc4767b9e, c117492ad90d291a361eedc929506f557495caf7, 7c7324184589a15bf3e67b4f0c1cc222f8d48db3]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.3.0-rc.0
|
||||
- @rocket.chat/core-typings@8.3.0-rc.0
|
||||
- @rocket.chat/logger@1.0.1-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.2.1
|
||||
- @rocket.chat/rest-typings@8.2.1
|
||||
</details>
|
||||
|
||||
## 7.9.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38319](https://github.com/RocketChat/Rocket.Chat/pull/38319)) Fixes incoming webhook integrations not receiving parsed JSON from x-www-form-urlencoded payload field.
|
||||
|
||||
- <details><summary>Updated dependencies [d3758a7d57ab602745369ef9d2ccdbf9271cf305, fbc4935dec220495201cf905017170d3cd1e275c, e57f15845e4df048dd2f08f11aa08215780a2c34, 88da141f3c2af6f91980c7ca8b8777161f99a068, 75d089ca40248af963d7cd2a8034c3c6de6b971e, 3b003e6b69c11b280d55bcc8db2f3e4ae7a4a573]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.2.0
|
||||
- @rocket.chat/rest-typings@8.2.0
|
||||
</details>
|
||||
|
||||
## 7.9.18-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.2.0-rc.2
|
||||
- @rocket.chat/rest-typings@8.2.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.18-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.2.0-rc.1
|
||||
- @rocket.chat/rest-typings@8.2.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.18-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38319](https://github.com/RocketChat/Rocket.Chat/pull/38319)) Fixes incoming webhook integrations not receiving parsed JSON from x-www-form-urlencoded payload field.
|
||||
|
||||
- <details><summary>Updated dependencies [d3758a7d57ab602745369ef9d2ccdbf9271cf305, fbc4935dec220495201cf905017170d3cd1e275c, e57f15845e4df048dd2f08f11aa08215780a2c34, 88da141f3c2af6f91980c7ca8b8777161f99a068, 75d089ca40248af963d7cd2a8034c3c6de6b971e, 3b003e6b69c11b280d55bcc8db2f3e4ae7a4a573]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.2.0-rc.0
|
||||
- @rocket.chat/rest-typings@8.2.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.1.1
|
||||
- @rocket.chat/rest-typings@8.1.1
|
||||
</details>
|
||||
|
||||
## 7.9.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38071](https://github.com/RocketChat/Rocket.Chat/pull/38071)) Improves file upload flow to prevent buffering of contents in memory
|
||||
|
||||
- <details><summary>Updated dependencies [fd319d58a621d66c20029fd962ddb24a1a24ecc3, 5c3c52b1fc8b0bd52d4744614ad4d2b8e2e03937, 6654c5b481f91bdcb03d68ee0f3a12d58201137e]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.1.0
|
||||
- @rocket.chat/core-typings@8.1.0
|
||||
</details>
|
||||
|
||||
## 7.9.16-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.1.0-rc.2
|
||||
- @rocket.chat/rest-typings@8.1.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.16-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [fd319d58a621d66c20029fd962ddb24a1a24ecc3]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.1.0-rc.1
|
||||
- @rocket.chat/core-typings@8.1.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.16-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#38071](https://github.com/RocketChat/Rocket.Chat/pull/38071)) Improves file upload flow to prevent buffering of contents in memory
|
||||
|
||||
- <details><summary>Updated dependencies [5c3c52b1fc8b0bd52d4744614ad4d2b8e2e03937, 6654c5b481f91bdcb03d68ee0f3a12d58201137e]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.1.0-rc.0
|
||||
- @rocket.chat/core-typings@8.1.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.1
|
||||
- @rocket.chat/rest-typings@8.0.1
|
||||
</details>
|
||||
|
||||
## 7.9.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [347b8f973440f3e2239f79c00c2d9b430859eef2, b802430fbfdc7fa69a976468dc6dee6f3c4de26f, 872da49986436d2efa65fc42e416b45d706fd59c, 176d5eae3fb249d7d20c3e260d9fadc1a56a2fca, ac11ea05ffadeca978c794ff38d5199d9acb2c29, f3f0b273ab49e9d1048ba43d52eb36005274905e, ac11ea05ffadeca978c794ff38d5199d9acb2c29, a1d65f493fdb039d34ef4f65d243a97931763f4b, ddc935727e9a7275813006d9dcaa7fe866610844, a5a7343a835b04812c70699be1b13e54f0e10d48, 73d9eb2783176954f42aa2cbeda8abf1d49ac260, 476a070b0099b95e4c463ee85960c4dcfbd87120, 239f4b1171bcf448cfba345cc90c4b5cd7c21afc, dc67590d14d510b069dee074c55314c56f74bb11, dccdcc5b4a0da4814f72a020bc4eccb8ea2497d8, 5ac1863be4c6e82666989f4b569928c0805691ff, ec0f8b435dd12c218adffa8892737c7ced4debb8, cb3c5e3455606a045f95f168dae6ed32a387697c, 5b3f93c47a03b628d613a77005e92021cd6cee4b]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.0.0
|
||||
- @rocket.chat/core-typings@8.0.0
|
||||
- @rocket.chat/logger@1.0.0
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.0-rc.5
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.5
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.0-rc.4
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.0-rc.3
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.0-rc.2
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@8.0.0-rc.1
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.14-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [347b8f973440f3e2239f79c00c2d9b430859eef2, b802430fbfdc7fa69a976468dc6dee6f3c4de26f, 872da49986436d2efa65fc42e416b45d706fd59c, 176d5eae3fb249d7d20c3e260d9fadc1a56a2fca, ac11ea05ffadeca978c794ff38d5199d9acb2c29, f3f0b273ab49e9d1048ba43d52eb36005274905e, ac11ea05ffadeca978c794ff38d5199d9acb2c29, a1d65f493fdb039d34ef4f65d243a97931763f4b, ddc935727e9a7275813006d9dcaa7fe866610844, a5a7343a835b04812c70699be1b13e54f0e10d48, 73d9eb2783176954f42aa2cbeda8abf1d49ac260, 476a070b0099b95e4c463ee85960c4dcfbd87120, 239f4b1171bcf448cfba345cc90c4b5cd7c21afc, dc67590d14d510b069dee074c55314c56f74bb11, dccdcc5b4a0da4814f72a020bc4eccb8ea2497d8, 5ac1863be4c6e82666989f4b569928c0805691ff, ec0f8b435dd12c218adffa8892737c7ced4debb8, cb3c5e3455606a045f95f168dae6ed32a387697c, 5b3f93c47a03b628d613a77005e92021cd6cee4b]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@8.0.0-rc.0
|
||||
- @rocket.chat/core-typings@8.0.0-rc.0
|
||||
- @rocket.chat/logger@1.0.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.13.2
|
||||
- @rocket.chat/rest-typings@7.13.2
|
||||
</details>
|
||||
|
||||
## 7.9.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.13.1
|
||||
- @rocket.chat/rest-typings@7.13.1
|
||||
</details>
|
||||
|
||||
## 7.9.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [7f1b834a55b1240c226afde77713262da47f45dc, 5c7e8ec1de894e7b8eeb6e57b0c8a43bd22d2d46, 65fbcbed9f64004b953dd9d4182b3fccb8147339, 035e0c7c387d6c99703513c7c00bf37e86538b71]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.13.0
|
||||
- @rocket.chat/rest-typings@7.13.0
|
||||
</details>
|
||||
|
||||
## 7.9.11-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.13.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.13.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.11-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.13.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.13.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.11-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [7f1b834a55b1240c226afde77713262da47f45dc, 5c7e8ec1de894e7b8eeb6e57b0c8a43bd22d2d46, 65fbcbed9f64004b953dd9d4182b3fccb8147339, 035e0c7c387d6c99703513c7c00bf37e86538b71]:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.13.0-rc.0
|
||||
- @rocket.chat/rest-typings@7.13.0-rc.0
|
||||
|
||||
## 7.9.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.2
|
||||
- @rocket.chat/rest-typings@7.12.2
|
||||
</details>
|
||||
|
||||
## 7.9.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.1
|
||||
- @rocket.chat/rest-typings@7.12.1
|
||||
</details>
|
||||
|
||||
## 7.9.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [48d1442a07e860fefd6991c97e94d6a9de345afe, d166e2a1ffba4e59361d5f79e8c376fca5cbf12f, 4ab2b7fdb9314538d45abdf4474ea9d2409799be, fd4f9b2523dae23518561cdcfa8d931f87d8964d]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.12.0
|
||||
- @rocket.chat/core-typings@7.12.0
|
||||
</details>
|
||||
|
||||
## 7.9.8-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.0-rc.4
|
||||
- @rocket.chat/rest-typings@7.12.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.9.8-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.0-rc.3
|
||||
- @rocket.chat/rest-typings@7.12.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.9.8-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.12.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.8-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.12.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.12.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.8-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [48d1442a07e860fefd6991c97e94d6a9de345afe, d166e2a1ffba4e59361d5f79e8c376fca5cbf12f, 4ab2b7fdb9314538d45abdf4474ea9d2409799be, fd4f9b2523dae23518561cdcfa8d931f87d8964d]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.12.0-rc.0
|
||||
- @rocket.chat/core-typings@7.12.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.9.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [d76a5578ed0e14fae2a041c0e09d565b28630d76, 8d2ad40c46334aaf35816e52a3b28dc2ac23ce2d, ba793542644d2b725924c4cef253e7d84bc2ff71, 3e177dbd0b65d70bce8587287ba73b30170eb0f7, c5ee569cc3120bb616001fd78687dbf623a6a504, 00611aceec7f158064863f2e829c5a6698687512, c15a47ee1d55118342d0f0ece7af77f6f41132b7, 276c270f34293729182e9ebab61e1ff2c35398fc, b0a4602a4461200b9872b2b073ec56fa55ecb466, 80fbb2eb421253e6066207949eaf2b41871e4428, 90a1759df57814306cbc859409fe041b33db7a39, b74ab6f18ff1343f4835975f663211b005ccc5a1]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.11.0
|
||||
- @rocket.chat/core-typings@7.11.0
|
||||
</details>
|
||||
|
||||
## 7.9.7-rc.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.7
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.7
|
||||
</details>
|
||||
|
||||
## 7.9.7-rc.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.6
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.6
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.5
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.5
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.4
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.3
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.11.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.11.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.6-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [d76a5578ed0e14fae2a041c0e09d565b28630d76, 8d2ad40c46334aaf35816e52a3b28dc2ac23ce2d, ba793542644d2b725924c4cef253e7d84bc2ff71, 3e177dbd0b65d70bce8587287ba73b30170eb0f7, c5ee569cc3120bb616001fd78687dbf623a6a504, 00611aceec7f158064863f2e829c5a6698687512, c15a47ee1d55118342d0f0ece7af77f6f41132b7, 276c270f34293729182e9ebab61e1ff2c35398fc, b0a4602a4461200b9872b2b073ec56fa55ecb466, 80fbb2eb421253e6066207949eaf2b41871e4428, 90a1759df57814306cbc859409fe041b33db7a39, b74ab6f18ff1343f4835975f663211b005ccc5a1]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.11.0-rc.0
|
||||
- @rocket.chat/core-typings@7.11.0-rc.0
|
||||
|
||||
</details>
|
||||
|
||||
## 7.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.10.2
|
||||
- @rocket.chat/rest-typings@7.10.2
|
||||
</details>
|
||||
|
||||
## 7.9.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.10.1
|
||||
- @rocket.chat/rest-typings@7.10.1
|
||||
|
||||
</details>
|
||||
|
||||
## 7.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#36535](https://github.com/RocketChat/Rocket.Chat/pull/36535) by [@ahmed-n-abdeltwab](https://github.com/ahmed-n-abdeltwab)) This change fixes the HTTP route validation to return the correct error message 'invalid-params' instead of 'error-invalid-params', ensuring consistency with our API error codes. The body validation should now return 'invalid-params' instead of 'error-invalid-params'.'
|
||||
|
||||
- <details><summary>Updated dependencies [f040b27ff67c31188026a0aed9ba1e9c4f717f08, a54f8837338246842585d037a0d0327a79245811, c5f0be15b31d1de03256f74bd277ad4ab753ada2, c86fbce9b44942662dc25a599fc12b009fd40a74, 2fea1a79b831999f148505b9442cd584e1b06d09, 2f162a0dca79274d4458a5853afe64c506a7554f, 1ca92c346e45486e9b6afc66566ae38fac65b48f, 5d7dec3a68f7281b4b4531fa708d7fc7589a863c, 17bca96ecbf23ea807aba2e6e8abc95ebd66b0d0, dc6acda84bf7452d96f375be3cd97748ed016bfc, c7db598e9f3c2ad47f6a6be2a9ba7078533c245b]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.10.0
|
||||
- @rocket.chat/core-typings@7.10.0
|
||||
</details>
|
||||
|
||||
## 7.9.4-rc.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.10.0-rc.6
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.6
|
||||
</details>
|
||||
|
||||
## 7.9.4-rc.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.10.0-rc.5
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.5
|
||||
</details>
|
||||
|
||||
## 7.9.1-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.10.0-rc.4
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.9.1-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.10.0-rc.3
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.9.1-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.10.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.1-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.10.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.1-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ([#36535](https://github.com/RocketChat/Rocket.Chat/pull/36535) by [@ahmed-n-abdeltwab](https://github.com/ahmed-n-abdeltwab)) This change fixes the HTTP route validation to return the correct error message 'invalid-params' instead of 'error-invalid-params', ensuring consistency with our API error codes. The body validation should now return 'invalid-params' instead of 'error-invalid-params'.'
|
||||
|
||||
- <details><summary>Updated dependencies [f040b27ff67c31188026a0aed9ba1e9c4f717f08, a54f8837338246842585d037a0d0327a79245811, c5f0be15b31d1de03256f74bd277ad4ab753ada2, c86fbce9b44942662dc25a599fc12b009fd40a74, 2fea1a79b831999f148505b9442cd584e1b06d09, 2f162a0dca79274d4458a5853afe64c506a7554f, 1ca92c346e45486e9b6afc66566ae38fac65b48f, 5d7dec3a68f7281b4b4531fa708d7fc7589a863c, 17bca96ecbf23ea807aba2e6e8abc95ebd66b0d0, dc6acda84bf7452d96f375be3cd97748ed016bfc, c7db598e9f3c2ad47f6a6be2a9ba7078533c245b]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.10.0-rc.0
|
||||
- @rocket.chat/core-typings@7.10.0-rc.0
|
||||
|
||||
</details>
|
||||
|
||||
## 7.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.9.3
|
||||
- @rocket.chat/rest-typings@7.9.3
|
||||
</details>
|
||||
|
||||
## 7.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.9.2
|
||||
- @rocket.chat/rest-typings@7.9.2
|
||||
</details>
|
||||
|
||||
## 7.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.9.1
|
||||
- @rocket.chat/rest-typings@7.9.1
|
||||
</details>
|
||||
|
||||
## 7.9.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [0de9318f344b49cb4afbce6bf8b26f943346343d, 2cec8acd5beddf5ad0c67c29fe632487cb82b026, d4226a564d699e44eb6264095da8de3850071d6b, 6f205101215b4d9890ac62f7acc21257f0cd3cf4, fd478a7d45a4505ad53d2d7aec8b44e9bf8fa41a]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.9.0
|
||||
- @rocket.chat/core-typings@7.9.0
|
||||
</details>
|
||||
|
||||
## 7.9.0-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
- @rocket.chat/core-typings@7.9.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.9.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.9.0-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.9.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.9.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.9.0-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [0de9318f344b49cb4afbce6bf8b26f943346343d, 2cec8acd5beddf5ad0c67c29fe632487cb82b026, d4226a564d699e44eb6264095da8de3850071d6b, 6f205101215b4d9890ac62f7acc21257f0cd3cf4, fd478a7d45a4505ad53d2d7aec8b44e9bf8fa41a]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.9.0-rc.0
|
||||
- @rocket.chat/core-typings@7.9.0-rc.0
|
||||
</details>
|
||||
|
||||
## 7.8.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.3
|
||||
- @rocket.chat/rest-typings@7.8.3
|
||||
</details>
|
||||
|
||||
## 7.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.2
|
||||
- @rocket.chat/rest-typings@7.8.2
|
||||
</details>
|
||||
|
||||
## 7.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.1
|
||||
- @rocket.chat/rest-typings@7.8.1
|
||||
</details>
|
||||
|
||||
## 7.8.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [5cd7b20d5fb6def0a76743576eaa19fc0bf01c4c, 1eb53d289e1280b5ac4ce4a6995e7fe28d50601a, da288f69a4c169eee942af73f490e7570fe41cc5, 3d024a900426c8bbf646e7ebedce0e17c9f7c140, 3779de0e8c5787f266bdeda5052b27c023c65f1c, d58457d25a9a899ebcc7fd0e1fe11d9d370c50d8]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.8.0
|
||||
- @rocket.chat/core-typings@7.8.0
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.9
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.9
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.8
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.8
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.7
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.7
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.6
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.6
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.5
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.5
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.4
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.4
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [d58457d25a9a899ebcc7fd0e1fe11d9d370c50d8]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.3
|
||||
- @rocket.chat/core-typings@7.8.0-rc.3
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.2
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.2
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies []:</summary>
|
||||
|
||||
- @rocket.chat/core-typings@7.8.0-rc.1
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.1
|
||||
</details>
|
||||
|
||||
## 7.8.0-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- <details><summary>Updated dependencies [5cd7b20d5fb6def0a76743576eaa19fc0bf01c4c, 1eb53d289e1280b5ac4ce4a6995e7fe28d50601a, da288f69a4c169eee942af73f490e7570fe41cc5, 3d024a900426c8bbf646e7ebedce0e17c9f7c140, 3779de0e8c5787f266bdeda5052b27c023c65f1c]:</summary>
|
||||
|
||||
- @rocket.chat/rest-typings@7.8.0-rc.0
|
||||
- @rocket.chat/core-typings@7.8.0-rc.0
|
||||
</details>
|
||||
128
packages/http-router/README.md
Normal file
128
packages/http-router/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Rocket.Chat HTTP Router
|
||||
|
||||
This package provides a flexible HTTP routing solution for Rocket.Chat services, built on top of Hono and Express.
|
||||
|
||||
## Features
|
||||
|
||||
- Type-safe route definitions with TypeScript
|
||||
- Express compatibility
|
||||
- Request validation via JSON Schema
|
||||
- Response validation
|
||||
- Middleware support
|
||||
- Nested routers
|
||||
- Strong TypeScript types for request and response objects
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @rocket.chat/http-router
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { Router } from '@rocket.chat/http-router';
|
||||
import express from 'express';
|
||||
|
||||
// Create a new router
|
||||
const api = new Router('api');
|
||||
|
||||
// Define routes with typed handlers
|
||||
api.get(
|
||||
'users',
|
||||
{
|
||||
// Request validation options
|
||||
query: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'number' },
|
||||
skip: { type: 'number' }
|
||||
}
|
||||
}
|
||||
},
|
||||
// Response validation
|
||||
response: {
|
||||
'200': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
users: { type: 'array' },
|
||||
total: { type: 'number' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Optional middleware
|
||||
async (c, next) => {
|
||||
console.log('Middleware 1');
|
||||
await next();
|
||||
},
|
||||
// Route handler
|
||||
async (c) => {
|
||||
const { limit, skip } = c.req.valid('query');
|
||||
|
||||
return {
|
||||
body: {
|
||||
users: [],
|
||||
total: 0
|
||||
},
|
||||
statusCode: 200
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Create an Express app and use the router
|
||||
const app = express();
|
||||
app.use(api.router);
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
The router supports middleware functions that can be used for authentication, logging, metrics, and more:
|
||||
|
||||
```typescript
|
||||
// Authentication middleware
|
||||
const authMiddleware = async (c, next) => {
|
||||
const token = c.req.header('Authorization');
|
||||
if (!token) {
|
||||
return {
|
||||
body: { error: 'Unauthorized' },
|
||||
statusCode: 401
|
||||
};
|
||||
}
|
||||
// Set context variables for later use
|
||||
c.set('userId', 'user123');
|
||||
await next();
|
||||
};
|
||||
|
||||
api.get('protected-route', {}, authMiddleware, async (c) => {
|
||||
const userId = c.get('userId');
|
||||
return {
|
||||
body: { message: `Hello, ${userId}` },
|
||||
statusCode: 200
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## Express Integration
|
||||
|
||||
This router is designed to work seamlessly with Express applications:
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { Router } from '@rocket.chat/http-router';
|
||||
|
||||
const app = express();
|
||||
const api = new Router('api');
|
||||
|
||||
// Define routes...
|
||||
|
||||
// Use the router with Express
|
||||
app.use(api.router);
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log('Server running on port 3000');
|
||||
});
|
||||
```
|
||||
6
packages/http-router/jest.config.ts
Normal file
6
packages/http-router/jest.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import server from '@rocket.chat/jest-presets/server';
|
||||
import type { Config } from 'jest';
|
||||
|
||||
export default {
|
||||
preset: server.preset,
|
||||
} satisfies Config;
|
||||
43
packages/http-router/package.json
Normal file
43
packages/http-router/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@rocket.chat/http-router",
|
||||
"version": "7.9.23",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc",
|
||||
"dev": "tsc --watch --preserveWatchOutput",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"test": "jest",
|
||||
"testunit": "jest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rocket.chat/core-typings": "workspace:^",
|
||||
"@rocket.chat/logger": "workspace:^",
|
||||
"@rocket.chat/rest-typings": "workspace:^",
|
||||
"ajv": "^8.20.0",
|
||||
"express": "^4.21.2",
|
||||
"hono": "4.13.5",
|
||||
"qs": "^6.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rocket.chat/jest-presets": "workspace:^",
|
||||
"@rocket.chat/tsconfig": "workspace:*",
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/jest": "~30.0.0",
|
||||
"@types/supertest": "~6.0.3",
|
||||
"eslint": "~9.39.5",
|
||||
"jest": "~30.2.0",
|
||||
"supertest": "~7.1.4",
|
||||
"ts-jest": "~29.4.12",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
755
packages/http-router/src/Router.spec.ts
Normal file
755
packages/http-router/src/Router.spec.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
import Ajv from 'ajv';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { Router } from './Router';
|
||||
|
||||
describe('Router', () => {
|
||||
const ajv = new Ajv();
|
||||
const dummyValidator = ajv.compile({
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
});
|
||||
|
||||
describe('Basic routing', () => {
|
||||
it('should handle GET requests', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.get(
|
||||
'hello',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { message: 'Hello, World!' },
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/hello');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: 'Hello, World!' });
|
||||
});
|
||||
|
||||
it('should handle POST requests', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.post(
|
||||
'echo',
|
||||
{
|
||||
response: {
|
||||
201: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
const bodyParams = await c.req.json();
|
||||
return {
|
||||
statusCode: 201,
|
||||
body: { received: bodyParams },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const testData = { message: 'Hello from POST' };
|
||||
const response = await request(app).post('/api/echo').send(testData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toEqual({ received: testData });
|
||||
});
|
||||
|
||||
it('should handle PUT requests', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.put(
|
||||
'update/:id',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const bodyParams = await c.req.json();
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
id,
|
||||
updated: bodyParams,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const testData = { name: 'Updated Resource' };
|
||||
const response = await request(app).put('/api/update/123').send(testData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ id: '123', updated: testData });
|
||||
});
|
||||
|
||||
it('should handle DELETE requests', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.delete(
|
||||
'remove/:id',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
const id = c.req.param('id');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { deleted: id },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).delete('/api/remove/123');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ deleted: '123' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should validate query parameters', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.get(
|
||||
'validate-query',
|
||||
{
|
||||
query: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'string' },
|
||||
},
|
||||
required: ['required'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
400: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const validResponse = await request(app).get('/api/validate-query?required=value');
|
||||
|
||||
expect(validResponse.status).toBe(200);
|
||||
|
||||
const invalidResponse = await request(app).get('/api/validate-query');
|
||||
|
||||
expect(invalidResponse.status).toBe(400);
|
||||
expect(invalidResponse.body).toHaveProperty('errorType', 'error-invalid-params');
|
||||
});
|
||||
|
||||
it('should validate request body', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.post(
|
||||
'validate-body',
|
||||
{
|
||||
body: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
required: ['name', 'age'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
400: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const validResponse = await request(app).post('/api/validate-body').send({ name: 'John', age: 30 });
|
||||
|
||||
expect(validResponse.status).toBe(200);
|
||||
|
||||
const invalidResponse = await request(app).post('/api/validate-body').send({ name: 'John' });
|
||||
|
||||
expect(invalidResponse.status).toBe(400);
|
||||
expect(invalidResponse.body).toHaveProperty('errorType', 'invalid-params');
|
||||
|
||||
const invalidTypeResponse = await request(app).post('/api/validate-body').send({ name: 'John', age: 'thirty' });
|
||||
|
||||
expect(invalidTypeResponse.status).toBe(400);
|
||||
expect(invalidTypeResponse.body).toHaveProperty('errorType', 'invalid-params');
|
||||
});
|
||||
|
||||
it('should validate response body in test mode', async () => {
|
||||
process.env.TEST_MODE = 'true';
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.get(
|
||||
'validate-response',
|
||||
{
|
||||
typed: true,
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
required: ['result'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { wrongField: 'wrong' },
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/validate-response');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty('errorType', 'error-invalid-body');
|
||||
|
||||
delete process.env.TEST_MODE;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Middleware', () => {
|
||||
it('should apply middleware to routes', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
const authMiddleware = async (c: any, next: () => Promise<void>) => {
|
||||
const token = c.req.header('Authorization');
|
||||
|
||||
if (!token || token !== 'Bearer valid-token') {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
errorType: 'unauthorized',
|
||||
error: 'Unauthorized access',
|
||||
},
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
c.set('userId', 'user-123');
|
||||
await next();
|
||||
};
|
||||
|
||||
api.get(
|
||||
'protected',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
401: dummyValidator,
|
||||
},
|
||||
},
|
||||
authMiddleware,
|
||||
async (c) => {
|
||||
const userId = c.get('userId');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { userId, message: 'Protected resource accessed' },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const unauthorizedResponse = await request(app).get('/api/protected');
|
||||
|
||||
expect(unauthorizedResponse.status).toBe(401);
|
||||
|
||||
const authorizedResponse = await request(app).get('/api/protected').set('Authorization', 'Bearer valid-token');
|
||||
|
||||
expect(authorizedResponse.status).toBe(200);
|
||||
expect(authorizedResponse.body).toEqual({
|
||||
userId: 'user-123',
|
||||
message: 'Protected resource accessed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support global middleware via use()', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.use(async (c, next) => {
|
||||
c.set('requestTime', 'test-time');
|
||||
await next();
|
||||
});
|
||||
|
||||
api.get(
|
||||
'time',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
const time = c.get('requestTime');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { time },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/time');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ time: 'test-time' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested routers', () => {
|
||||
it('should support nested routers with the use() method', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const v1 = new Router('/v1');
|
||||
const users = new Router('/users');
|
||||
|
||||
users.get(
|
||||
':id',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
const id = c.req.param('id');
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { userId: id },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
v1.use(users);
|
||||
api.use(v1);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/v1/users/123');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ userId: '123' });
|
||||
});
|
||||
|
||||
it('should pass middleware from parent to child routers', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const v1 = new Router('/v1');
|
||||
|
||||
v1.use(async (c, next) => {
|
||||
c.header('API-Version', 'v1');
|
||||
await next();
|
||||
});
|
||||
|
||||
v1.get(
|
||||
'info',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { version: 'v1' },
|
||||
}),
|
||||
);
|
||||
|
||||
api.use(v1);
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/v1/info');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers).toHaveProperty('api-version', 'v1');
|
||||
expect(response.body).toEqual({ version: 'v1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle errors thrown in route handlers', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
api.get(
|
||||
'error',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
500: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
try {
|
||||
const response = await request(app).get('/api/error');
|
||||
expect(response.status).toBe(500);
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content types', () => {
|
||||
it('should not auto-parse multipart/form-data and provide raw request for manual parsing', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.post(
|
||||
'form-data',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async (c) => {
|
||||
// For multipart/form-data, routes use c.env.incoming for manual parsing
|
||||
// In production, routes call UploadService.parse(c.env.incoming)
|
||||
const hasIncoming = !!c.env.incoming;
|
||||
const contentType = c.env.incoming?.headers['content-type'];
|
||||
const isMultipart = contentType?.includes('multipart/form-data');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
receivedRawRequest: hasIncoming,
|
||||
receivedIncoming: isMultipart,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).post('/api/form-data').field('name', 'Test User');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
receivedRawRequest: true,
|
||||
receivedIncoming: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set custom response headers', async () => {
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
|
||||
api.get(
|
||||
'custom-headers',
|
||||
{
|
||||
response: {
|
||||
200: dummyValidator,
|
||||
},
|
||||
},
|
||||
async () => ({
|
||||
statusCode: 200,
|
||||
body: { message: 'With custom headers' },
|
||||
headers: {
|
||||
'X-Custom-Header': 'custom-value',
|
||||
'Cache-Control': 'private, max-age=3600',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(api.router);
|
||||
|
||||
const response = await request(app).get('/api/custom-headers');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers).toHaveProperty('x-custom-header', 'custom-value');
|
||||
expect(response.headers).toHaveProperty('cache-control', 'private, max-age=3600');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if the query request is not valid', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const test = new Router('/test').get(
|
||||
'/',
|
||||
{
|
||||
typed: true,
|
||||
query: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['customProperty'],
|
||||
}),
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { customProperty: 'valid' }, // Ensure valid response for schema
|
||||
};
|
||||
},
|
||||
);
|
||||
app.use(api.use(test).router);
|
||||
const response = await request(app).get('/api/test');
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('error', "must have required property 'customProperty'");
|
||||
});
|
||||
it('should fail if the body request is not valid', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const test = new Router('/test').post(
|
||||
'/',
|
||||
{
|
||||
typed: true,
|
||||
body: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['customProperty'],
|
||||
}),
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { customProperty: 'valid' }, // Ensure valid response for schema
|
||||
};
|
||||
},
|
||||
);
|
||||
app.use(api.use(test).router);
|
||||
const response = await request(app).post('/api/test').send({});
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('error', "must have required property 'customProperty'");
|
||||
});
|
||||
|
||||
it('should fail if the body response has additional properties', async () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.TEST_MODE = 'true';
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const test = new Router('/test').get(
|
||||
'/',
|
||||
{
|
||||
typed: true,
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { asda: 1 }, // This body is invalid according to the schema
|
||||
};
|
||||
},
|
||||
);
|
||||
app.use(api.use(test).router);
|
||||
const response = await request(app).get('/api/test');
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty(
|
||||
'error',
|
||||
"Invalid response for endpoint GET - http://localhost/api/test. Error: at path '(root)': must NOT have additional properties (additionalProperty: asda)",
|
||||
);
|
||||
});
|
||||
|
||||
it('middleware should be applied only on the right path', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const v1 = new Router('/v1').use(async (x, next) => {
|
||||
x.header('x-api-version', 'v1');
|
||||
await next();
|
||||
});
|
||||
const v2 = new Router('/v2');
|
||||
const test = new Router('/test').get(
|
||||
'/',
|
||||
{
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['customProperty'],
|
||||
}),
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
customProperty: 'customProperty',
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
app.use(api.use(v1.use(test)).use(v2.use(test)).router);
|
||||
|
||||
const response1 = await request(app).get('/api/v1/test');
|
||||
|
||||
expect(response1.statusCode).toBe(200);
|
||||
expect(response1.headers).toHaveProperty('x-api-version', 'v1');
|
||||
|
||||
const response2 = await request(app).get('/api/v2/test');
|
||||
|
||||
expect(response2.statusCode).toBe(200);
|
||||
expect(response2.headers).not.toHaveProperty('x-api-version');
|
||||
});
|
||||
|
||||
it('should fail if the delete request body is not valid', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const test = new Router('/test').delete(
|
||||
'/',
|
||||
{
|
||||
typed: true,
|
||||
body: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['customProperty'],
|
||||
}),
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { customProperty: 'test' },
|
||||
};
|
||||
},
|
||||
);
|
||||
app.use(api.use(test).router);
|
||||
const response = await request(app).delete('/api/test').send({});
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('error', "must have required property 'customProperty'");
|
||||
});
|
||||
|
||||
it('should fail if the put request body is not valid', async () => {
|
||||
const ajv = new Ajv();
|
||||
const app = express();
|
||||
const api = new Router('/api');
|
||||
const test = new Router('/test').put(
|
||||
'/',
|
||||
{
|
||||
typed: true,
|
||||
body: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['customProperty'],
|
||||
}),
|
||||
response: {
|
||||
200: ajv.compile({
|
||||
type: 'object',
|
||||
properties: {
|
||||
customProperty: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { customProperty: 'test' },
|
||||
};
|
||||
},
|
||||
);
|
||||
app.use(api.use(test).router);
|
||||
const response = await request(app).put('/api/test').send({});
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toHaveProperty('error', "must have required property 'customProperty'");
|
||||
});
|
||||
});
|
||||
471
packages/http-router/src/Router.ts
Normal file
471
packages/http-router/src/Router.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import type { Method } from '@rocket.chat/rest-typings';
|
||||
import type { AnySchema } from 'ajv';
|
||||
import express from 'express';
|
||||
import type { Context, HonoRequest, MiddlewareHandler } from 'hono';
|
||||
import { Hono } from 'hono';
|
||||
import type { StatusCode } from 'hono/utils/http-status';
|
||||
|
||||
import type { ResponseSchema, TypedOptions } from './definition';
|
||||
import { honoAdapterForExpress } from './middlewares/honoAdapterForExpress';
|
||||
import { parseQueryParams } from './parseQueryParams';
|
||||
|
||||
const logger = new Logger('HttpRouter');
|
||||
|
||||
type MiddlewareHandlerListAndActionHandler<TOptions extends TypedOptions, TContext = (c: Context) => Promise<ResponseSchema<TOptions>>> = [
|
||||
...MiddlewareHandler[],
|
||||
TContext,
|
||||
];
|
||||
|
||||
function splitArray<T, U>(arr: [...T[], U]): [T[], U] {
|
||||
const last = arr[arr.length - 1];
|
||||
const rest = arr.slice(0, -1) as T[];
|
||||
return [rest, last as U];
|
||||
}
|
||||
|
||||
function coerceDatesToStrings(obj: unknown): unknown {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(coerceDatesToStrings);
|
||||
}
|
||||
if (obj && typeof obj === 'object') {
|
||||
const newObj: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value instanceof Date) {
|
||||
newObj[key] = value.toISOString();
|
||||
} else {
|
||||
newObj[key] = coerceDatesToStrings(value);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export type Route = {
|
||||
responses: Record<
|
||||
number,
|
||||
{
|
||||
description: string;
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AnySchema;
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
parameters?: {
|
||||
schema: AnySchema;
|
||||
in: 'query';
|
||||
name: 'query';
|
||||
required: true;
|
||||
}[];
|
||||
requestBody?: {
|
||||
required: true;
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AnySchema;
|
||||
};
|
||||
};
|
||||
};
|
||||
security?: {
|
||||
userId: [];
|
||||
authToken: [];
|
||||
}[];
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
export abstract class AbstractRouter<TActionCallback = (c: Context) => Promise<ResponseSchema<TypedOptions>>> {
|
||||
protected abstract convertActionToHandler(action: TActionCallback, logger: Logger): (c: Context) => Promise<ResponseSchema<TypedOptions>>;
|
||||
}
|
||||
|
||||
type InnerRouter = Hono<{
|
||||
Variables: {
|
||||
remoteAddress: string;
|
||||
bodyParams: Record<string, unknown>;
|
||||
queryParams: Record<string, unknown>;
|
||||
};
|
||||
}>;
|
||||
|
||||
export class Router<
|
||||
TBasePath extends string,
|
||||
TOperations extends {
|
||||
[x: string]: unknown;
|
||||
} = NonNullable<unknown>,
|
||||
TActionCallback = (c: Context) => Promise<ResponseSchema<TypedOptions>>,
|
||||
> extends AbstractRouter<TActionCallback> {
|
||||
protected innerRouter: InnerRouter;
|
||||
|
||||
constructor(readonly base: TBasePath) {
|
||||
super();
|
||||
this.innerRouter = new Hono();
|
||||
}
|
||||
|
||||
public typedRoutes: Record<string, Record<string, Route>> = {};
|
||||
|
||||
protected registerTypedRoutes<
|
||||
TSubPathPattern extends string,
|
||||
TOptions extends TypedOptions,
|
||||
TPathPattern extends `${TBasePath}/${TSubPathPattern}`,
|
||||
>(method: Method, subpath: TSubPathPattern, options: TOptions): void {
|
||||
const path = `/${this.base}/${subpath}`.replaceAll('//', '/') as TPathPattern;
|
||||
this.typedRoutes = this.typedRoutes || {};
|
||||
this.typedRoutes[path] = this.typedRoutes[path] || {};
|
||||
const { query, response = {}, authRequired, body, tags, ...rest } = options;
|
||||
this.typedRoutes[path][method.toLowerCase()] = {
|
||||
responses: Object.fromEntries(
|
||||
Object.entries(response).map(([status, schema]) => [
|
||||
parseInt(status, 10),
|
||||
{
|
||||
description: '',
|
||||
content: {
|
||||
'application/json': { schema: 'schema' in schema ? schema.schema : schema },
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
...(query && {
|
||||
parameters: [
|
||||
{
|
||||
schema: query.schema,
|
||||
in: 'query',
|
||||
name: 'query',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
...(body && {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': { schema: body.schema },
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(authRequired && {
|
||||
...rest,
|
||||
security: [
|
||||
{
|
||||
userId: [],
|
||||
authToken: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
tags,
|
||||
};
|
||||
}
|
||||
|
||||
protected async parseBodyParams({ request }: { request: HonoRequest }): Promise<NonNullable<unknown>> {
|
||||
try {
|
||||
const contentType = request.header('content-type') || '';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
return (await request.raw.clone().json()) as NonNullable<unknown>;
|
||||
}
|
||||
|
||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const req = await request.raw.clone().formData();
|
||||
return Object.fromEntries(req.entries());
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch {
|
||||
// No problem if there is error, just means the endpoint is going to have to parse the body itself if necessary
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
protected parseQueryParams(request: HonoRequest) {
|
||||
return parseQueryParams(request.raw.url.split('?')?.[1] || '');
|
||||
}
|
||||
|
||||
protected method<TSubPathPattern extends string, TOptions extends TypedOptions>(
|
||||
method: Method,
|
||||
subpath: TSubPathPattern,
|
||||
options: TOptions,
|
||||
...actions: MiddlewareHandlerListAndActionHandler<TOptions, TActionCallback>
|
||||
): Router<TBasePath, TOperations, TActionCallback> {
|
||||
const [middlewares, action] = splitArray<MiddlewareHandler, TActionCallback>(actions);
|
||||
const convertedAction = this.convertActionToHandler(action, logger);
|
||||
|
||||
const path = `/${subpath}`.replace('//', '/');
|
||||
(
|
||||
this.innerRouter[method.toLowerCase() as Lowercase<Method>] as (
|
||||
path: string,
|
||||
...handlers: Array<MiddlewareHandler | ((c: Context) => Promise<ResponseSchema<TypedOptions>>)>
|
||||
) => InnerRouter
|
||||
)(path, ...middlewares, async (c: Context) => {
|
||||
const { req, res } = c;
|
||||
|
||||
let queryParams: Record<string, any>;
|
||||
try {
|
||||
queryParams = this.parseQueryParams(req);
|
||||
c.set('queryParams', queryParams);
|
||||
} catch (e) {
|
||||
logger.warn({ msg: 'Error parsing query params for request', path: req.path, err: e });
|
||||
|
||||
return c.json({ success: false, error: 'Invalid query parameters' }, 400);
|
||||
}
|
||||
|
||||
if (options.query) {
|
||||
const validatorFn = options.query;
|
||||
if (typeof options.query === 'function' && !validatorFn(queryParams)) {
|
||||
logger.warn({
|
||||
msg: 'Query parameters validation failed - route spec does not match request payload',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
|
||||
bodyParams: undefined,
|
||||
queryParams,
|
||||
});
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
errorType: 'error-invalid-params',
|
||||
error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const bodyParams = await this.parseBodyParams({ request: req });
|
||||
c.set('bodyParams', bodyParams);
|
||||
|
||||
if (options.body) {
|
||||
const validatorFn = options.body;
|
||||
if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || bodyParams)) {
|
||||
logger.warn({
|
||||
msg: 'Request body validation failed - route spec does not match request payload',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
|
||||
bodyParams,
|
||||
queryParams: undefined,
|
||||
});
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
errorType: 'invalid-params',
|
||||
error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await convertedAction(c);
|
||||
const { body, statusCode, headers } = response as {
|
||||
body: any;
|
||||
statusCode: number;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) {
|
||||
const responseValidatorFn = options?.response?.[statusCode as keyof typeof options.response];
|
||||
/* c8 ignore next 3 */
|
||||
if (!responseValidatorFn && options.typed) {
|
||||
throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`);
|
||||
}
|
||||
if (responseValidatorFn && !responseValidatorFn(coerceDatesToStrings(body))) {
|
||||
const errorMessage = responseValidatorFn.errors
|
||||
?.map((error: any) => {
|
||||
const pathDesc = error.instancePath ? `at path '${error.instancePath}'` : "at path '(root)'";
|
||||
const paramsStr = Object.keys(error.params || {}).length
|
||||
? ` (${Object.entries(error.params)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(', ')})`
|
||||
: '';
|
||||
return `${pathDesc}: ${error.message}${paramsStr}`;
|
||||
})
|
||||
.join('\n');
|
||||
logger.warn({
|
||||
msg: 'Response validation failed - response does not match route spec',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
error: errorMessage,
|
||||
originalResponse: body,
|
||||
});
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
body,
|
||||
errorType: 'error-invalid-body',
|
||||
error: `Invalid response for endpoint ${req.method} - ${req.url}. Error: ${errorMessage}`,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const responseHeaders = Object.fromEntries(
|
||||
Object.entries({
|
||||
...res.headers,
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'Pragma': 'no-cache',
|
||||
...headers,
|
||||
}).map(([key, value]) => [key.toLowerCase(), value]),
|
||||
);
|
||||
|
||||
const contentType = (responseHeaders['content-type'] || 'application/json') as string;
|
||||
|
||||
const isContentLess = (statusCode: number): statusCode is 101 | 204 | 205 | 304 => {
|
||||
return [101, 204, 205, 304].includes(statusCode);
|
||||
};
|
||||
|
||||
if (isContentLess(statusCode)) {
|
||||
return c.status(statusCode);
|
||||
}
|
||||
Object.entries(responseHeaders).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
c.header(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
return c.body(contentType?.match(/json|javascript/) ? JSON.stringify(body) : body, statusCode as StatusCode);
|
||||
});
|
||||
this.registerTypedRoutes(method, subpath, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected convertActionToHandler(action: TActionCallback, _logger: Logger): (c: Context) => Promise<ResponseSchema<TypedOptions>> {
|
||||
// Default implementation simply passes through the action
|
||||
// Subclasses can override this to provide custom handling
|
||||
return action as (c: Context) => Promise<ResponseSchema<TypedOptions>>;
|
||||
}
|
||||
|
||||
get<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
|
||||
subpath: TSubPathPattern,
|
||||
options: TOptions,
|
||||
...action: MiddlewareHandlerListAndActionHandler<TOptions, TActionCallback>
|
||||
): Router<
|
||||
TBasePath,
|
||||
| TOperations
|
||||
| ({
|
||||
method: 'GET';
|
||||
path: TPathPattern;
|
||||
} & Omit<TOptions, 'response'>),
|
||||
TActionCallback
|
||||
> {
|
||||
return this.method('GET', subpath, options, ...action);
|
||||
}
|
||||
|
||||
post<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
|
||||
subpath: TSubPathPattern,
|
||||
options: TOptions,
|
||||
...action: MiddlewareHandlerListAndActionHandler<TOptions, TActionCallback>
|
||||
): Router<
|
||||
TBasePath,
|
||||
| TOperations
|
||||
| ({
|
||||
method: 'POST';
|
||||
path: TPathPattern;
|
||||
} & Omit<TOptions, 'response'>),
|
||||
TActionCallback
|
||||
> {
|
||||
return this.method('POST', subpath, options, ...action);
|
||||
}
|
||||
|
||||
put<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
|
||||
subpath: TSubPathPattern,
|
||||
options: TOptions,
|
||||
...action: MiddlewareHandlerListAndActionHandler<TOptions, TActionCallback>
|
||||
): Router<
|
||||
TBasePath,
|
||||
| TOperations
|
||||
| ({
|
||||
method: 'PUT';
|
||||
path: TPathPattern;
|
||||
} & Omit<TOptions, 'response'>),
|
||||
TActionCallback
|
||||
> {
|
||||
return this.method('PUT', subpath, options, ...action);
|
||||
}
|
||||
|
||||
delete<TSubPathPattern extends string, TOptions extends TypedOptions, TPathPattern extends `${TBasePath}/${TSubPathPattern}`>(
|
||||
subpath: TSubPathPattern,
|
||||
options: TOptions,
|
||||
...action: MiddlewareHandlerListAndActionHandler<TOptions, TActionCallback>
|
||||
): Router<
|
||||
TBasePath,
|
||||
| TOperations
|
||||
| ({
|
||||
method: 'DELETE';
|
||||
path: TPathPattern;
|
||||
} & Omit<TOptions, 'response'>),
|
||||
TActionCallback
|
||||
> {
|
||||
return this.method('DELETE', subpath, options, ...action);
|
||||
}
|
||||
|
||||
use<FN extends MiddlewareHandler>(fn: FN): Router<TBasePath, TOperations, TActionCallback>;
|
||||
|
||||
use<IRouter extends Router<any, any, any>>(
|
||||
innerRouter: IRouter,
|
||||
): IRouter extends Router<any, infer IOperations, any>
|
||||
? Router<TBasePath, ConcatPathOptions<TBasePath, IOperations, TOperations>, TActionCallback>
|
||||
: never;
|
||||
|
||||
use(innerRouter: unknown): any {
|
||||
if (innerRouter instanceof Router) {
|
||||
this.typedRoutes = {
|
||||
...this.typedRoutes,
|
||||
...Object.fromEntries(Object.entries(innerRouter.typedRoutes).map(([path, routes]) => [`${this.base}${path}`, routes])),
|
||||
};
|
||||
|
||||
this.innerRouter.route(innerRouter.base, innerRouter.innerRouter);
|
||||
}
|
||||
if (typeof innerRouter === 'function') {
|
||||
this.innerRouter.use(innerRouter as any);
|
||||
}
|
||||
return this as any;
|
||||
}
|
||||
|
||||
get router(): express.Router {
|
||||
// eslint-disable-next-line new-cap
|
||||
const router = express.Router();
|
||||
const hono = new Hono();
|
||||
router.use(
|
||||
this.base,
|
||||
honoAdapterForExpress(
|
||||
hono.route(this.base, this.innerRouter).options('*', (c) => {
|
||||
return c.body('OK');
|
||||
}),
|
||||
),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
getHonoRouter(): InnerRouter {
|
||||
return this.innerRouter;
|
||||
}
|
||||
}
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {};
|
||||
|
||||
type ConcatPathOptions<
|
||||
TPath extends string,
|
||||
TOptions extends {
|
||||
[x: string]: unknown;
|
||||
},
|
||||
TOther extends {
|
||||
[x: string]: unknown;
|
||||
},
|
||||
> = Prettify<
|
||||
Filter<
|
||||
{
|
||||
[x in keyof TOptions]: x extends 'path' ? (TOptions[x] extends string ? `${TPath}${TOptions[x]}` : never) : TOptions[x];
|
||||
} & TOther
|
||||
>
|
||||
>;
|
||||
|
||||
type Filter<
|
||||
TOther extends {
|
||||
[x: string]: unknown;
|
||||
},
|
||||
> = TOther extends { method: Method; path: string } ? TOther : never;
|
||||
|
||||
export type ExtractRouterEndpoints<TRoute extends Router<any, any, any>> =
|
||||
TRoute extends Router<any, infer TOperations, any> ? TOperations : never;
|
||||
57
packages/http-router/src/definition.ts
Normal file
57
packages/http-router/src/definition.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { LicenseModule } from '@rocket.chat/core-typings';
|
||||
import type { ValidateFunction } from 'ajv';
|
||||
import type { Request } from 'express';
|
||||
|
||||
type Range<N extends number, Result extends number[] = []> = Result['length'] extends N
|
||||
? Result[number]
|
||||
: Range<N, [...Result, Result['length']]>;
|
||||
|
||||
export type SuccessStatusCodes = Exclude<Range<208>, Range<200>>;
|
||||
|
||||
export type RedirectStatusCodes = Exclude<Range<308>, Range<300>>;
|
||||
|
||||
export type AuthorizationStatusCodes = Exclude<Range<451>, Range<400>>;
|
||||
|
||||
export type ErrorStatusCodes = Exclude<Exclude<Range<511>, Range<500>>, 509>;
|
||||
|
||||
type HTTPStatusCodes = SuccessStatusCodes | RedirectStatusCodes | AuthorizationStatusCodes | ErrorStatusCodes;
|
||||
|
||||
export type ResponseSchema<T extends TypedOptions> = {
|
||||
[K in keyof T['response']]: K extends HTTPStatusCodes
|
||||
? {
|
||||
statusCode: K;
|
||||
body: Parameters<NonNullable<T['response'][K]>>[0];
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
: never;
|
||||
}[keyof T['response']];
|
||||
|
||||
export type TypedRequest = Request & {
|
||||
response: {
|
||||
setHeader: (name: string, value: string) => void;
|
||||
};
|
||||
routePath: string;
|
||||
queryParams: Record<string, any>;
|
||||
bodyParams: Record<string, any>;
|
||||
urlParams: Record<string, any>;
|
||||
requestIp: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TypedResponse = {
|
||||
statusCode?: number;
|
||||
body: any;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type TypedOptions = {
|
||||
response: {
|
||||
[K in HTTPStatusCodes]?: ValidateFunction;
|
||||
};
|
||||
query?: ValidateFunction;
|
||||
body?: ValidateFunction;
|
||||
tags?: string[];
|
||||
typed?: boolean;
|
||||
license?: LicenseModule[];
|
||||
authRequired?: boolean;
|
||||
};
|
||||
3
packages/http-router/src/index.ts
Normal file
3
packages/http-router/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Router';
|
||||
export type * from './definition';
|
||||
export * from './middlewares/honoAdapterForExpress';
|
||||
@@ -0,0 +1,219 @@
|
||||
import { Readable, PassThrough } from 'node:stream';
|
||||
|
||||
import type { Request, Response as ExpressResponse } from 'express';
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
import { honoAdapterForExpress } from './honoAdapterForExpress';
|
||||
|
||||
const stringToReadableStream = (str: string): ReadableStream<Uint8Array> => {
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(str));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const bufferToReadableStream = (buffer: Buffer): ReadableStream<Uint8Array> => {
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array(buffer));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createMockResponse = () => {
|
||||
const passthrough = new PassThrough();
|
||||
const chunks: Buffer[] = [];
|
||||
passthrough.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
|
||||
const mockResBase = passthrough as any;
|
||||
mockResBase.status = jest.fn().mockReturnThis();
|
||||
mockResBase.setHeader = jest.fn().mockReturnThis();
|
||||
mockResBase.send = jest.fn().mockReturnThis();
|
||||
mockResBase.getHeader = jest.fn();
|
||||
mockResBase.getHeaders = jest.fn();
|
||||
|
||||
const getOutputBuffer = () => Buffer.concat(chunks);
|
||||
const getOutputString = (encoding: BufferEncoding = 'utf-8') => getOutputBuffer().toString(encoding);
|
||||
|
||||
return { mockRes: mockResBase as ExpressResponse, getOutputBuffer, getOutputString };
|
||||
};
|
||||
|
||||
describe('honoAdapterForExpress', () => {
|
||||
let mockHono: Hono;
|
||||
let mockExpressRequest: Request;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHono = {
|
||||
request: jest.fn(),
|
||||
} as unknown as Hono;
|
||||
|
||||
mockExpressRequest = {
|
||||
originalUrl: '/test',
|
||||
method: 'GET',
|
||||
headers: { 'x-test-header': 'test-value' },
|
||||
body: undefined,
|
||||
on: jest.fn(),
|
||||
readable: true,
|
||||
socket: { encrypted: false } as any,
|
||||
} as unknown as Request;
|
||||
(mockExpressRequest as any).duplex = 'half';
|
||||
});
|
||||
|
||||
it('should adapt a Hono response with JSON body', async () => {
|
||||
const jsonData = { message: 'hello' };
|
||||
const jsonString = JSON.stringify(jsonData);
|
||||
const mockHonoResponse = new Response(stringToReadableStream(jsonString), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes, getOutputString } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect(mockHono.request).toHaveBeenCalledWith(
|
||||
'/test',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.any(Headers),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.setHeader).toHaveBeenCalledWith('content-type', 'application/json');
|
||||
|
||||
await new Promise((resolve) => (mockRes as any).on('finish', resolve));
|
||||
expect(getOutputString()).toBe(jsonString);
|
||||
});
|
||||
|
||||
it('should adapt a Hono response with text body', async () => {
|
||||
const textData = 'Hello, world!';
|
||||
const mockHonoResponse = new Response(stringToReadableStream(textData), {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
});
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes, getOutputString } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.setHeader).toHaveBeenCalledWith('content-type', 'text/plain');
|
||||
|
||||
await new Promise((resolve) => (mockRes as any).on('finish', resolve));
|
||||
expect(getOutputString()).toBe(textData);
|
||||
});
|
||||
|
||||
it('should adapt a Hono response with image (Buffer) body', async () => {
|
||||
const imageBuffer = Buffer.from([1, 2, 3, 4, 5]);
|
||||
const mockHonoResponse = new Response(bufferToReadableStream(imageBuffer), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/png' },
|
||||
});
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes, getOutputBuffer } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.setHeader).toHaveBeenCalledWith('content-type', 'image/png');
|
||||
|
||||
await new Promise((resolve) => (mockRes as any).on('finish', resolve));
|
||||
expect(getOutputBuffer()).toEqual(imageBuffer);
|
||||
});
|
||||
|
||||
it('should adapt a Hono response with no body', async () => {
|
||||
const mockHonoResponse = new Response(null, {
|
||||
status: 204,
|
||||
headers: { 'x-custom-header': 'empty' },
|
||||
});
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes, getOutputString } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(204);
|
||||
expect(mockRes.setHeader).toHaveBeenCalledWith('x-custom-header', 'empty');
|
||||
|
||||
await new Promise((resolve) => (mockRes as any).on('finish', resolve));
|
||||
expect(getOutputString()).toBe('');
|
||||
});
|
||||
|
||||
it('should handle POST request with body', async () => {
|
||||
const postBody = { data: 'some data' };
|
||||
const postBodyString = JSON.stringify(postBody);
|
||||
|
||||
const mockPostRequest = {
|
||||
...mockExpressRequest,
|
||||
method: 'POST',
|
||||
body: Readable.from(postBodyString) as any,
|
||||
headers: { 'content-type': 'application/json', 'content-length': String(postBodyString.length) },
|
||||
} as Request;
|
||||
(mockPostRequest as any).duplex = 'half';
|
||||
|
||||
const mockHonoResponse = new Response(stringToReadableStream('{"status":"ok"}'), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes, getOutputString } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
await adapter(mockPostRequest, mockRes);
|
||||
|
||||
expect(mockHono.request).toHaveBeenCalledWith(
|
||||
'/test',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: expect.anything(),
|
||||
headers: expect.any(Headers),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
const calledWithHeaders = (mockHono.request as jest.Mock).mock.calls[0][1].headers as Headers;
|
||||
expect(calledWithHeaders.get('content-type')).toBe('application/json');
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
|
||||
await new Promise((resolve) => (mockRes as any).on('finish', resolve));
|
||||
expect(getOutputString()).toBe('{"status":"ok"}');
|
||||
});
|
||||
|
||||
it('should correctly set duplex and handle isDisturbed for request stream', async () => {
|
||||
const mockHonoResponse = new Response(null, { status: 204 });
|
||||
(mockHono.request as jest.Mock).mockResolvedValue(mockHonoResponse);
|
||||
|
||||
const { mockRes } = createMockResponse();
|
||||
const adapter = honoAdapterForExpress(mockHono);
|
||||
|
||||
mockExpressRequest.on = jest.fn();
|
||||
Object.defineProperty(mockExpressRequest, 'readableFlowing', { value: null, writable: true });
|
||||
|
||||
jest.spyOn(Readable, 'isDisturbed').mockReturnValue(false);
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect((mockExpressRequest as any).duplex).toBe('half');
|
||||
expect(mockHono.request).toHaveBeenCalledTimes(1);
|
||||
|
||||
(mockHono.request as jest.Mock).mockClear();
|
||||
jest.spyOn(Readable, 'isDisturbed').mockReturnValue(true);
|
||||
await adapter(mockExpressRequest, mockRes);
|
||||
|
||||
expect(mockHono.request).not.toHaveBeenCalled();
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
export const honoAdapterForExpress = (hono: Hono) => async (expressReq: Request, res: Response) => {
|
||||
(expressReq as unknown as any).duplex = 'half';
|
||||
|
||||
if (Readable.isDisturbed(expressReq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { body, ...req } = expressReq;
|
||||
|
||||
const honoRes = await hono.request(
|
||||
expressReq.originalUrl,
|
||||
{
|
||||
...req,
|
||||
...(['POST', 'PUT', 'DELETE'].includes(expressReq.method) && { body: expressReq as unknown as ReadableStream }),
|
||||
headers: new Headers(Object.fromEntries(Object.entries(expressReq.headers)) as Record<string, string>),
|
||||
},
|
||||
{
|
||||
incoming: expressReq,
|
||||
},
|
||||
);
|
||||
res.status(honoRes.status);
|
||||
honoRes.headers.forEach((value, key) => res.setHeader(key, value));
|
||||
|
||||
if (!honoRes.body) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (honoRes.body instanceof ReadableStream) {
|
||||
Readable.fromWeb(honoRes.body as any).pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Converting it to a Buffer because res.send appends always a charset to the Content-Type
|
||||
// https://github.com/expressjs/express/issues/2238
|
||||
res.send(Buffer.from(await honoRes.text()));
|
||||
};
|
||||
60
packages/http-router/src/parseQueryParams.test.ts
Normal file
60
packages/http-router/src/parseQueryParams.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { parseQueryParams } from './parseQueryParams';
|
||||
|
||||
describe('parseQueryParams', () => {
|
||||
it('should parse simple query string', () => {
|
||||
const result = parseQueryParams('foo=bar');
|
||||
expect(result).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('should parse multiple query parameters', () => {
|
||||
const result = parseQueryParams('foo=bar&baz=qux');
|
||||
expect(result).toEqual({ foo: 'bar', baz: 'qux' });
|
||||
});
|
||||
|
||||
it('should parse array parameters', () => {
|
||||
const result = parseQueryParams('ids[]=1&ids[]=2&ids[]=3');
|
||||
expect(result).toEqual({ ids: ['1', '2', '3'] });
|
||||
});
|
||||
|
||||
it('should parse nested objects', () => {
|
||||
const result = parseQueryParams('user[name]=john&user[age]=30');
|
||||
expect(result).toEqual({ user: { name: 'john', age: '30' } });
|
||||
});
|
||||
|
||||
it('should handle empty query string', () => {
|
||||
const result = parseQueryParams('');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should decode URL encoded values', () => {
|
||||
const result = parseQueryParams('name=John%20Doe');
|
||||
expect(result).toEqual({ name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should handle boolean-like values as strings', () => {
|
||||
const result = parseQueryParams('active=true&disabled=false');
|
||||
expect(result).toEqual({ active: 'true', disabled: 'false' });
|
||||
});
|
||||
|
||||
it('should throw error when array limit is exceeded', () => {
|
||||
const largeArray = Array(501)
|
||||
.fill(0)
|
||||
.map((_, i) => `ids[]=${i}`)
|
||||
.join('&');
|
||||
expect(() => parseQueryParams(largeArray)).toThrow();
|
||||
});
|
||||
|
||||
it('should parse arrays within the limit', () => {
|
||||
const array = Array(500)
|
||||
.fill(0)
|
||||
.map((_, i) => `ids[]=${i}`)
|
||||
.join('&');
|
||||
const result = parseQueryParams(array);
|
||||
expect(result.ids).toHaveLength(500);
|
||||
});
|
||||
|
||||
it('should parse as array even without brackets', () => {
|
||||
const result = parseQueryParams('ids=1&ids=2');
|
||||
expect(result.ids).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
5
packages/http-router/src/parseQueryParams.ts
Normal file
5
packages/http-router/src/parseQueryParams.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import qs from 'qs'; // Using qs specifically to keep express compatibility
|
||||
|
||||
export function parseQueryParams(url: string) {
|
||||
return qs.parse(url, { arrayLimit: 500, throwOnLimitExceeded: true });
|
||||
}
|
||||
10
packages/http-router/tsconfig.json
Normal file
10
packages/http-router/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/server.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext"],
|
||||
"declaration": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user