├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .node-version ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── README_jp.md ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── src ├── createClient.ts ├── createManagementClient.ts ├── index.ts ├── lib │ └── fetch.ts ├── types.ts └── utils │ ├── constants.ts │ ├── isCheckValue.ts │ └── parseQuery.ts ├── tests ├── createClient.test.ts ├── createManagementClient.test.ts ├── get.test.ts ├── getAllContentIds.test.ts ├── getAllContents.test.ts ├── lib │ └── fetch.test.ts ├── mocks │ ├── handlers.ts │ └── server.ts ├── requestInit.test.ts ├── uploadMedia.test.ts ├── utils │ ├── isCheckValue.test.ts │ └── parseQuery.test.ts └── write.test.ts ├── tsconfig.json └── tsup.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'prettier', 7 | ], 8 | rules: { 9 | '@typescript-eslint/explicit-function-return-type': 'off', 10 | '@typescript-eslint/no-explicit-any': 'off', 11 | '@typescript-eslint/no-empty-interface': 'off', 12 | '@typescript-eslint/no-inferrable-types': 'off', 13 | '@typescript-eslint/no-non-null-assertion': 'off', 14 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 15 | '@typescript-eslint/no-triple-slash-reference': 'off', 16 | '@typescript-eslint/no-unused-vars': 'off', 17 | '@typescript-eslint/no-var-requires': 'off', 18 | '@typescript-eslint/prefer-interface': 'off', 19 | '@typescript-eslint/prefer-namespace-keyword': 'off', 20 | '@typescript-eslint/explicit-module-boundary-types': 'off', 21 | }, 22 | env: { 23 | browser: true, 24 | node: true, 25 | es6: true, 26 | jest: true, 27 | }, 28 | overrides: [ 29 | { 30 | files: ['tests/**'], 31 | plugins: ['jest'], 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - run: npm ci 29 | - run: npm run build 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '18.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - name: release on npm 18 | run: | 19 | npm ci 20 | npm run lint 21 | npm run test 22 | npm run build 23 | - run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v18.19.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microCMS JavaScript SDK 2 | 3 | [日本語版 README](README_jp.md) 4 | 5 | It helps you to use microCMS from JavaScript and Node.js applications. 6 | 7 | Discord 8 | 9 | ## Tutorial 10 | 11 | See the [official tutorial](https://document.microcms.io/tutorial/javascript/javascript-top). 12 | 13 | ## Getting started 14 | 15 | ### Installation 16 | 17 | #### Node.js 18 | 19 | ```bash 20 | $ npm install microcms-js-sdk 21 | 22 | or 23 | 24 | $ yarn add microcms-js-sdk 25 | ``` 26 | 27 | > [!IMPORTANT] 28 | > v3.0.0 or later requires Node.js **v18 or higher**. 29 | 30 | #### Browser(Self-hosting) 31 | 32 | Download and unzip `microcms-js-sdk-x.y.z.tgz` from the [releases page](https://github.com/microcmsio/microcms-js-sdk/releases). Then, host it on any server of your choice and use it. The target file is `./dist/umd/microcms-js-sdk.js`. 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | #### Browser(CDN) 39 | 40 | Please load and use the URL provided by an external provider. 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | or 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | > [!WARNING] 53 | > The hosting service (cdn.jsdelivr.net) is not related to microCMS. For production use, we recommend self-hosting on your own server. 54 | 55 | ## Contents API 56 | 57 | ### Import 58 | 59 | #### Node.js 60 | 61 | ```javascript 62 | const { createClient } = require('microcms-js-sdk'); // CommonJS 63 | ``` 64 | 65 | or 66 | 67 | ```javascript 68 | import { createClient } from 'microcms-js-sdk'; //ES6 69 | ``` 70 | 71 | #### Usage with a browser 72 | 73 | ```html 74 | 77 | ``` 78 | 79 | ### Create client object 80 | 81 | ```javascript 82 | // Initialize Client SDK. 83 | const client = createClient({ 84 | serviceDomain: 'YOUR_DOMAIN', // YOUR_DOMAIN is the XXXX part of XXXX.microcms.io 85 | apiKey: 'YOUR_API_KEY', 86 | // retry: true // Retry attempts up to a maximum of two times. 87 | }); 88 | ``` 89 | 90 | ### API methods 91 | 92 | The table below shows each API method of microCMS JavaScript SDK and indicates which API format (List Format or Object Format) they can be used with using ✔️. 93 | 94 | | Method | List Format | Object Format | 95 | |-------------------|-------------|---------------| 96 | | getList | ✔️ | | 97 | | getListDetail | ✔️ | | 98 | | getObject | | ✔️ | 99 | | getAllContentIds | ✔️ | | 100 | | getAllContents | ✔️ | | 101 | | create | ✔️ | | 102 | | update | ✔️ | ✔️ | 103 | | delete | ✔️ | | 104 | 105 | > [!NOTE] 106 | > - ✔️ in "List Format" indicates the method can be used when the API type is set to List Format. 107 | > - ✔️ in "Object Format" indicates the method can be used when the API type is set to Object Format. 108 | 109 | ### Get content list 110 | 111 | The `getList` method is used to retrieve a list of content from a specified endpoint. 112 | 113 | ```javascript 114 | client 115 | .getList({ 116 | endpoint: 'endpoint', 117 | }) 118 | .then((res) => console.log(res)) 119 | .catch((err) => console.error(err)); 120 | ``` 121 | 122 | #### Get content list with parameters 123 | 124 | The `queries` property can be used to specify parameters for retrieving content that matches specific criteria. For more details on each available property, refer to the [microCMS Documentation](https://document.microcms.io/content-api/get-list-contents#h929d25d495). 125 | 126 | ```javascript 127 | client 128 | .getList({ 129 | endpoint: 'endpoint', 130 | queries: { 131 | draftKey: 'abcd', 132 | limit: 100, 133 | offset: 1, 134 | orders: 'createdAt', 135 | q: 'Hello', 136 | fields: 'id,title', 137 | ids: 'foo', 138 | filters: 'publishedAt[greater_than]2021-01-01T03:00:00.000Z', 139 | depth: 1, 140 | } 141 | }) 142 | .then((res) => console.log(res)) 143 | .catch((err) => console.error(err)); 144 | ``` 145 | 146 | ### Get single content 147 | 148 | The `getListDetail` method is used to retrieve a single content specified by its ID. 149 | 150 | ```javascript 151 | client 152 | .getListDetail({ 153 | endpoint: 'endpoint', 154 | contentId: 'contentId', 155 | }) 156 | .then((res) => console.log(res)) 157 | .catch((err) => console.error(err)); 158 | ``` 159 | 160 | #### Get single content with parameters 161 | 162 | The `queries` property can be used to specify parameters for retrieving a single content that matches specific criteria. For more details on each available property, refer to the [microCMS Documentation](https://document.microcms.io/content-api/get-content#h929d25d495). 163 | 164 | ```javascript 165 | client 166 | .getListDetail({ 167 | endpoint: 'endpoint', 168 | contentId: 'contentId', 169 | queries: { 170 | draftKey: 'abcd', 171 | fields: 'id,title', 172 | depth: 1, 173 | } 174 | }) 175 | .then((res) => console.log(res)) 176 | .catch((err) => console.error(err)); 177 | 178 | ``` 179 | 180 | ### Get object format content 181 | 182 | The `getObject` method is used to retrieve a single object format content 183 | 184 | ```javascript 185 | client 186 | .getObject({ 187 | endpoint: 'endpoint', 188 | }) 189 | .then((res) => console.log(res)) 190 | .catch((err) => console.error(err)); 191 | ``` 192 | 193 | ### Get all contentIds 194 | 195 | The `getAllContentIds` method is used to retrieve all content IDs only. 196 | 197 | ```javascript 198 | client 199 | .getAllContentIds({ 200 | endpoint: 'endpoint', 201 | }) 202 | .then((res) => console.log(res)) 203 | .catch((err) => console.error(err)); 204 | ``` 205 | 206 | #### Get all contentIds with filters 207 | 208 | It is possible to retrieve only the content IDs for a specific category by specifying the `filters`. 209 | 210 | ```javascript 211 | client 212 | .getAllContentIds({ 213 | endpoint: 'endpoint', 214 | filters: 'category[equals]uN28Folyn', 215 | }) 216 | .then((res) => console.log(res)) 217 | .catch((err) => console.error(err)); 218 | ``` 219 | 220 | #### Get all contentIds with draftKey 221 | 222 | It is possible to include content from a specific draft by specifying the `draftKey`. 223 | 224 | ```javascript 225 | client 226 | .getAllContentIds({ 227 | endpoint: 'endpoint', 228 | draftKey: 'draftKey', 229 | }) 230 | .then((res) => console.log(res)) 231 | .catch((err) => console.error(err)); 232 | ``` 233 | 234 | #### Get all contentIds with alternateField 235 | 236 | The `alternateField` property can be used to address cases where the value of a field other than content ID is used in a URL, etc. 237 | 238 | ```javascript 239 | client 240 | .getAllContentIds({ 241 | endpoint: 'endpoint', 242 | alternateField: 'url', 243 | }) 244 | .then((res) => console.log(res)) 245 | .catch((err) => console.error(err)); 246 | ``` 247 | 248 | ### Get all contents 249 | 250 | The `getAllContents` method is used to retrieve all content data. 251 | 252 | ```javascript 253 | client 254 | .getAllContents({ 255 | endpoint: 'endpoint', 256 | }) 257 | .then((res) => console.log(res)) 258 | .catch((err) => console.error(err)); 259 | ``` 260 | 261 | #### Get all contents with parameters 262 | 263 | The `queries` property can be used to specify parameters for retrieving all content that matches specific criteria. For more details on each available property, refer to the [microCMS Documentation](https://document.microcms.io/content-api/get-list-contents#h929d25d495). 264 | 265 | ```javascript 266 | client 267 | .getAllContents({ 268 | endpoint: 'endpoint', 269 | queries: { filters: 'createdAt[greater_than]2021-01-01T03:00:00.000Z', orders: '-createdAt' }, 270 | }) 271 | .then((res) => console.log(res)) 272 | .catch((err) => console.error(err)); 273 | ``` 274 | 275 | ### Create content 276 | 277 | The `create` method is used to register content. 278 | 279 | ```javascript 280 | client 281 | .create({ 282 | endpoint: 'endpoint', 283 | content: { 284 | title: 'title', 285 | body: 'body', 286 | }, 287 | }) 288 | .then((res) => console.log(res.id)) 289 | .catch((err) => console.error(err)); 290 | ``` 291 | 292 | #### Create content with specified ID 293 | 294 | By specifying the `contentId` property, it is possible to register content with a specified ID. 295 | 296 | ```javascript 297 | client 298 | .create({ 299 | endpoint: 'endpoint', 300 | contentId: 'contentId', 301 | content: { 302 | title: 'title', 303 | body: 'body', 304 | }, 305 | }) 306 | .then((res) => console.log(res.id)) 307 | .catch((err) => console.error(err)); 308 | ``` 309 | 310 | #### Create draft content 311 | 312 | By specifying the `isDraft` property, it is possible to register the content as a draft. 313 | 314 | ```javascript 315 | client 316 | .create({ 317 | endpoint: 'endpoint', 318 | content: { 319 | title: 'title', 320 | body: 'body', 321 | }, 322 | // Available with microCMS paid plans 323 | // https://microcms.io/pricing 324 | isDraft: true, 325 | }) 326 | .then((res) => console.log(res.id)) 327 | .catch((err) => console.error(err)); 328 | ``` 329 | 330 | #### Create draft content with specified ID 331 | 332 | By specifying the `contentId` and `isDraft` properties, it is possible to register the content as a draft with a specified ID. 333 | 334 | ```javascript 335 | client 336 | .create({ 337 | endpoint: 'endpoint', 338 | contentId: 'contentId', 339 | content: { 340 | title: 'title', 341 | body: 'body', 342 | }, 343 | // Available with microCMS paid plans 344 | // https://microcms.io/pricing 345 | isDraft: true, 346 | }) 347 | .then((res) => console.log(res.id)) 348 | .catch((err) => console.error(err)); 349 | ``` 350 | 351 | ### Update content 352 | 353 | The `update` method is used to update a single content specified by its ID. 354 | 355 | ```javascript 356 | client 357 | .update({ 358 | endpoint: 'endpoint', 359 | contentId: 'contentId', 360 | content: { 361 | title: 'title', 362 | }, 363 | }) 364 | .then((res) => console.log(res.id)) 365 | .catch((err) => console.error(err)); 366 | ``` 367 | 368 | #### Update object format content 369 | 370 | When updating object content, use the `update` method without specifying a `contentId` property. 371 | 372 | ```javascript 373 | client 374 | .update({ 375 | endpoint: 'endpoint', 376 | content: { 377 | title: 'title', 378 | }, 379 | }) 380 | .then((res) => console.log(res.id)) 381 | .catch((err) => console.error(err)); 382 | ``` 383 | 384 | ### Delete content 385 | 386 | The `delete` method is used to delete a single content specified by its ID. 387 | 388 | ```javascript 389 | client 390 | .delete({ 391 | endpoint: 'endpoint', 392 | contentId: 'contentId', 393 | }) 394 | .catch((err) => console.error(err)); 395 | ``` 396 | 397 | ### TypeScript 398 | 399 | If you are using TypeScript, use `getList`, `getListDetail`, `getObject`. This internally contains a common type of content. 400 | 401 | #### Response type for getList method 402 | 403 | ```typescript 404 | type Content = { 405 | text: string, 406 | }; 407 | /** 408 | * { 409 | * contents: Content[]; // This is array type of Content 410 | * totalCount: number; 411 | * limit: number; 412 | * offset: number; 413 | * } 414 | */ 415 | client.getList({ /* other */ }) 416 | ``` 417 | 418 | #### Response type for getListDetail method 419 | 420 | ```typescript 421 | type Content = { 422 | text: string, 423 | }; 424 | /** 425 | * { 426 | * id: string; 427 | * createdAt: string; 428 | * updatedAt: string; 429 | * publishedAt?: string; 430 | * revisedAt?: string; 431 | * text: string; // This is Content type. 432 | * } 433 | */ 434 | client.getListDetail({ /* other */ }) 435 | ``` 436 | 437 | #### Response type for getObject method 438 | 439 | ```typescript 440 | type Content = { 441 | text: string, 442 | }; 443 | /** 444 | * { 445 | * createdAt: string; 446 | * updatedAt: string; 447 | * publishedAt?: string; 448 | * revisedAt?: string; 449 | * text: string; // This is Content type. 450 | * } 451 | */ 452 | 453 | client.getObject({ /* other */ }) 454 | ``` 455 | 456 | #### Response type for getAllContentIds method 457 | 458 | ```typescript 459 | /** 460 | * string[] // This is array type of string 461 | */ 462 | client.getAllContentIds({ /* other */ }) 463 | ``` 464 | 465 | #### Create method with type safety 466 | 467 | Since `content` will be of type `Content`, no required fields will be missed. 468 | 469 | ```typescript 470 | type Content = { 471 | title: string; 472 | body?: string; 473 | }; 474 | 475 | client.create({ 476 | endpoint: 'endpoint', 477 | content: { 478 | title: 'title', 479 | body: 'body', 480 | }, 481 | }); 482 | ``` 483 | 484 | #### Update method with type safety 485 | 486 | The `content` will be of type `Partial`, so you can enter only the items needed for the update. 487 | 488 | ```typescript 489 | type Content = { 490 | title: string; 491 | body?: string; 492 | }; 493 | 494 | client.update({ 495 | endpoint: 'endpoint', 496 | content: { 497 | body: 'body', 498 | }, 499 | }); 500 | ``` 501 | 502 | ### CustomRequestInit 503 | 504 | #### Next.js App Router 505 | 506 | You can now use the fetch option of the Next.js App Router as CustomRequestInit. 507 | Please refer to the official Next.js documentation as the available options depend on the Next.js Type file. 508 | 509 | [Functions: fetch \| Next\.js](https://nextjs.org/docs/app/api-reference/functions/fetch) 510 | 511 | ```ts 512 | const response = await client.getList({ 513 | customRequestInit: { 514 | next: { 515 | revalidate: 60, 516 | }, 517 | }, 518 | endpoint: 'endpoint', 519 | }); 520 | ``` 521 | 522 | #### AbortController: abort() method 523 | 524 | You can abort fetch requests. 525 | 526 | ```ts 527 | const controller = new AbortController(); 528 | const response = await client.getObject({ 529 | customRequestInit: { 530 | signal: controller.signal, 531 | }, 532 | endpoint: 'config', 533 | }); 534 | 535 | setTimeout(() => { 536 | controller.abort(); 537 | }, 1000); 538 | ``` 539 | 540 | ## Management API 541 | 542 | ### Import 543 | 544 | #### Node.js 545 | 546 | ```javascript 547 | const { createManagementClient } = require('microcms-js-sdk'); // CommonJS 548 | ``` 549 | 550 | or 551 | 552 | ```javascript 553 | import { createManagementClient } from 'microcms-js-sdk'; //ES6 554 | ``` 555 | 556 | #### Usage with a browser 557 | 558 | ```html 559 | 562 | ``` 563 | 564 | ### Create client object 565 | 566 | ```javascript 567 | const client = createManagementClient({ 568 | serviceDomain: 'YOUR_DOMAIN', // YOUR_DOMAIN is the XXXX part of XXXX.microcms.io 569 | apiKey: 'YOUR_API_KEY', 570 | }); 571 | ``` 572 | 573 | ### Upload media 574 | 575 | Media files can be uploaded using the 'POST /api/v1/media' endpoint of the Management API. 576 | 577 | #### Node.js 578 | 579 | ```javascript 580 | // Blob 581 | import { readFileSync } from 'fs'; 582 | 583 | const file = readFileSync('path/to/file'); 584 | client 585 | .uploadMedia({ 586 | data: new Blob([file], { type: 'image/png' }), 587 | name: 'image.png', 588 | }) 589 | .then((res) => console.log(res)) 590 | .catch((err) => console.error(err)); 591 | 592 | // or ReadableStream 593 | import { createReadStream } from 'fs'; 594 | import { Stream } from 'stream'; 595 | 596 | const file = createReadStream('path/to/file'); 597 | client 598 | .uploadMedia({ 599 | data: Stream.Readable.toWeb(file), 600 | name: 'image.png', 601 | type: 'image/png', 602 | }) 603 | .then((res) => console.log(res)) 604 | .catch((err) => console.error(err)); 605 | 606 | // or URL 607 | client 608 | .uploadMedia({ 609 | data: 'https://example.com/image.png', 610 | // name: 'image.png', ← Optional 611 | }) 612 | .then((res) => console.log(res)) 613 | .catch((err) => console.error(err)); 614 | ``` 615 | 616 | #### Browser 617 | 618 | ```javascript 619 | // File 620 | const file = document.querySelector('input[type="file"]').files[0]; 621 | client 622 | .uploadMedia({ 623 | data: file, 624 | }) 625 | .then((res) => console.log(res)) 626 | .catch((err) => console.error(err)); 627 | 628 | // or URL 629 | client 630 | .uploadMedia({ 631 | data: 'https://example.com/image.png', 632 | // name: 'image.png', ← Optional 633 | }) 634 | .then((res) => console.log(res)) 635 | .catch((err) => console.error(err)); 636 | ``` 637 | 638 | ### TypeScript 639 | 640 | #### Parameter type for uploadMedia method 641 | 642 | ```typescript 643 | type UploadMediaRequest = 644 | | { data: File } 645 | | { data: Blob; name: string } 646 | | { data: ReadableStream; name: string; type: `image/${string}` } 647 | | { 648 | data: URL | string; 649 | name?: string | null | undefined; 650 | customRequestHeaders?: HeadersInit; 651 | }; 652 | function uploadMedia(params: UploadMediaRequest): Promise<{ url: string }>; 653 | ``` 654 | 655 | ## Tips 656 | 657 | ### Separate API keys for read and write 658 | 659 | ```javascript 660 | const readClient = createClient({ 661 | serviceDomain: 'serviceDomain', 662 | apiKey: 'readApiKey', 663 | }); 664 | const writeClient = createClient({ 665 | serviceDomain: 'serviceDomain', 666 | apiKey: 'writeApiKey', 667 | }); 668 | ``` 669 | 670 | ## LICENSE 671 | 672 | Apache-2.0 673 | -------------------------------------------------------------------------------- /README_jp.md: -------------------------------------------------------------------------------- 1 | # microCMS JavaScript SDK 2 | 3 | [English README](README.md) 4 | 5 | JavaScriptやNode.jsのアプリケーションからmicroCMSのAPIと簡単に通信できます。 6 | 7 | Discord 8 | 9 | ## チュートリアル 10 | 11 | 公式ドキュメントの [チュートリアル](https://document.microcms.io/tutorial/javascript/javascript-top)をご覧ください。 12 | 13 | ## はじめに 14 | 15 | ### インストール 16 | 17 | #### Node.js 18 | 19 | ```bash 20 | $ npm install microcms-js-sdk 21 | 22 | または 23 | 24 | $ yarn add microcms-js-sdk 25 | ``` 26 | 27 | > [!IMPORTANT] 28 | > v3.0.0以上を使用する場合は、Node.jsのv18以上が必要です。 29 | 30 | #### ブラウザ(セルフホスティング) 31 | 32 | [リリースページ](https://github.com/microcmsio/microcms-js-sdk/releases)から`microcms-js-sdk-x.y.z.tgz`をダウンロードして解凍してください。その後、お好みのサーバーにアップロードして使用してください。対象ファイルは `./dist/umd/microcms-js-sdk.js` です。 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | #### ブラウザ(CDN) 39 | 40 | 外部プロバイダーが提供するURLを読み込んでご利用ください。 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | または 47 | 48 | ```html 49 | 50 | ``` 51 | 52 | > [!WARNING] 53 | > ホスティングサービス(cdn.jsdelivr.net)はmicroCMSとは関係ありません。本番環境でのご利用には、お客様のサーバーでのセルフホスティングをお勧めします。 54 | 55 | ## コンテンツAPI 56 | 57 | ### インポート 58 | 59 | #### Node.js 60 | 61 | ```javascript 62 | const { createClient } = require('microcms-js-sdk'); // CommonJS 63 | ``` 64 | 65 | または 66 | 67 | ```javascript 68 | import { createClient } from 'microcms-js-sdk'; //ES6 69 | ``` 70 | 71 | #### ブラウザ 72 | 73 | ```html 74 | 77 | ``` 78 | 79 | ### クライアントオブジェクトの作成 80 | 81 | ```javascript 82 | // クライアントオブジェクトを作成します。 83 | const client = createClient({ 84 | serviceDomain: 'YOUR_DOMAIN', // YOUR_DOMAINはXXXX.microcms.ioのXXXXの部分です。 85 | apiKey: 'YOUR_API_KEY', 86 | // retry: true // 最大2回まで再試行します。 87 | }); 88 | ``` 89 | 90 | ### APIメソッド 91 | 92 | 以下の表は、microCMS JavaScript SDKの各メソッドがリスト形式のAPIまたはオブジェクト形式のAPI、どちらで使用できるかを示しています。 93 | 94 | | メソッド | リスト形式 | オブジェクト形式 | 95 | |-------------------|-------------|---------------| 96 | | getList | ✔️ | | 97 | | getListDetail | ✔️ | | 98 | | getObject | | ✔️ | 99 | | getAllContentIds | ✔️ | | 100 | | getAllContents | ✔️ | | 101 | | create | ✔️ | | 102 | | update | ✔️ | ✔️ | 103 | | delete | ✔️ | | 104 | 105 | > [!NOTE] 106 | > - 「リスト形式」の✔️は、APIの型がリスト形式に設定されている場合に使用できるメソッドを示します。 107 | > - 「オブジェクト形式」の✔️は、APIの型がオブジェクト形式に設定されている場合に使用できるメソッドを示します。 108 | 109 | ### コンテンツ一覧の取得 110 | 111 | `getList`メソッドは、指定されたエンドポイントからコンテンツ一覧を取得するために使用します。 112 | 113 | ```javascript 114 | client 115 | .getList({ 116 | endpoint: 'endpoint', 117 | }) 118 | .then((res) => console.log(res)) 119 | .catch((err) => console.error(err)); 120 | ``` 121 | 122 | #### queriesプロパティを使用したコンテンツ一覧の取得 123 | 124 | `queries`プロパティを使用して、特定の条件に一致するコンテンツ一覧を取得できます。利用可能な各プロパティの詳細については、[microCMSのドキュメント](https://document.microcms.io/content-api/get-list-contents#h929d25d495)を参照してください。 125 | 126 | ```javascript 127 | client 128 | .getList({ 129 | endpoint: 'endpoint', 130 | queries: { 131 | draftKey: 'abcd', 132 | limit: 100, 133 | offset: 1, 134 | orders: 'createdAt', 135 | q: 'こんにちは', 136 | fields: 'id,title', 137 | ids: 'foo', 138 | filters: 'publishedAt[greater_than]2021-01-01T03:00:00.000Z', 139 | depth: 1, 140 | } 141 | }) 142 | .then((res) => console.log(res)) 143 | .catch((err) => console.error(err)); 144 | ``` 145 | 146 | ### 単一コンテンツの取得 147 | 148 | `getListDetail`メソッドは、指定されたエンドポイントから、IDで指定された単一コンテンツを取得するために使用します。 149 | 150 | ```javascript 151 | client 152 | .getListDetail({ 153 | endpoint: 'endpoint', 154 | contentId: 'contentId', 155 | }) 156 | .then((res) => console.log(res)) 157 | .catch((err) => console.error(err)); 158 | ``` 159 | 160 | #### queriesプロパティを使用した単一コンテンツの取得 161 | 162 | `queries`プロパティを使用して、特定の条件に一致する単一コンテンツを取得できます。利用可能な各プロパティの詳細については、[microCMSのドキュメント](https://document.microcms.io/content-api/get-content#h929d25d495)を参照してください。 163 | 164 | ```javascript 165 | client 166 | .getListDetail({ 167 | endpoint: 'endpoint', 168 | contentId: 'contentId', 169 | queries: { 170 | draftKey: 'abcd', 171 | fields: 'id,title', 172 | depth: 1, 173 | } 174 | }) 175 | .then((res) => console.log(res)) 176 | .catch((err) => console.error(err)); 177 | 178 | ``` 179 | 180 | ### オブジェクト形式のコンテンツの取得 181 | 182 | `getObject`メソッドは、指定されたエンドポイントからオブジェクト形式のコンテンツを取得するために使用します。 183 | 184 | ```javascript 185 | client 186 | .getObject({ 187 | endpoint: 'endpoint', 188 | }) 189 | .then((res) => console.log(res)) 190 | .catch((err) => console.error(err)); 191 | ``` 192 | 193 | ### コンテンツIDの全件取得 194 | 195 | `getAllContentIds`メソッドは、指定されたエンドポイントからコンテンツIDのみを全件取得するために使用します。 196 | 197 | ```javascript 198 | client 199 | .getAllContentIds({ 200 | endpoint: 'endpoint', 201 | }) 202 | .then((res) => console.log(res)) 203 | .catch((err) => console.error(err)); 204 | ``` 205 | 206 | #### filtersプロパティを使用したコンテンツIDの全件取得 207 | 208 | `filters`プロパティを使用することで、条件に一致するコンテンツIDを全件取得できます。 209 | 210 | ```javascript 211 | client 212 | .getAllContentIds({ 213 | endpoint: 'endpoint', 214 | filters: 'category[equals]uN28Folyn', 215 | }) 216 | .then((res) => console.log(res)) 217 | .catch((err) => console.error(err)); 218 | ``` 219 | 220 | #### 下書き中のコンテンツのIDを全件取得 221 | 222 | `draftKey`プロパティを使用することで、下書き中のコンテンツのIDを全件取得できます。 223 | 224 | ```javascript 225 | client 226 | .getAllContentIds({ 227 | endpoint: 'endpoint', 228 | draftKey: 'draftKey', 229 | }) 230 | .then((res) => console.log(res)) 231 | .catch((err) => console.error(err)); 232 | ``` 233 | 234 | #### コンテンツID以外のフィールドの値を全件取得 235 | 236 | `alternateField`プロパティにフィールドIDを指定することで、コンテンツID以外のフィールドの値を全件取得できます。 237 | 238 | ```javascript 239 | client 240 | .getAllContentIds({ 241 | endpoint: 'endpoint', 242 | alternateField: 'url', 243 | }) 244 | .then((res) => console.log(res)) 245 | .catch((err) => console.error(err)); 246 | ``` 247 | 248 | ### コンテンツの全件取得 249 | 250 | `getAllContents`メソッドは、指定されたエンドポイントから、コンテンツを全件取得するために使用します。 251 | 252 | ```javascript 253 | client 254 | .getAllContents({ 255 | endpoint: 'endpoint', 256 | }) 257 | .then((res) => console.log(res)) 258 | .catch((err) => console.error(err)); 259 | ``` 260 | 261 | #### queriesプロパティを使用したコンテンツの全件取得 262 | 263 | `queries`プロパティを使用して、特定の条件に一致するすべてのコンテンツを取得できます。利用可能な各プロパティの詳細については、[microCMSのドキュメント](https://document.microcms.io/content-api/get-list-contents#h929d25d495)を参照してください。 264 | 265 | ```javascript 266 | client 267 | .getAllContents({ 268 | endpoint: 'endpoint', 269 | queries: { filters: 'createdAt[greater_than]2021-01-01T03:00:00.000Z', orders: '-createdAt' }, 270 | }) 271 | .then((res) => console.log(res)) 272 | .catch((err) => console.error(err)); 273 | ``` 274 | 275 | ### コンテンツの登録 276 | 277 | `create`メソッドは指定されたエンドポイントにコンテンツを登録するために使用します。 278 | 279 | ```javascript 280 | client 281 | .create({ 282 | endpoint: 'endpoint', 283 | content: { 284 | title: 'タイトル', 285 | body: '本文', 286 | }, 287 | }) 288 | .then((res) => console.log(res.id)) 289 | .catch((err) => console.error(err)); 290 | ``` 291 | 292 | #### IDを指定してコンテンツを登録 293 | 294 | `contentId`プロパティを使用することで、指定されたIDでコンテンツを登録できます。 295 | 296 | ```javascript 297 | client 298 | .create({ 299 | endpoint: 'endpoint', 300 | contentId: 'contentId', 301 | content: { 302 | title: 'タイトル', 303 | body: '本文', 304 | }, 305 | }) 306 | .then((res) => console.log(res.id)) 307 | .catch((err) => console.error(err)); 308 | ``` 309 | 310 | #### 下書き中のステータスでコンテンツを登録 311 | 312 | `isDraft`プロパティを使用することで、下書き中のステータスでコンテンツを登録できます。 313 | 314 | ```javascript 315 | client 316 | .create({ 317 | endpoint: 'endpoint', 318 | content: { 319 | title: 'タイトル', 320 | body: '本文', 321 | }, 322 | // 有料プランから利用可能 323 | // https://microcms.io/pricing 324 | isDraft: true, 325 | }) 326 | .then((res) => console.log(res.id)) 327 | .catch((err) => console.error(err)); 328 | ``` 329 | 330 | #### 指定されたIDかつ下書き中のステータスでコンテンツを登録 331 | 332 | `contentId`プロパティと`isDraft`プロパティを使用することで、指定されたIDかつ下書き中のステータスでコンテンツを登録できます。 333 | 334 | ```javascript 335 | client 336 | .create({ 337 | endpoint: 'endpoint', 338 | contentId: 'contentId', 339 | content: { 340 | title: 'タイトル', 341 | body: '本文', 342 | }, 343 | // 有料プランから利用可能 344 | // https://microcms.io/pricing 345 | isDraft: true, 346 | }) 347 | .then((res) => console.log(res.id)) 348 | .catch((err) => console.error(err)); 349 | ``` 350 | 351 | ### コンテンツの編集 352 | 353 | `update`メソッドは特定のコンテンツを編集するために使用します。 354 | 355 | ```javascript 356 | client 357 | .update({ 358 | endpoint: 'endpoint', 359 | contentId: 'contentId', 360 | content: { 361 | title: 'タイトル', 362 | }, 363 | }) 364 | .then((res) => console.log(res.id)) 365 | .catch((err) => console.error(err)); 366 | ``` 367 | 368 | #### オブジェクト形式のコンテンツの編集 369 | 370 | APIの型がオブジェクト形式のコンテンツを編集する場合は、`contentId`プロパティを使用せずに、エンドポイントのみを指定します。 371 | 372 | ```javascript 373 | client 374 | .update({ 375 | endpoint: 'endpoint', 376 | content: { 377 | title: 'タイトル', 378 | }, 379 | }) 380 | .then((res) => console.log(res.id)) 381 | .catch((err) => console.error(err)); 382 | ``` 383 | 384 | ### コンテンツの削除 385 | 386 | `delete`メソッドは指定されたエンドポイントから特定のコンテンツを削除するために使用します。 387 | 388 | ```javascript 389 | client 390 | .delete({ 391 | endpoint: 'endpoint', 392 | contentId: 'contentId', 393 | }) 394 | .catch((err) => console.error(err)); 395 | ``` 396 | 397 | ### TypeScript 398 | 399 | `getList`メソッド、`getListDetail`メソッド、`getObject`メソッドはデフォルトのレスポンスの型を定義しています。 400 | 401 | #### getListメソッドのレスポンスの型 402 | 403 | ```typescript 404 | type Content = { 405 | text: string, 406 | }; 407 | /** 408 | * { 409 | * contents: Content[]; // 設定したスキーマの型を格納する配列 410 | * totalCount: number; 411 | * limit: number; 412 | * offset: number; 413 | * } 414 | */ 415 | client.getList({ /* その他のプロパティ */ }) 416 | ``` 417 | 418 | #### getListDetailメソッドのレスポンスの型 419 | 420 | ```typescript 421 | type Content = { 422 | text: string, 423 | }; 424 | /** 425 | * { 426 | * id: string; 427 | * createdAt: string; 428 | * updatedAt: string; 429 | * publishedAt?: string; 430 | * revisedAt?: string; 431 | * text: string; // 設定したスキーマの型 432 | * } 433 | */ 434 | client.getListDetail({ /* その他のプロパティ */ }) 435 | ``` 436 | 437 | #### getObjectメソッドのレスポンスの型 438 | 439 | ```typescript 440 | type Content = { 441 | text: string, 442 | }; 443 | /** 444 | * { 445 | * createdAt: string; 446 | * updatedAt: string; 447 | * publishedAt?: string; 448 | * revisedAt?: string; 449 | * text: string; // 設定したスキーマの型 450 | * } 451 | */ 452 | client.getObject({ /* その他のプロパティ */ }) 453 | ``` 454 | 455 | #### getAllContentIdsメソッドのレスポンスの型 456 | 457 | ```typescript 458 | /** 459 | * string[] 460 | */ 461 | client.getAllContentIds({ /* その他のプロパティ */ }) 462 | ``` 463 | 464 | #### 型安全なコンテンツの登録 465 | 466 | `content`の型は`Content`であるため、型安全なコンテンツの登録が可能です。 467 | 468 | ```typescript 469 | type Content = { 470 | title: string; 471 | body?: string; 472 | }; 473 | 474 | client.create({ 475 | endpoint: 'endpoint', 476 | content: { 477 | title: 'タイトル', 478 | body: '本文', 479 | }, 480 | }); 481 | ``` 482 | 483 | #### 型安全なコンテンツの編集 484 | 485 | `content`は`Partial`型であるため、編集したいプロパティだけを渡せます。 486 | 487 | ```typescript 488 | type Content = { 489 | title: string; 490 | body?: string; 491 | }; 492 | 493 | client.update({ 494 | endpoint: 'endpoint', 495 | content: { 496 | body: '本文', 497 | }, 498 | }); 499 | ``` 500 | 501 | ### CustomRequestInit 502 | 503 | #### Next.js App Router 504 | 505 | Next.jsのApp Routerで利用されるfetchのcacheオプションを指定できます。 506 | 507 | 指定可能なオプションは、Next.jsの公式ドキュメントを参照してください。 508 | 509 | [Functions: fetch \| Next\.js](https://nextjs.org/docs/app/api-reference/functions/fetch) 510 | 511 | ```ts 512 | const response = await client.getList({ 513 | customRequestInit: { 514 | next: { 515 | revalidate: 60, 516 | }, 517 | }, 518 | endpoint: 'endpoint', 519 | }); 520 | ``` 521 | 522 | #### AbortController: abortメソッド 523 | 524 | fetchリクエストを中断できます。 525 | 526 | ```ts 527 | const controller = new AbortController(); 528 | const response = await client.getObject({ 529 | customRequestInit: { 530 | signal: controller.signal, 531 | }, 532 | endpoint: 'config', 533 | }); 534 | 535 | setTimeout(() => { 536 | controller.abort(); 537 | }, 1000); 538 | ``` 539 | 540 | ## マネジメントAPI 541 | 542 | ### インポート 543 | 544 | #### Node.js 545 | 546 | ```javascript 547 | const { createManagementClient } = require('microcms-js-sdk'); // CommonJS 548 | ``` 549 | 550 | または 551 | 552 | ```javascript 553 | import { createManagementClient } from 'microcms-js-sdk'; //ES6 554 | ``` 555 | 556 | #### ブラウザ 557 | 558 | ```html 559 | 562 | ``` 563 | 564 | ### クライアントオブジェクトの作成 565 | 566 | ```javascript 567 | const client = createManagementClient({ 568 | serviceDomain: 'YOUR_DOMAIN', // YOUR_DOMAINはXXXX.microcms.ioのXXXXの部分です。 569 | apiKey: 'YOUR_API_KEY', 570 | }); 571 | ``` 572 | 573 | ### メディアのアップロード 574 | 575 | メディアに画像やファイルをアップロードできます。 576 | 577 | #### Node.js 578 | 579 | ```javascript 580 | // Blob 581 | import { readFileSync } from 'fs'; 582 | 583 | const file = readFileSync('path/to/file'); 584 | client 585 | .uploadMedia({ 586 | data: new Blob([file], { type: 'image/png' }), 587 | name: 'image.png', 588 | }) 589 | .then((res) => console.log(res)) 590 | .catch((err) => console.error(err)); 591 | 592 | // or ReadableStream 593 | import { createReadStream } from 'fs'; 594 | import { Stream } from 'stream'; 595 | 596 | const file = createReadStream('path/to/file'); 597 | client 598 | .uploadMedia({ 599 | data: Stream.Readable.toWeb(file), 600 | name: 'image.png', 601 | type: 'image/png', 602 | }) 603 | .then((res) => console.log(res)) 604 | .catch((err) => console.error(err)); 605 | 606 | // or URL 607 | client 608 | .uploadMedia({ 609 | data: 'https://example.com/image.png', 610 | // name: 'image.png', ← 任意 611 | }) 612 | .then((res) => console.log(res)) 613 | .catch((err) => console.error(err)); 614 | ``` 615 | 616 | #### ブラウザ 617 | 618 | ```javascript 619 | // File 620 | const file = document.querySelector('input[type="file"]').files[0]; 621 | client 622 | .uploadMedia({ 623 | data: file, 624 | }) 625 | .then((res) => console.log(res)) 626 | .catch((err) => console.error(err)); 627 | 628 | // or URL 629 | client 630 | .uploadMedia({ 631 | data: 'https://example.com/image.png', 632 | // name: 'image.png', ← 任意 633 | }) 634 | .then((res) => console.log(res)) 635 | .catch((err) => console.error(err)); 636 | ``` 637 | 638 | ### TypeScript 639 | 640 | #### uploadMediaメソッドのパラメータの型 641 | 642 | ```typescript 643 | type UploadMediaRequest = 644 | | { data: File } 645 | | { data: Blob; name: string } 646 | | { data: ReadableStream; name: string; type: `image/${string}` } 647 | | { 648 | data: URL | string; 649 | name?: string | null | undefined; 650 | customRequestHeaders?: HeadersInit; 651 | }; 652 | function uploadMedia(params: UploadMediaRequest): Promise<{ url: string }>; 653 | ``` 654 | 655 | ## ヒント 656 | 657 | ### 読み取り用と書き込み用で別々のAPIキーを使用する 658 | 659 | ```javascript 660 | const readClient = createClient({ 661 | serviceDomain: 'serviceDomain', 662 | apiKey: 'readApiKey', 663 | }); 664 | const writeClient = createClient({ 665 | serviceDomain: 'serviceDomain', 666 | apiKey: 'writeApiKey', 667 | }); 668 | ``` 669 | 670 | ## ライセンス 671 | 672 | Apache-2.0 673 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | setupFilesAfterEnv: ['./jest.setup.ts'], 6 | collectCoverageFrom: ['src/**/*.ts', '!src/index.ts', '!src/types.ts'], 7 | coverageProvider: 'v8', 8 | }; 9 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { server } from './tests/mocks/server'; 2 | 3 | // Establish API mocking before all tests. 4 | beforeAll(() => server.listen()); 5 | // Reset any request handlers that we may add during the tests, 6 | // so they don't affect other tests. 7 | afterEach(() => server.resetHandlers()); 8 | // Clean up after the tests are finished. 9 | afterAll(() => server.close()); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microcms-js-sdk", 3 | "version": "3.2.0", 4 | "description": "JavaScript SDK Client for microCMS.", 5 | "engines": { 6 | "node": ">=18.0.0" 7 | }, 8 | "main": "./dist/microcms-js-sdk.js", 9 | "module": "./dist/esm/microcms-js-sdk.js", 10 | "unpkg": "./dist/umd/microcms-js-sdk.js", 11 | "types": "./dist/microcms-js-sdk.d.ts", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/microcmsio/microcms-js-sdk.git" 15 | }, 16 | "author": "microCMS", 17 | "license": "Apache-2.0", 18 | "keywords": [ 19 | "JavaScript", 20 | "node", 21 | "SDK", 22 | "microCMS" 23 | ], 24 | "scripts": { 25 | "build": "tsup", 26 | "postbuild": "mkdir -p ./dist/umd/ && cp -pR ./dist/iife/* ./dist/umd", 27 | "lint": "eslint ./src", 28 | "lint:fix": "eslint --fix ./src", 29 | "test": "jest --coverage=false", 30 | "test:coverage": "jest --coverage=true" 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "dependencies": { 36 | "async-retry": "^1.3.3" 37 | }, 38 | "devDependencies": { 39 | "@swc/core": "^1.4.7", 40 | "@types/async-retry": "^1.4.8", 41 | "@types/jest": "^29.5.12", 42 | "@types/node": "^18.19.14", 43 | "@typescript-eslint/eslint-plugin": "^7.2.0", 44 | "@typescript-eslint/parser": "^7.2.0", 45 | "eslint": "^8.57.0", 46 | "eslint-config-prettier": "^9.1.0", 47 | "eslint-plugin-jest": "^27.9.0", 48 | "jest": "^29.7.0", 49 | "msw": "^2.2.3", 50 | "prettier": "^3.2.5", 51 | "ts-jest": "^29.1.2", 52 | "tsup": "^8.0.2", 53 | "typescript": "^5.4.2" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/createClient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * microCMS API SDK 3 | * https://github.com/microcmsio/microcms-js-sdk 4 | */ 5 | import retry from 'async-retry'; 6 | import { generateFetchClient } from './lib/fetch'; 7 | import { 8 | CreateRequest, 9 | DeleteRequest, 10 | GetAllContentIdsRequest, 11 | GetAllContentRequest, 12 | GetListDetailRequest, 13 | GetListRequest, 14 | GetObjectRequest, 15 | GetRequest, 16 | MakeRequest, 17 | MicroCMSClient, 18 | MicroCMSListContent, 19 | MicroCMSListResponse, 20 | MicroCMSObjectContent, 21 | MicroCMSQueries, 22 | UpdateRequest, 23 | WriteApiRequestResult, 24 | } from './types'; 25 | import { 26 | API_VERSION_1, 27 | BASE_DOMAIN, 28 | MAX_RETRY_COUNT, 29 | MIN_TIMEOUT_MS, 30 | } from './utils/constants'; 31 | import { isString } from './utils/isCheckValue'; 32 | import { parseQuery } from './utils/parseQuery'; 33 | 34 | /** 35 | * Initialize SDK Client 36 | */ 37 | export const createClient = ({ 38 | serviceDomain, 39 | apiKey, 40 | retry: retryOption, 41 | }: MicroCMSClient) => { 42 | if (!serviceDomain || !apiKey) { 43 | throw new Error('parameter is required (check serviceDomain and apiKey)'); 44 | } 45 | 46 | if (!isString(serviceDomain) || !isString(apiKey)) { 47 | throw new Error('parameter is not string'); 48 | } 49 | 50 | /** 51 | * Defined microCMS base URL 52 | */ 53 | const baseUrl = `https://${serviceDomain}.${BASE_DOMAIN}/api/${API_VERSION_1}`; 54 | 55 | /** 56 | * Make request 57 | */ 58 | const makeRequest = async ({ 59 | endpoint, 60 | contentId, 61 | queries = {}, 62 | requestInit, 63 | }: MakeRequest) => { 64 | const fetchClient = generateFetchClient(apiKey); 65 | const queryString = parseQuery(queries); 66 | const url = `${baseUrl}/${endpoint}${contentId ? `/${contentId}` : ''}${ 67 | queryString ? `?${queryString}` : '' 68 | }`; 69 | 70 | const getMessageFromResponse = async (response: Response) => { 71 | // Enclose `response.json()` in a try since it may throw an error 72 | // Only return the `message` if there is a `message` 73 | try { 74 | const { message } = await response.json(); 75 | return message ?? null; 76 | } catch (_) { 77 | return null; 78 | } 79 | }; 80 | 81 | return await retry( 82 | async (bail) => { 83 | let response; 84 | try { 85 | response = await fetchClient(url, { 86 | ...requestInit, 87 | method: requestInit?.method ?? 'GET', 88 | }); 89 | 90 | // If a status code in the 400 range other than 429 is returned, do not retry. 91 | if ( 92 | response.status !== 429 && 93 | response.status >= 400 && 94 | response.status < 500 95 | ) { 96 | const message = await getMessageFromResponse(response); 97 | 98 | return bail( 99 | new Error( 100 | `fetch API response status: ${response.status}${ 101 | message ? `\n message is \`${message}\`` : '' 102 | }`, 103 | ), 104 | ); 105 | } 106 | 107 | // If the response fails with any other status code, retry until the set number of attempts is reached. 108 | if (!response.ok) { 109 | const message = await getMessageFromResponse(response); 110 | 111 | return Promise.reject( 112 | new Error( 113 | `fetch API response status: ${response.status}${ 114 | message ? `\n message is \`${message}\`` : '' 115 | }`, 116 | ), 117 | ); 118 | } 119 | 120 | if (requestInit?.method === 'DELETE') return; 121 | 122 | return response.json(); 123 | } catch (error) { 124 | if (error.data) { 125 | throw error.data; 126 | } 127 | 128 | if (error.response?.data) { 129 | throw error.response.data; 130 | } 131 | 132 | return Promise.reject( 133 | new Error(`Network Error.\n Details: ${error.message ?? ''}`), 134 | ); 135 | } 136 | }, 137 | { 138 | retries: retryOption ? MAX_RETRY_COUNT : 0, 139 | onRetry: (err, num) => { 140 | console.log(err); 141 | console.log(`Waiting for retry (${num}/${MAX_RETRY_COUNT})`); 142 | }, 143 | minTimeout: MIN_TIMEOUT_MS, 144 | }, 145 | ); 146 | }; 147 | 148 | /** 149 | * Get list and object API data for microCMS 150 | */ 151 | const get = async ({ 152 | endpoint, 153 | contentId, 154 | queries = {}, 155 | customRequestInit, 156 | }: GetRequest): Promise => { 157 | if (!endpoint) { 158 | return Promise.reject(new Error('endpoint is required')); 159 | } 160 | return await makeRequest({ 161 | endpoint, 162 | contentId, 163 | queries, 164 | requestInit: customRequestInit, 165 | }); 166 | }; 167 | 168 | /** 169 | * Get list API data for microCMS 170 | */ 171 | const getList = async ({ 172 | endpoint, 173 | queries = {}, 174 | customRequestInit, 175 | }: GetListRequest): Promise> => { 176 | if (!endpoint) { 177 | return Promise.reject(new Error('endpoint is required')); 178 | } 179 | return await makeRequest({ 180 | endpoint, 181 | queries, 182 | requestInit: customRequestInit, 183 | }); 184 | }; 185 | 186 | /** 187 | * Get list API detail data for microCMS 188 | */ 189 | const getListDetail = async ({ 190 | endpoint, 191 | contentId, 192 | queries = {}, 193 | customRequestInit, 194 | }: GetListDetailRequest): Promise => { 195 | if (!endpoint) { 196 | return Promise.reject(new Error('endpoint is required')); 197 | } 198 | return await makeRequest({ 199 | endpoint, 200 | contentId, 201 | queries, 202 | requestInit: customRequestInit, 203 | }); 204 | }; 205 | 206 | /** 207 | * Get object API data for microCMS 208 | */ 209 | const getObject = async ({ 210 | endpoint, 211 | queries = {}, 212 | customRequestInit, 213 | }: GetObjectRequest): Promise => { 214 | if (!endpoint) { 215 | return Promise.reject(new Error('endpoint is required')); 216 | } 217 | return await makeRequest({ 218 | endpoint, 219 | queries, 220 | requestInit: customRequestInit, 221 | }); 222 | }; 223 | 224 | const getAllContentIds = async ({ 225 | endpoint, 226 | alternateField, 227 | draftKey, 228 | filters, 229 | orders, 230 | customRequestInit, 231 | }: GetAllContentIdsRequest): Promise => { 232 | const limit = 100; 233 | const defaultQueries: MicroCMSQueries = { 234 | draftKey, 235 | filters, 236 | orders, 237 | limit, 238 | fields: alternateField ?? 'id', 239 | depth: 0, 240 | }; 241 | 242 | const { totalCount } = await makeRequest({ 243 | endpoint, 244 | queries: { ...defaultQueries, limit: 0 }, 245 | requestInit: customRequestInit, 246 | }); 247 | 248 | let contentIds: string[] = []; 249 | let offset = 0; 250 | 251 | const sleep = (ms: number) => 252 | new Promise((resolve) => setTimeout(resolve, ms)); 253 | const isStringArray = (arr: unknown[]): arr is string[] => 254 | arr.every((item) => typeof item === 'string'); 255 | 256 | while (contentIds.length < totalCount) { 257 | const { contents } = (await makeRequest({ 258 | endpoint, 259 | queries: { ...defaultQueries, offset }, 260 | requestInit: customRequestInit, 261 | })) as MicroCMSListResponse>; 262 | 263 | const ids = contents.map((content) => content[alternateField ?? 'id']); 264 | 265 | if (!isStringArray(ids)) { 266 | throw new Error( 267 | 'The value of the field specified by `alternateField` is not a string.', 268 | ); 269 | } 270 | 271 | contentIds = [...contentIds, ...ids]; 272 | 273 | offset += limit; 274 | if (contentIds.length < totalCount) { 275 | await sleep(1000); // sleep for 1 second before the next request 276 | } 277 | } 278 | 279 | return contentIds; 280 | }; 281 | 282 | /** 283 | * Get all content API data for microCMS 284 | */ 285 | const getAllContents = async ({ 286 | endpoint, 287 | queries = {}, 288 | customRequestInit, 289 | }: GetAllContentRequest): Promise<(T & MicroCMSListContent)[]> => { 290 | const limit = 100; 291 | 292 | const { totalCount } = await makeRequest({ 293 | endpoint, 294 | queries: { ...queries, limit: 0 }, 295 | requestInit: customRequestInit, 296 | }); 297 | 298 | let contents: (T & MicroCMSListContent)[] = []; 299 | let offset = 0; 300 | 301 | const sleep = (ms: number) => 302 | new Promise((resolve) => setTimeout(resolve, ms)); 303 | 304 | while (contents.length < totalCount) { 305 | const { contents: _contents } = (await makeRequest({ 306 | endpoint, 307 | queries: { ...queries, limit, offset }, 308 | requestInit: customRequestInit, 309 | })) as MicroCMSListResponse; 310 | 311 | contents = contents.concat(_contents); 312 | 313 | offset += limit; 314 | if (contents.length < totalCount) { 315 | await sleep(1000); // sleep for 1 second before the next request 316 | } 317 | } 318 | 319 | return contents; 320 | }; 321 | 322 | /** 323 | * Create new content in the microCMS list API data 324 | */ 325 | const create = async >({ 326 | endpoint, 327 | contentId, 328 | content, 329 | isDraft = false, 330 | customRequestInit, 331 | }: CreateRequest): Promise => { 332 | if (!endpoint) { 333 | return Promise.reject(new Error('endpoint is required')); 334 | } 335 | 336 | const queries: MakeRequest['queries'] = isDraft ? { status: 'draft' } : {}; 337 | const requestInit: MakeRequest['requestInit'] = { 338 | ...customRequestInit, 339 | method: contentId ? 'PUT' : 'POST', 340 | headers: { 341 | 'Content-Type': 'application/json', 342 | }, 343 | body: JSON.stringify(content), 344 | }; 345 | 346 | return makeRequest({ 347 | endpoint, 348 | contentId, 349 | queries, 350 | requestInit, 351 | }); 352 | }; 353 | 354 | /** 355 | * Update content in their microCMS list and object API data 356 | */ 357 | const update = async >({ 358 | endpoint, 359 | contentId, 360 | content, 361 | customRequestInit, 362 | }: UpdateRequest): Promise => { 363 | if (!endpoint) { 364 | return Promise.reject(new Error('endpoint is required')); 365 | } 366 | 367 | const requestInit: MakeRequest['requestInit'] = { 368 | ...customRequestInit, 369 | method: 'PATCH', 370 | headers: { 371 | 'Content-Type': 'application/json', 372 | }, 373 | body: JSON.stringify(content), 374 | }; 375 | 376 | return makeRequest({ 377 | endpoint, 378 | contentId, 379 | requestInit, 380 | }); 381 | }; 382 | 383 | /** 384 | * Delete content in their microCMS list and object API data 385 | */ 386 | const _delete = async ({ 387 | endpoint, 388 | contentId, 389 | customRequestInit, 390 | }: DeleteRequest): Promise => { 391 | if (!endpoint) { 392 | return Promise.reject(new Error('endpoint is required')); 393 | } 394 | 395 | if (!contentId) { 396 | return Promise.reject(new Error('contentId is required')); 397 | } 398 | 399 | const requestInit: MakeRequest['requestInit'] = { 400 | ...customRequestInit, 401 | method: 'DELETE', 402 | headers: {}, 403 | body: undefined, 404 | }; 405 | 406 | await makeRequest({ endpoint, contentId, requestInit }); 407 | }; 408 | 409 | return { 410 | get, 411 | getList, 412 | getListDetail, 413 | getObject, 414 | getAllContentIds, 415 | getAllContents, 416 | create, 417 | update, 418 | delete: _delete, 419 | }; 420 | }; 421 | -------------------------------------------------------------------------------- /src/createManagementClient.ts: -------------------------------------------------------------------------------- 1 | import { generateFetchClient } from './lib/fetch'; 2 | import { MicroCMSManagementClient, UploadMediaRequest } from './types'; 3 | import { 4 | API_VERSION_1, 5 | API_VERSION_2, 6 | BASE_MANAGEMENT_DOMAIN, 7 | } from './utils/constants'; 8 | import { isString } from './utils/isCheckValue'; 9 | import { parseQuery } from './utils/parseQuery'; 10 | 11 | interface MakeRequest { 12 | path: string; 13 | apiVersion: typeof API_VERSION_1 | typeof API_VERSION_2; 14 | queries?: Record; 15 | requestInit?: RequestInit; 16 | } 17 | 18 | export const createManagementClient = ({ 19 | serviceDomain, 20 | apiKey, 21 | }: MicroCMSManagementClient) => { 22 | if (!serviceDomain || !apiKey) { 23 | throw new Error('parameter is required (check serviceDomain and apiKey)'); 24 | } 25 | 26 | if (!isString(serviceDomain) || !isString(apiKey)) { 27 | throw new Error('parameter is not string'); 28 | } 29 | 30 | /** 31 | * Make request 32 | */ 33 | const makeRequest = async ({ 34 | path, 35 | apiVersion, 36 | queries = {}, 37 | requestInit, 38 | }: MakeRequest) => { 39 | /** 40 | * Defined microCMS base URL 41 | */ 42 | const baseUrl = `https://${serviceDomain}.${BASE_MANAGEMENT_DOMAIN}/api/${apiVersion}`; 43 | 44 | const fetchClient = generateFetchClient(apiKey); 45 | const queryString = parseQuery(queries); 46 | const url = `${baseUrl}/${path}${queryString ? `?${queryString}` : ''}`; 47 | 48 | const getMessageFromResponse = async (response: Response) => { 49 | // Enclose `response.json()` in a try since it may throw an error 50 | // Only return the `message` if there is a `message` 51 | try { 52 | const { message } = await response.json(); 53 | return message ?? null; 54 | } catch (_) { 55 | return null; 56 | } 57 | }; 58 | 59 | let response: Response; 60 | try { 61 | response = await fetchClient(url, { 62 | ...requestInit, 63 | method: requestInit?.method ?? 'GET', 64 | }); 65 | } catch (error) { 66 | if (error.data) { 67 | throw error.data; 68 | } 69 | 70 | if (error.response?.data) { 71 | throw error.response.data; 72 | } 73 | 74 | return Promise.reject( 75 | new Error(`Network Error.\n Details: ${error.message ?? ''}`), 76 | ); 77 | } 78 | 79 | // If the response fails with any other status code, retry until the set number of attempts is reached. 80 | if (!response.ok) { 81 | const message = await getMessageFromResponse(response); 82 | 83 | return Promise.reject( 84 | new Error( 85 | `fetch API response status: ${response.status}${ 86 | message ? `\n message is \`${message}\`` : '' 87 | }`, 88 | ), 89 | ); 90 | } 91 | 92 | return response.json(); 93 | }; 94 | 95 | const uploadMedia = async ({ 96 | data, 97 | name, 98 | type, 99 | customRequestHeaders, 100 | }: UploadMediaRequest): Promise<{ url: string }> => { 101 | const formData = new FormData(); 102 | 103 | if (data instanceof Blob) { 104 | // Node.jsではFile APIはnode:bufferからのみサポートされているため、instance of Fileでは判定せずにnameプロパティが存在するかで判定する 105 | if ((data as File).name) { 106 | formData.set('file', data, (data as File).name); 107 | } else { 108 | if (!name) { 109 | throw new Error('name is required when data is a Blob'); 110 | } 111 | formData.set('file', data, name); 112 | } 113 | } else if (data instanceof ReadableStream) { 114 | if (!name) { 115 | throw new Error('name is required when data is a ReadableStream'); 116 | } 117 | if (!type) { 118 | throw new Error('type is required when data is a ReadableStream'); 119 | } 120 | 121 | const chunks = []; 122 | const reader = data.getReader(); 123 | 124 | let chunk; 125 | while (!(chunk = await reader.read()).done) { 126 | chunks.push(chunk.value); 127 | } 128 | 129 | formData.set('file', new Blob(chunks, { type }), name); 130 | } else if (typeof data === 'string' || data instanceof URL) { 131 | const url = data instanceof URL ? data : new URL(data); 132 | const response = await fetch( 133 | url.toString(), 134 | customRequestHeaders ? { headers: customRequestHeaders } : undefined, 135 | ); 136 | const blob = await response.blob(); 137 | const nameFromURL = new URL(response.url).pathname.split('/').pop(); 138 | formData.set('file', blob, name ?? nameFromURL); 139 | } 140 | 141 | return makeRequest({ 142 | path: 'media', 143 | apiVersion: API_VERSION_1, 144 | requestInit: { method: 'POST', body: formData }, 145 | }); 146 | }; 147 | 148 | return { 149 | uploadMedia, 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createClient } from './createClient'; 2 | export { createManagementClient } from './createManagementClient'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { Fetch } from 'src/types'; 2 | 3 | export const generateFetchClient = (apiKey: string): Fetch => { 4 | return async (req, init) => { 5 | const headers = new Headers(init?.headers); 6 | 7 | if (!headers.has('X-MICROCMS-API-KEY')) { 8 | headers.set('X-MICROCMS-API-KEY', apiKey); 9 | } 10 | 11 | return fetch(req, { ...init, headers }); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Fetch = typeof fetch; 2 | 3 | /** 4 | * microCMS createClient params 5 | */ 6 | export interface MicroCMSClient { 7 | serviceDomain: string; 8 | apiKey: string; 9 | retry?: boolean; 10 | } 11 | 12 | /** 13 | * microCMS createManagementClient params 14 | */ 15 | export interface MicroCMSManagementClient { 16 | serviceDomain: string; 17 | apiKey: string; 18 | } 19 | 20 | type depthNumber = 0 | 1 | 2 | 3; 21 | 22 | /** 23 | * microCMS queries 24 | * https://document.microcms.io/content-api/get-list-contents#h9ce528688c 25 | * https://document.microcms.io/content-api/get-content#h9ce528688c 26 | */ 27 | export interface MicroCMSQueries { 28 | draftKey?: string; 29 | limit?: number; 30 | offset?: number; 31 | orders?: string; 32 | fields?: string | string[]; 33 | q?: string; 34 | depth?: depthNumber; 35 | ids?: string | string[]; 36 | filters?: string; 37 | richEditorFormat?: 'html' | 'object'; 38 | } 39 | 40 | /** 41 | * microCMS contentId 42 | * https://document.microcms.io/manual/content-id-setting 43 | */ 44 | export interface MicroCMSContentId { 45 | id: string; 46 | } 47 | 48 | /** 49 | * microCMS content common date 50 | */ 51 | export interface MicroCMSDate { 52 | createdAt: string; 53 | updatedAt: string; 54 | publishedAt?: string; 55 | revisedAt?: string; 56 | } 57 | 58 | /** 59 | * microCMS image 60 | */ 61 | export interface MicroCMSImage { 62 | url: string; 63 | width?: number; 64 | height?: number; 65 | alt?: string; 66 | } 67 | 68 | /** 69 | * microCMS list api Response 70 | */ 71 | export interface MicroCMSListResponse { 72 | contents: (T & MicroCMSListContent)[]; 73 | totalCount: number; 74 | limit: number; 75 | offset: number; 76 | } 77 | 78 | /** 79 | * microCMS list content common types 80 | */ 81 | export type MicroCMSListContent = MicroCMSContentId & MicroCMSDate; 82 | 83 | /** 84 | * microCMS object content common types 85 | */ 86 | export type MicroCMSObjectContent = MicroCMSDate; 87 | 88 | export interface MakeRequest { 89 | endpoint: string; 90 | contentId?: string; 91 | queries?: MicroCMSQueries & Record; 92 | requestInit?: RequestInit; 93 | } 94 | 95 | export type CustomRequestInit = Omit< 96 | RequestInit, 97 | 'method' | 'headers' | 'body' 98 | >; 99 | 100 | export interface GetRequest { 101 | endpoint: string; 102 | contentId?: string; 103 | queries?: MicroCMSQueries; 104 | customRequestInit?: CustomRequestInit; 105 | } 106 | 107 | export interface GetListDetailRequest { 108 | endpoint: string; 109 | contentId: string; 110 | queries?: MicroCMSQueries; 111 | customRequestInit?: CustomRequestInit; 112 | } 113 | 114 | export interface GetListRequest { 115 | endpoint: string; 116 | queries?: MicroCMSQueries; 117 | customRequestInit?: CustomRequestInit; 118 | } 119 | 120 | export interface GetObjectRequest { 121 | endpoint: string; 122 | queries?: MicroCMSQueries; 123 | customRequestInit?: CustomRequestInit; 124 | } 125 | 126 | export interface GetAllContentIdsRequest { 127 | endpoint: string; 128 | /** 129 | * @type {string} alternateField 130 | * @example 'url' 131 | * If you are using a URL other than the content ID, for example, you can specify that value in the `alternateField` field. 132 | */ 133 | alternateField?: string; 134 | draftKey?: string; 135 | filters?: string; 136 | orders?: string; 137 | customRequestInit?: CustomRequestInit; 138 | } 139 | 140 | export interface GetAllContentRequest { 141 | endpoint: string; 142 | queries?: Omit; 143 | customRequestInit?: CustomRequestInit; 144 | } 145 | 146 | export interface WriteApiRequestResult { 147 | id: string; 148 | } 149 | 150 | export interface CreateRequest { 151 | endpoint: string; 152 | contentId?: string; 153 | content: T; 154 | isDraft?: boolean; 155 | customRequestInit?: CustomRequestInit; 156 | } 157 | 158 | export interface UpdateRequest { 159 | endpoint: string; 160 | contentId?: string; 161 | content: Partial; 162 | customRequestInit?: CustomRequestInit; 163 | } 164 | 165 | export interface DeleteRequest { 166 | endpoint: string; 167 | contentId: string; 168 | customRequestInit?: CustomRequestInit; 169 | } 170 | 171 | export type UploadMediaRequest = 172 | | { 173 | data: File; 174 | name?: undefined; 175 | type?: undefined; 176 | customRequestHeaders?: undefined; 177 | } 178 | | { 179 | data: Blob; 180 | name: string; 181 | type?: undefined; 182 | customRequestHeaders?: undefined; 183 | } 184 | | { 185 | data: ReadableStream; 186 | name: string; 187 | type: `image/${string}`; 188 | customRequestHeaders?: undefined; 189 | } 190 | | { 191 | data: URL | string; 192 | name?: string | null | undefined; 193 | type?: undefined; 194 | customRequestHeaders?: HeadersInit | null | undefined; 195 | }; 196 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const BASE_DOMAIN = 'microcms.io'; 2 | export const BASE_MANAGEMENT_DOMAIN = 'microcms-management.io'; 3 | export const API_VERSION_1 = 'v1'; 4 | export const API_VERSION_2 = 'v2'; 5 | export const MAX_RETRY_COUNT = 2; 6 | export const MIN_TIMEOUT_MS = 5000; 7 | -------------------------------------------------------------------------------- /src/utils/isCheckValue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check object 3 | * 4 | * @param {unknown} value 5 | * @returns {boolean} 6 | */ 7 | export const isObject = (value: unknown): value is Record => { 8 | return value !== null && typeof value === 'object'; 9 | }; 10 | 11 | /** 12 | * Check string 13 | * 14 | * @param {unknown} value 15 | * @returns {boolean} 16 | */ 17 | export const isString = (value: unknown): value is string => { 18 | return typeof value === 'string'; 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/parseQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse query. 3 | * 4 | * @param {object} queries 5 | * @return {string} queryString 6 | */ 7 | import { MicroCMSQueries } from '../types'; 8 | import { isObject } from './isCheckValue'; 9 | 10 | export const parseQuery = (queries: MicroCMSQueries): string => { 11 | if (!isObject(queries)) { 12 | throw new Error('queries is not object'); 13 | } 14 | const queryString = new URLSearchParams( 15 | Object.entries(queries).reduce( 16 | (acc, [key, value]) => { 17 | if (value !== undefined) { 18 | acc[key] = String(value); 19 | } 20 | return acc; 21 | }, 22 | {} as Record, 23 | ), 24 | ).toString(); 25 | 26 | return queryString; 27 | }; 28 | -------------------------------------------------------------------------------- /tests/createClient.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createClient } from '../src/createClient'; 3 | import { testBaseUrl } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | 6 | describe('createClient', () => { 7 | test('Functions is generated to request the API', () => { 8 | const client = createClient({ 9 | serviceDomain: 'serviceDomain', 10 | apiKey: 'apiKey', 11 | retry: false, 12 | }); 13 | 14 | expect(typeof client.get === 'function').toBe(true); 15 | expect(typeof client.getList === 'function').toBe(true); 16 | expect(typeof client.getListDetail === 'function').toBe(true); 17 | expect(typeof client.getObject === 'function').toBe(true); 18 | expect(typeof client.create === 'function').toBe(true); 19 | expect(typeof client.update === 'function').toBe(true); 20 | expect(typeof client.delete === 'function').toBe(true); 21 | }); 22 | 23 | test('Throws an error if `serviceDomain` or `apiKey` is missing', () => { 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-expect-error 26 | expect(() => createClient({ serviceDomain: 'foo' })).toThrow( 27 | new Error('parameter is required (check serviceDomain and apiKey)'), 28 | ); 29 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 30 | // @ts-expect-error 31 | expect(() => createClient({ apiKey: 'foo' })).toThrow( 32 | new Error('parameter is required (check serviceDomain and apiKey)'), 33 | ); 34 | }); 35 | test('Throws an error if `serviceDomain` or `apiKey` is missing', () => { 36 | expect(() => 37 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 38 | // @ts-expect-error 39 | createClient({ serviceDomain: 10, apiKey: 'foo' }), 40 | ).toThrow(new Error('parameter is not string')); 41 | expect(() => 42 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 43 | // @ts-expect-error 44 | createClient({ serviceDomain: 'foo', apiKey: 10 }), 45 | ).toThrow(new Error('parameter is not string')); 46 | }); 47 | 48 | describe('Throws an error when response.ok is false', () => { 49 | test('If there is a message', () => { 50 | server.use( 51 | http.get(`${testBaseUrl}/list-type`, async () => { 52 | return HttpResponse.json( 53 | { message: 'X-MICROCMS-KEY header is invalid.' }, 54 | { status: 401 }, 55 | ); 56 | }), 57 | ); 58 | const client = createClient({ 59 | serviceDomain: 'serviceDomain', 60 | apiKey: 'apiKey', 61 | }); 62 | 63 | expect(client.get({ endpoint: 'list-type' })).rejects.toThrow( 64 | new Error( 65 | 'fetch API response status: 401\n message is `X-MICROCMS-KEY header is invalid.`', 66 | ), 67 | ); 68 | }); 69 | test('If there is no message', () => { 70 | server.use( 71 | http.get(`${testBaseUrl}/list-type`, async () => { 72 | return new HttpResponse(null, { status: 404 }); 73 | }), 74 | ); 75 | const client = createClient({ 76 | serviceDomain: 'serviceDomain', 77 | apiKey: 'apiKey', 78 | }); 79 | 80 | expect(client.get({ endpoint: 'list-type' })).rejects.toThrow( 81 | new Error('fetch API response status: 404'), 82 | ); 83 | }); 84 | }); 85 | 86 | test('Throws an error in the event of a network error.', () => { 87 | server.use( 88 | http.get(`${testBaseUrl}/list-type`, async () => { 89 | return HttpResponse.error(); 90 | }), 91 | ); 92 | const client = createClient({ 93 | serviceDomain: 'serviceDomain', 94 | apiKey: 'apiKey', 95 | }); 96 | 97 | expect(client.get({ endpoint: 'list-type' })).rejects.toThrow( 98 | new Error('Network Error.\n Details: Failed to fetch'), 99 | ); 100 | }); 101 | 102 | describe('Retry option is true', () => { 103 | const retryableClient = createClient({ 104 | serviceDomain: 'serviceDomain', 105 | apiKey: 'apiKey', 106 | retry: true, 107 | }); 108 | 109 | test('Returns an error message if three times failed', async () => { 110 | let apiCallCount = 0; 111 | 112 | server.use( 113 | http.get(`${testBaseUrl}/500`, async () => { 114 | apiCallCount++; 115 | return new HttpResponse(null, { 116 | status: 500, 117 | }); 118 | }), 119 | ); 120 | 121 | await expect(retryableClient.get({ endpoint: '500' })).rejects.toThrow( 122 | new Error('fetch API response status: 500'), 123 | ); 124 | expect(apiCallCount).toBe(3); 125 | }, 30000); 126 | 127 | test('Returns an error message if 4xx error(excluding 429)', async () => { 128 | let apiCallCount = 0; 129 | 130 | server.use( 131 | http.get(`${testBaseUrl}/400`, async () => { 132 | apiCallCount++; 133 | return new HttpResponse(null, { status: 400 }); 134 | }), 135 | ); 136 | 137 | await expect(retryableClient.get({ endpoint: '400' })).rejects.toThrow( 138 | new Error('fetch API response status: 400'), 139 | ); 140 | expect(apiCallCount).toBe(1); 141 | }); 142 | 143 | test('List format contents can be retrieved if failed twice and succeeded once', async () => { 144 | let failedRequestCount = 0; 145 | server.use( 146 | http.get(`${testBaseUrl}/two-times-fail`, () => { 147 | if (failedRequestCount < 2) { 148 | failedRequestCount++; 149 | return new HttpResponse(null, { status: 500 }); 150 | } else { 151 | return HttpResponse.json( 152 | { 153 | contents: [ 154 | { 155 | id: 'foo', 156 | title: 'Hello, microCMS!', 157 | createdAt: '2022-10-28T04:04:29.625Z', 158 | updatedAt: '2022-10-28T04:04:29.625Z', 159 | publishedAt: '2022-10-28T04:04:29.625Z', 160 | revisedAt: '2022-10-28T04:04:29.625Z', 161 | }, 162 | ], 163 | totalCount: 1, 164 | limit: 10, 165 | offset: 0, 166 | }, 167 | { status: 200 }, 168 | ); 169 | } 170 | }), 171 | ); 172 | 173 | const data = await retryableClient.get({ endpoint: 'two-times-fail' }); 174 | expect(data).toEqual({ 175 | contents: [ 176 | { 177 | id: 'foo', 178 | title: 'Hello, microCMS!', 179 | createdAt: '2022-10-28T04:04:29.625Z', 180 | updatedAt: '2022-10-28T04:04:29.625Z', 181 | publishedAt: '2022-10-28T04:04:29.625Z', 182 | revisedAt: '2022-10-28T04:04:29.625Z', 183 | }, 184 | ], 185 | totalCount: 1, 186 | limit: 10, 187 | offset: 0, 188 | }); 189 | }, 30000); 190 | 191 | test('List format contents can be retrieved if succeeded once and failed twice', async () => { 192 | let apiCallCount = 0; 193 | server.use( 194 | http.get(`${testBaseUrl}/only-first-time-success`, () => { 195 | apiCallCount++; 196 | if (apiCallCount === 1) { 197 | return HttpResponse.json( 198 | { 199 | contents: [ 200 | { 201 | id: 'foo', 202 | title: 'Hello, microCMS!', 203 | createdAt: '2022-10-28T04:04:29.625Z', 204 | updatedAt: '2022-10-28T04:04:29.625Z', 205 | publishedAt: '2022-10-28T04:04:29.625Z', 206 | revisedAt: '2022-10-28T04:04:29.625Z', 207 | }, 208 | ], 209 | totalCount: 1, 210 | limit: 10, 211 | offset: 0, 212 | }, 213 | { status: 200 }, 214 | ); 215 | } else { 216 | return new HttpResponse(null, { status: 500 }); 217 | } 218 | }), 219 | ); 220 | 221 | const data = await retryableClient.get({ 222 | endpoint: 'only-first-time-success', 223 | }); 224 | expect(data).toEqual({ 225 | contents: [ 226 | { 227 | id: 'foo', 228 | title: 'Hello, microCMS!', 229 | createdAt: '2022-10-28T04:04:29.625Z', 230 | updatedAt: '2022-10-28T04:04:29.625Z', 231 | publishedAt: '2022-10-28T04:04:29.625Z', 232 | revisedAt: '2022-10-28T04:04:29.625Z', 233 | }, 234 | ], 235 | totalCount: 1, 236 | limit: 10, 237 | offset: 0, 238 | }); 239 | expect(apiCallCount).toBe(1); 240 | }, 30000); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /tests/createManagementClient.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createManagementClient } from '../src/createManagementClient'; 3 | import { testBaseManagementUrlOfVersion1 } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | 6 | // mswの不具合で、FormDataのテストが終わらないため、テストをスキップ 7 | // https://github.com/mswjs/msw/issues/2078 8 | describe.skip('createManagementClient', () => { 9 | test('Functions is generated to request the API', () => { 10 | const client = createManagementClient({ 11 | serviceDomain: 'serviceDomain', 12 | apiKey: 'apiKey', 13 | }); 14 | 15 | expect(typeof client.uploadMedia === 'function').toBe(true); 16 | }); 17 | 18 | test('Throws an error if `serviceDomain` or `apiKey` is missing', () => { 19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 20 | // @ts-expect-error 21 | expect(() => createManagementClient({ serviceDomain: 'foo' })).toThrow( 22 | new Error('parameter is required (check serviceDomain and apiKey)'), 23 | ); 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-expect-error 26 | expect(() => createManagementClient({ apiKey: 'foo' })).toThrow( 27 | new Error('parameter is required (check serviceDomain and apiKey)'), 28 | ); 29 | }); 30 | test('Throws an error if `serviceDomain` or `apiKey` is missing', () => { 31 | expect(() => 32 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 33 | // @ts-expect-error 34 | createManagementClient({ serviceDomain: 10, apiKey: 'foo' }), 35 | ).toThrow(new Error('parameter is not string')); 36 | expect(() => 37 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 38 | // @ts-expect-error 39 | createManagementClient({ serviceDomain: 'foo', apiKey: 10 }), 40 | ).toThrow(new Error('parameter is not string')); 41 | }); 42 | 43 | describe('Throws an error when response.ok is false', () => { 44 | test('If there is a message', () => { 45 | server.use( 46 | http.post(`${testBaseManagementUrlOfVersion1}/media`, async () => { 47 | return HttpResponse.json( 48 | { message: 'X-MICROCMS-KEY header is invalid.' }, 49 | { status: 401 }, 50 | ); 51 | }), 52 | ); 53 | const client = createManagementClient({ 54 | serviceDomain: 'serviceDomain', 55 | apiKey: 'apiKey', 56 | }); 57 | 58 | expect( 59 | client.uploadMedia({ 60 | data: new Blob([], { type: 'image/png' }), 61 | name: 'image.png', 62 | }), 63 | ).rejects.toThrow( 64 | new Error( 65 | 'fetch API response status: 401\n message is `X-MICROCMS-KEY header is invalid.`', 66 | ), 67 | ); 68 | }); 69 | test('If there is no message', () => { 70 | server.use( 71 | http.post(`${testBaseManagementUrlOfVersion1}/media`, async () => { 72 | return new HttpResponse(null, { status: 500 }); 73 | }), 74 | ); 75 | const client = createManagementClient({ 76 | serviceDomain: 'serviceDomain', 77 | apiKey: 'apiKey', 78 | }); 79 | 80 | expect( 81 | client.uploadMedia({ 82 | data: new Blob([], { type: 'image/png' }), 83 | name: 'image.png', 84 | }), 85 | ).rejects.toThrow(new Error('fetch API response status: 500')); 86 | }); 87 | }); 88 | 89 | test('Throws an error in the event of a network error.', () => { 90 | server.use( 91 | http.post(`${testBaseManagementUrlOfVersion1}/media`, async () => { 92 | return HttpResponse.error(); 93 | }), 94 | ); 95 | const client = createManagementClient({ 96 | serviceDomain: 'serviceDomain', 97 | apiKey: 'apiKey', 98 | }); 99 | 100 | expect( 101 | client.uploadMedia({ 102 | data: new Blob([], { type: 'image/png' }), 103 | name: 'image.png', 104 | }), 105 | ).rejects.toThrow(new Error('Network Error.\n Details: Failed to fetch')); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/get.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createClient } from '../src/createClient'; 3 | import { testBaseUrl } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | 6 | const client = createClient({ 7 | serviceDomain: 'serviceDomain', 8 | apiKey: 'apiKey', 9 | }); 10 | 11 | describe('get', () => { 12 | test('List format contents can be retrieved', async () => { 13 | const data = await client.get({ endpoint: 'list-type' }); 14 | expect(data).toEqual({ 15 | contents: [ 16 | { 17 | id: 'foo', 18 | title: 'Hello, microCMS!', 19 | createdAt: '2022-10-28T04:04:29.625Z', 20 | updatedAt: '2022-10-28T04:04:29.625Z', 21 | publishedAt: '2022-10-28T04:04:29.625Z', 22 | revisedAt: '2022-10-28T04:04:29.625Z', 23 | }, 24 | ], 25 | totalCount: 1, 26 | limit: 10, 27 | offset: 0, 28 | }); 29 | }); 30 | test('The contents of the details of the list format can be retrieved', async () => { 31 | const data = await client.get({ endpoint: 'list-type', contentId: 'foo' }); 32 | expect(data).toEqual({ 33 | id: 'foo', 34 | title: 'Hello, microCMS!', 35 | createdAt: '2022-10-28T04:04:29.625Z', 36 | updatedAt: '2022-10-28T04:04:29.625Z', 37 | publishedAt: '2022-10-28T04:04:29.625Z', 38 | revisedAt: '2022-10-28T04:04:29.625Z', 39 | }); 40 | }); 41 | test('Object type contents can be retrieved.', async () => { 42 | const data = await client.get({ endpoint: 'object-type' }); 43 | expect(data).toEqual({ 44 | id: 'foo', 45 | title: 'Hello, microCMS!', 46 | createdAt: '2022-10-28T04:04:29.625Z', 47 | updatedAt: '2022-10-28T04:04:29.625Z', 48 | publishedAt: '2022-10-28T04:04:29.625Z', 49 | revisedAt: '2022-10-28T04:04:29.625Z', 50 | }); 51 | }); 52 | 53 | test('Returns an error message if `endpoint` is not specified', () => { 54 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 55 | // @ts-expect-error 56 | expect(client.get({})).rejects.toThrow(new Error('endpoint is required')); 57 | }); 58 | test('Return error message in case of server error', () => { 59 | // Create temporary server error 60 | server.use( 61 | http.get(`${testBaseUrl}/list-type`, () => { 62 | return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }); 63 | }, { once: true }) 64 | ); 65 | 66 | expect(client.get({ endpoint: 'list-type' })).rejects.toThrow( 67 | new Error( 68 | 'fetch API response status: 500\n message is `Internal Server Error`' 69 | ) 70 | ); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/getAllContentIds.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createClient } from '../src/createClient'; 3 | import { testBaseUrl } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | 6 | const client = createClient({ 7 | serviceDomain: 'serviceDomain', 8 | apiKey: 'apiKey', 9 | }); 10 | 11 | describe('getAllContentIds', () => { 12 | afterEach(() => { 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | test('should fetch all content ids', async () => { 17 | server.use( 18 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 19 | return HttpResponse.json({ 20 | totalCount: 100, 21 | }, { status: 200 }); 22 | }, { once: true }), 23 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 24 | return HttpResponse.json({ 25 | contents: Array(100) 26 | .fill(null) 27 | .map((_, index) => ({ id: `id${index}` })), 28 | }, { status: 200 }); 29 | }, { once: true }), 30 | ); 31 | 32 | const result = await client.getAllContentIds({ 33 | endpoint: 'getAllContentIds-list-type', 34 | }); 35 | 36 | expect(result).toHaveLength(100); 37 | expect(result).toContain('id0'); 38 | expect(result).toContain('id99'); 39 | }); 40 | 41 | test('should handle pagination and fetch more than limit', async () => { 42 | server.use( 43 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 44 | return HttpResponse.json({ 45 | totalCount: 250, 46 | }, { status: 200 }); 47 | }, { once: true }), 48 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 49 | return HttpResponse.json({ 50 | contents: Array(100) 51 | .fill(null) 52 | .map((_, index) => ({ id: `id${index}` })), 53 | }, { status: 200 }); 54 | }, { once: true }), 55 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 56 | return HttpResponse.json({ 57 | contents: Array(100) 58 | .fill(null) 59 | .map((_, index) => ({ id: `id${index + 100}` })), 60 | }, { status: 200 }); 61 | }, { once: true }), 62 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 63 | return HttpResponse.json({ 64 | contents: Array(50) 65 | .fill(null) 66 | .map((_, index) => ({ id: `id${index + 200}` })), 67 | }, { status: 200 }); 68 | }, { once: true }), 69 | ); 70 | 71 | const result = await client.getAllContentIds({ 72 | endpoint: 'getAllContentIds-list-type', 73 | }); 74 | 75 | expect(result).toHaveLength(250); 76 | expect(result).toContain('id0'); 77 | expect(result).toContain('id249'); 78 | }); 79 | 80 | test('should fetch all content ids with alternateField field', async () => { 81 | server.use( 82 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 83 | return HttpResponse.json({ 84 | totalCount: 100, 85 | }, { status: 200 }); 86 | }, { once: true }), 87 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 88 | return HttpResponse.json({ 89 | contents: Array(100) 90 | .fill(null) 91 | .map((_, index) => ({ url: `id${index}` })), 92 | }, { status: 200 }); 93 | }, { once: true }), 94 | ); 95 | 96 | const result = await client.getAllContentIds({ 97 | endpoint: 'getAllContentIds-list-type', 98 | alternateField: 'url', 99 | }); 100 | 101 | expect(result).toHaveLength(100); 102 | expect(result).toContain('id0'); 103 | expect(result).toContain('id99'); 104 | }); 105 | 106 | test('should throw error when alternateField field is not string', async () => { 107 | server.use( 108 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 109 | return HttpResponse.json({ 110 | totalCount: 100, 111 | }, { status: 200 }); 112 | }, { once: true }), 113 | http.get(`${testBaseUrl}/getAllContentIds-list-type`, () => { 114 | return HttpResponse.json({ 115 | contents: Array(100) 116 | .fill(null) 117 | .map(() => ({ image: { url: 'url', width: 100, height: 100 } })), 118 | }, { status: 200 }); 119 | }, { once: true }), 120 | ); 121 | 122 | await expect( 123 | client.getAllContentIds({ 124 | endpoint: 'getAllContentIds-list-type', 125 | alternateField: 'image', 126 | }), 127 | ).rejects.toThrowError( 128 | 'The value of the field specified by `alternateField` is not a string.', 129 | ); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/getAllContents.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createClient } from '../src/createClient'; 3 | import { testBaseUrl } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | 6 | const client = createClient({ 7 | serviceDomain: 'serviceDomain', 8 | apiKey: 'apiKey', 9 | }); 10 | 11 | describe('getAllContents', () => { 12 | afterEach(() => { 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | test('should fetch all contents', async () => { 17 | server.use( 18 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 19 | return HttpResponse.json({ 20 | totalCount: 100, 21 | }, { status: 200 }); 22 | }, { once: true }), 23 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 24 | return HttpResponse.json({ 25 | contents: Array(100) 26 | .fill(null) 27 | .map((_, index) => ({ id: `id${index}` })), 28 | }, { status: 200 }); 29 | }, { once: true }), 30 | ); 31 | 32 | const result = await client.getAllContents({ 33 | endpoint: 'getAllContents-list-type', 34 | }); 35 | 36 | expect(result).toHaveLength(100); 37 | expect(result).toContainEqual({ id: 'id0' }); 38 | expect(result).toContainEqual({ id: 'id99' }); 39 | }); 40 | 41 | test('should handle pagination and fetch more than limit', async () => { 42 | server.use( 43 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 44 | return HttpResponse.json({ 45 | totalCount: 250, 46 | }, { status: 200 }); 47 | }, { once: true }), 48 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 49 | return HttpResponse.json({ 50 | contents: Array(100) 51 | .fill(null) 52 | .map((_, index) => ({ id: `id${index}` })), 53 | }, { status: 200 }); 54 | }, { once: true }), 55 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 56 | return HttpResponse.json({ 57 | contents: Array(100) 58 | .fill(null) 59 | .map((_, index) => ({ id: `id${index + 100}` })), 60 | }, { status: 200 }); 61 | }, { once: true }), 62 | http.get(`${testBaseUrl}/getAllContents-list-type`, () => { 63 | return HttpResponse.json({ 64 | contents: Array(50) 65 | .fill(null) 66 | .map((_, index) => ({ id: `id${index + 200}` })), 67 | }, { status: 200 }); 68 | }, { once: true }), 69 | ); 70 | 71 | const result = await client.getAllContents({ 72 | endpoint: 'getAllContents-list-type', 73 | }); 74 | 75 | expect(result).toHaveLength(250); 76 | expect(result).toContainEqual({ id: 'id0' }); 77 | expect(result).toContainEqual({ id: 'id249' }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /tests/lib/fetch.test.ts: -------------------------------------------------------------------------------- 1 | import { generateFetchClient } from '../../src/lib/fetch'; 2 | 3 | const fetchMock = jest.fn(() => 4 | Promise.resolve({ 5 | ok: true, 6 | json: () => Promise.resolve(), 7 | }), 8 | ); 9 | 10 | describe('generateFetchClient', () => { 11 | beforeEach(() => { 12 | fetchMock.mockClear(); 13 | global.fetch = fetchMock as any; 14 | }); 15 | 16 | test('should correctly set the X-MICROCMS-API-KEY header if not already present', async () => { 17 | const apiKey = 'testApiKey'; 18 | const testUrl = 'http://test.com'; 19 | const fetchClient = generateFetchClient(apiKey); 20 | 21 | await fetchClient(testUrl, {}); 22 | 23 | expect(fetch).toHaveBeenCalledWith( 24 | testUrl, 25 | expect.objectContaining({ headers: expect.anything() }), 26 | ); 27 | const calledWithHeaders = new Headers( 28 | (fetch as jest.Mock).mock.calls[0][1].headers, 29 | ); 30 | expect(calledWithHeaders.get('X-MICROCMS-API-KEY')).toBe(apiKey); 31 | }); 32 | 33 | test('should not overwrite the X-MICROCMS-API-KEY header if already present', async () => { 34 | const apiKey = 'testApiKey'; 35 | const existingApiKey = 'existingApiKey'; 36 | const testUrl = 'http://test.com'; 37 | const fetchClient = generateFetchClient(apiKey); 38 | const headers = new Headers({ 'X-MICROCMS-API-KEY': existingApiKey }); 39 | 40 | await fetchClient(testUrl, { headers }); 41 | 42 | expect(fetch).toHaveBeenCalledWith( 43 | testUrl, 44 | expect.objectContaining({ headers: expect.anything() }), 45 | ); 46 | const calledWithHeaders = new Headers( 47 | (fetch as jest.Mock).mock.calls[0][1].headers, 48 | ); 49 | expect(calledWithHeaders.get('X-MICROCMS-API-KEY')).toBe(existingApiKey); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { DefaultBodyType, http, HttpResponse, StrictRequest } from 'msw'; 2 | 3 | import { 4 | API_VERSION_1, 5 | API_VERSION_2, 6 | BASE_DOMAIN, 7 | BASE_MANAGEMENT_DOMAIN, 8 | } from '../../src/utils/constants'; 9 | 10 | const baseUrl = `https://serviceDomain.${BASE_DOMAIN}/api/${API_VERSION_1}`; 11 | const baseManagementUrlOfVersion1 = `https://serviceDomain.${BASE_MANAGEMENT_DOMAIN}/api/${API_VERSION_1}`; 12 | const baseManagementUrlOfVersion2 = `https://serviceDomain.${BASE_MANAGEMENT_DOMAIN}/api/${API_VERSION_2}`; 13 | 14 | const hasValidApiKey = (req: StrictRequest) => { 15 | return req.headers.get('X-MICROCMS-API-KEY') === 'apiKey'; 16 | }; 17 | 18 | export const handlers = [ 19 | http.get('http://example.com', ({ request }) => { 20 | if (!hasValidApiKey(request)) 21 | return new HttpResponse(null, { status: 401 }); 22 | return new HttpResponse(null, { status: 200 }); 23 | }), 24 | http.get(`${baseUrl}/list-type`, ({ request }) => { 25 | if (!hasValidApiKey(request)) 26 | return new HttpResponse(null, { status: 401 }); 27 | 28 | return HttpResponse.json({ 29 | contents: [ 30 | { 31 | id: 'foo', 32 | title: 'Hello, microCMS!', 33 | createdAt: '2022-10-28T04:04:29.625Z', 34 | updatedAt: '2022-10-28T04:04:29.625Z', 35 | publishedAt: '2022-10-28T04:04:29.625Z', 36 | revisedAt: '2022-10-28T04:04:29.625Z', 37 | }, 38 | ], 39 | totalCount: 1, 40 | limit: 10, 41 | offset: 0, 42 | }); 43 | }), 44 | http.post(`${baseUrl}/list-type`, ({ request }) => { 45 | if (!hasValidApiKey(request)) 46 | return new HttpResponse(null, { status: 401 }); 47 | return HttpResponse.json({ id: 'foo' }); 48 | }), 49 | http.put(`${baseUrl}/list-type`, ({ request }) => { 50 | if (!hasValidApiKey(request)) 51 | return new HttpResponse(null, { status: 401 }); 52 | return HttpResponse.json( 53 | { massage: 'contentId is necessary.' }, 54 | { status: 400 }, 55 | ); 56 | }), 57 | http.patch(`${baseUrl}/list-type`, ({ request }) => { 58 | if (!hasValidApiKey(request)) 59 | return new HttpResponse(null, { status: 401 }); 60 | return HttpResponse.json( 61 | { massage: 'Content is not exists.' }, 62 | { status: 400 }, 63 | ); 64 | }), 65 | http.delete(`${baseUrl}/list-type`, ({ request }) => { 66 | if (!hasValidApiKey(request)) 67 | return new HttpResponse(null, { status: 401 }); 68 | return HttpResponse.json( 69 | { massage: 'Content is not exists.' }, 70 | { status: 400 }, 71 | ); 72 | }), 73 | 74 | http.get(`${baseUrl}/list-type/foo`, ({ request }) => { 75 | if (!hasValidApiKey(request)) 76 | return new HttpResponse(null, { status: 401 }); 77 | return HttpResponse.json({ 78 | id: 'foo', 79 | title: 'Hello, microCMS!', 80 | createdAt: '2022-10-28T04:04:29.625Z', 81 | updatedAt: '2022-10-28T04:04:29.625Z', 82 | publishedAt: '2022-10-28T04:04:29.625Z', 83 | revisedAt: '2022-10-28T04:04:29.625Z', 84 | }); 85 | }), 86 | http.post(`${baseUrl}/list-type/foo`, ({ request }) => { 87 | if (!hasValidApiKey(request)) 88 | return new HttpResponse(null, { status: 401 }); 89 | return HttpResponse.json({}, { status: 404 }); 90 | }), 91 | http.put(`${baseUrl}/list-type/foo`, ({ request }) => { 92 | if (!hasValidApiKey(request)) 93 | return new HttpResponse(null, { status: 401 }); 94 | return HttpResponse.json({ id: 'foo' }, { status: 201 }); 95 | }), 96 | http.patch(`${baseUrl}/list-type/foo`, ({ request }) => { 97 | if (!hasValidApiKey(request)) 98 | return new HttpResponse(null, { status: 401 }); 99 | return HttpResponse.json({ id: 'foo' }, { status: 200 }); 100 | }), 101 | http.delete(`${baseUrl}/list-type/foo`, ({ request }) => { 102 | if (!hasValidApiKey(request)) 103 | return new HttpResponse(null, { status: 401 }); 104 | return HttpResponse.json({}, { status: 202 }); 105 | }), 106 | 107 | http.get(`${baseUrl}/object-type`, ({ request }) => { 108 | if (!hasValidApiKey(request)) 109 | return new HttpResponse(null, { status: 401 }); 110 | return HttpResponse.json({ 111 | id: 'foo', 112 | title: 'Hello, microCMS!', 113 | createdAt: '2022-10-28T04:04:29.625Z', 114 | updatedAt: '2022-10-28T04:04:29.625Z', 115 | publishedAt: '2022-10-28T04:04:29.625Z', 116 | revisedAt: '2022-10-28T04:04:29.625Z', 117 | }); 118 | }), 119 | http.post(`${baseUrl}/object-type`, ({ request }) => { 120 | if (!hasValidApiKey(request)) 121 | return new HttpResponse(null, { status: 401 }); 122 | return HttpResponse.json( 123 | { 124 | message: 'POST is forbidden.', 125 | }, 126 | { status: 400 }, 127 | ); 128 | }), 129 | http.put(`${baseUrl}/object-type`, ({ request }) => { 130 | if (!hasValidApiKey(request)) 131 | return new HttpResponse(null, { status: 401 }); 132 | return HttpResponse.json( 133 | { 134 | message: 'PUT is forbidden.', 135 | }, 136 | { status: 400 }, 137 | ); 138 | }), 139 | http.patch(`${baseUrl}/object-type`, ({ request }) => { 140 | if (!hasValidApiKey(request)) 141 | return new HttpResponse(null, { status: 401 }); 142 | return HttpResponse.json({ id: 'foo' }, { status: 200 }); 143 | }), 144 | http.delete(`${baseUrl}/object-type`, ({ request }) => { 145 | if (!hasValidApiKey(request)) 146 | return new HttpResponse(null, { status: 401 }); 147 | return HttpResponse.json( 148 | { 149 | message: 'DELETE is forbidden.', 150 | }, 151 | { status: 400 }, 152 | ); 153 | }), 154 | ]; 155 | 156 | export { 157 | baseUrl as testBaseUrl, 158 | baseManagementUrlOfVersion1 as testBaseManagementUrlOfVersion1, 159 | baseManagementUrlOfVersion2 as testBaseManagementUrlOfVersion2, 160 | }; 161 | -------------------------------------------------------------------------------- /tests/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | // Setup requests interception using the given handlers. 5 | export const server = setupServer(...handlers); 6 | -------------------------------------------------------------------------------- /tests/requestInit.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '../src/createClient'; 2 | 3 | const fetchMock = jest.fn(() => 4 | Promise.resolve({ 5 | ok: true, 6 | json: () => Promise.resolve(), 7 | }), 8 | ); 9 | 10 | const client = createClient({ 11 | serviceDomain: 'serviceDomain', 12 | apiKey: 'apiKey', 13 | }); 14 | 15 | beforeEach(() => { 16 | fetchMock.mockClear(); 17 | global.fetch = fetchMock as any; 18 | }); 19 | 20 | describe('requestInit', () => { 21 | test('Default request init is passed for fetch in get request.', async () => { 22 | await client.get({ endpoint: 'object-type' }); 23 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 24 | // @ts-expect-error 25 | expect(fetchMock.mock.calls[0][1]).toEqual({ 26 | method: 'GET', 27 | headers: new Headers({ 28 | 'X-MICROCMS-API-KEY': 'apiKey', 29 | }), 30 | }); 31 | }); 32 | 33 | test('Custom request init added cache parameter is passed for fetch in get request.', async () => { 34 | await client.get({ 35 | endpoint: 'object-type', 36 | customRequestInit: { 37 | cache: 'no-store', 38 | }, 39 | }); 40 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 41 | // @ts-expect-error 42 | expect(fetchMock.mock.calls[0][1]).toEqual({ 43 | method: 'GET', 44 | headers: new Headers({ 45 | 'X-MICROCMS-API-KEY': 'apiKey', 46 | }), 47 | cache: 'no-store', 48 | }); 49 | }); 50 | 51 | test('Custom request init added for Next.js parameter is passed for fetch in get request.', async () => { 52 | await client.get({ 53 | endpoint: 'object-type', 54 | customRequestInit: { 55 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 56 | // @ts-expect-error 57 | next: { 58 | revalidate: 10, 59 | }, 60 | }, 61 | }); 62 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 63 | // @ts-expect-error 64 | expect(fetchMock.mock.calls[0][1]).toEqual({ 65 | method: 'GET', 66 | headers: new Headers({ 67 | 'X-MICROCMS-API-KEY': 'apiKey', 68 | }), 69 | next: { 70 | revalidate: 10, 71 | }, 72 | }); 73 | }); 74 | 75 | test('Custom request init added method, headers and body parameters is not overwrited for fetch in create request.', async () => { 76 | await client.create({ 77 | endpoint: 'list-type', 78 | content: { title: 'title' }, 79 | customRequestInit: { 80 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 81 | // @ts-expect-error 82 | method: 'GET', 83 | headers: { 84 | 'X-MICROCMS-API-KEY': 'OverwrittenApiKey', 85 | }, 86 | body: { title: 'Overwritten Title' }, 87 | }, 88 | }); 89 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 90 | // @ts-expect-error 91 | expect(fetchMock.mock.calls[0][1]).toEqual({ 92 | method: 'POST', 93 | headers: new Headers({ 94 | 'Content-Type': 'application/json', 95 | 'X-MICROCMS-API-KEY': 'apiKey', 96 | }), 97 | body: JSON.stringify({ title: 'title' }), 98 | }); 99 | }); 100 | 101 | test('Custom request init added method, headers and body parameters is not overwrited for fetch in update request.', async () => { 102 | await client.update({ 103 | endpoint: 'list-type', 104 | content: { title: 'title' }, 105 | customRequestInit: { 106 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 107 | // @ts-expect-error 108 | method: 'GET', 109 | headers: { 110 | 'X-MICROCMS-API-KEY': 'OverwrittenApiKey', 111 | }, 112 | body: { title: 'Overwritten Title' }, 113 | }, 114 | }); 115 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 116 | // @ts-expect-error 117 | expect(fetchMock.mock.calls[0][1]).toEqual({ 118 | method: 'PATCH', 119 | headers: new Headers({ 120 | 'Content-Type': 'application/json', 121 | 'X-MICROCMS-API-KEY': 'apiKey', 122 | }), 123 | body: JSON.stringify({ title: 'title' }), 124 | }); 125 | }); 126 | 127 | test('Custom request init added method, headers and body parameters is not overwrited for fetch in delete request.', async () => { 128 | await client.delete({ 129 | endpoint: 'list-type', 130 | contentId: 'contentId', 131 | customRequestInit: { 132 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 133 | // @ts-expect-error 134 | method: 'GET', 135 | headers: { 136 | 'X-MICROCMS-API-KEY': 'OverwrittenApiKey', 137 | }, 138 | }, 139 | }); 140 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 141 | // @ts-expect-error 142 | expect(fetchMock.mock.calls[0][1]).toEqual({ 143 | method: 'DELETE', 144 | headers: new Headers({ 145 | 'X-MICROCMS-API-KEY': 'apiKey', 146 | }), 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/uploadMedia.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import { createManagementClient } from '../src/createManagementClient'; 3 | import { testBaseManagementUrlOfVersion1 } from './mocks/handlers'; 4 | import { server } from './mocks/server'; 5 | import { File } from 'buffer'; 6 | 7 | const client = createManagementClient({ 8 | serviceDomain: 'serviceDomain', 9 | apiKey: 'apiKey', 10 | }); 11 | 12 | // mswの不具合で、FormDataのテストが終わらないため、テストをスキップ 13 | // https://github.com/mswjs/msw/issues/2078 14 | describe.skip('uploadMedia', () => { 15 | const uploadMediaApiMockFn = jest.fn(); 16 | 17 | beforeEach(() => { 18 | server.use( 19 | http.post( 20 | `${testBaseManagementUrlOfVersion1}/media`, 21 | async ({ request }) => { 22 | const data = await request.formData(); 23 | const file = data.get('file'); 24 | 25 | uploadMediaApiMockFn(file); 26 | 27 | return HttpResponse.json( 28 | { url: 'https://images.microcms-assets.io/image.png' }, 29 | { status: 201 }, 30 | ); 31 | }, 32 | ), 33 | ); 34 | }); 35 | 36 | afterEach(() => { 37 | uploadMediaApiMockFn.mockClear(); 38 | }); 39 | 40 | test('If the data received is a Blob', async () => { 41 | await client.uploadMedia({ 42 | data: new Blob([], { type: 'image/png' }), 43 | name: 'image.png', 44 | }); 45 | 46 | expect(uploadMediaApiMockFn).toHaveBeenCalledTimes(1); 47 | expect(uploadMediaApiMockFn.mock.calls[0][0].name).toBe('image.png'); 48 | expect(uploadMediaApiMockFn.mock.calls[0][0].type).toBe('image/png'); 49 | }); 50 | 51 | test('If the data received is a File', async () => { 52 | await client.uploadMedia({ 53 | // Node.jsのFileにはwebkitRelativePathプロパティが存在しないためanyで回避 54 | data: new File([], 'image.png', { type: 'image/png' }) as any, 55 | }); 56 | 57 | expect(uploadMediaApiMockFn).toHaveBeenCalledTimes(1); 58 | expect(uploadMediaApiMockFn.mock.calls[0][0].name).toBe('image.png'); 59 | expect(uploadMediaApiMockFn.mock.calls[0][0].type).toBe('image/png'); 60 | }); 61 | 62 | test('If the data received is a ReadableStream', async () => { 63 | await client.uploadMedia({ 64 | data: new ReadableStream({ 65 | start(controller) { 66 | controller.enqueue(new Uint8Array([])); 67 | controller.close(); 68 | }, 69 | }), 70 | name: 'image.png', 71 | type: 'image/png', 72 | }); 73 | 74 | expect(uploadMediaApiMockFn).toHaveBeenCalledTimes(1); 75 | expect(uploadMediaApiMockFn.mock.calls[0][0].name).toBe('image.png'); 76 | expect(uploadMediaApiMockFn.mock.calls[0][0].type).toBe('image/png'); 77 | }); 78 | 79 | test('If the data received is a URL or string', async () => { 80 | server.use( 81 | http.get('https://example.com/image.png', async () => { 82 | return HttpResponse.arrayBuffer( 83 | await new Blob([], { type: 'image/png' }).arrayBuffer(), 84 | { headers: { 'Content-Type': 'image/png' } }, 85 | ); 86 | }), 87 | ); 88 | 89 | await client.uploadMedia({ 90 | data: 'https://example.com/image.png', 91 | }); 92 | 93 | expect(uploadMediaApiMockFn).toHaveBeenCalledTimes(1); 94 | expect(uploadMediaApiMockFn.mock.calls[0][0].name).toBe('image.png'); 95 | expect(uploadMediaApiMockFn.mock.calls[0][0].type).toBe('image/png'); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/utils/isCheckValue.test.ts: -------------------------------------------------------------------------------- 1 | import { isObject, isString } from '../../src/utils/isCheckValue'; 2 | 3 | describe('isObject', () => { 4 | test('Returns true for objects', () => { 5 | expect(isObject({})).toBe(true); 6 | expect(isObject([])).toBe(true); 7 | }); 8 | test('Returns false for non-objects', () => { 9 | expect(isObject('')).toBe(false); 10 | expect(isObject(0)).toBe(false); 11 | expect(isObject(null)).toBe(false); 12 | expect(isObject(undefined)).toBe(false); 13 | expect(isObject(() => undefined)).toBe(false); 14 | }); 15 | }); 16 | 17 | describe('isString', () => { 18 | test('Returns true for strings', () => { 19 | expect(isString('')).toBe(true); 20 | }); 21 | test('Returns false for not-strings', () => { 22 | expect(isString(0)).toBe(false); 23 | expect(isString({})).toBe(false); 24 | expect(isString([])).toBe(false); 25 | expect(isString(null)).toBe(false); 26 | expect(isString(undefined)).toBe(false); 27 | expect(isString(() => undefined)).toBe(false); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/utils/parseQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { parseQuery } from '../../src/utils/parseQuery'; 2 | 3 | describe('parseQuery', () => { 4 | test('Object type query string returned as string', () => { 5 | expect( 6 | parseQuery({ 7 | limit: 100, 8 | fields: ['id', 'title'], 9 | orders: 'publishedAt', 10 | }), 11 | ).toBe('limit=100&fields=id%2Ctitle&orders=publishedAt'); 12 | }); 13 | 14 | test('Throws an error if a non-object is specified', () => { 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-expect-error 17 | expect(() => parseQuery('')).toThrowError( 18 | new Error('queries is not object'), 19 | ); 20 | }); 21 | 22 | test('Undefined values are removed from the query string', () => { 23 | expect( 24 | parseQuery({ 25 | limit: 100, 26 | fields: undefined, 27 | orders: 'publishedAt', 28 | }), 29 | ).toBe('limit=100&orders=publishedAt'); 30 | }); 31 | 32 | test('Multiple undefined values are removed from the query string', () => { 33 | expect( 34 | parseQuery({ 35 | limit: undefined, 36 | fields: undefined, 37 | orders: 'publishedAt', 38 | }), 39 | ).toBe('orders=publishedAt'); 40 | }); 41 | 42 | test('All undefined values results in an empty query string', () => { 43 | expect( 44 | parseQuery({ 45 | limit: undefined, 46 | fields: undefined, 47 | orders: undefined, 48 | }), 49 | ).toBe(''); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/write.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | import { createClient } from '../src/createClient'; 4 | 5 | import { testBaseUrl } from './mocks/handlers'; 6 | import { server } from './mocks/server'; 7 | 8 | const client = createClient({ 9 | serviceDomain: 'serviceDomain', 10 | apiKey: 'apiKey', 11 | }); 12 | 13 | interface ContentType { 14 | title: string; 15 | body?: string; 16 | } 17 | 18 | describe('create', () => { 19 | const postApiMockFn = jest.fn(); 20 | const putApiMockFn = jest.fn(); 21 | 22 | beforeEach(() => { 23 | server.use( 24 | http.post(`${testBaseUrl}/list-type`, async ({ request }) => { 25 | const url = new URL(request.url); 26 | const statusParams = url.searchParams.get('status'); 27 | const body = await request.json(); 28 | postApiMockFn(statusParams, body); 29 | return HttpResponse.json({ id: 'foo' }); 30 | }), 31 | http.put(`${testBaseUrl}/list-type/foo`, async ({ request }) => { 32 | const url = new URL(request.url); 33 | const statusParams = url.searchParams.get('status'); 34 | const body = await request.json(); 35 | putApiMockFn(statusParams, body); 36 | return HttpResponse.json({ id: 'foo' }, { status: 201 }); 37 | }), 38 | ); 39 | }); 40 | afterEach(() => { 41 | postApiMockFn.mockClear(); 42 | putApiMockFn.mockClear(); 43 | }); 44 | 45 | test('Content can be posted without specifying an id', async () => { 46 | const data = await client.create({ 47 | endpoint: 'list-type', 48 | content: { 49 | title: 'title', 50 | body: 'body', 51 | }, 52 | }); 53 | expect(data).toEqual({ id: 'foo' }); 54 | // Confirm POST api was called 55 | expect(postApiMockFn).toHaveBeenCalledTimes(1); 56 | // Confirm that body is specified. 57 | expect(postApiMockFn).toHaveBeenCalledWith(null, { 58 | title: 'title', 59 | body: 'body', 60 | }); 61 | }); 62 | test('Draft content can be posted by specifying an id', async () => { 63 | const data = await client.create({ 64 | endpoint: 'list-type', 65 | content: { 66 | title: 'title', 67 | body: 'body', 68 | }, 69 | isDraft: true, 70 | }); 71 | expect(data).toEqual({ id: 'foo' }); 72 | // Confirm POST api was called 73 | expect(postApiMockFn).toHaveBeenCalledTimes(1); 74 | // Confirm that status=draft is specified in the query string and that body is specified. 75 | expect(postApiMockFn).toHaveBeenCalledWith('draft', { 76 | title: 'title', 77 | body: 'body', 78 | }); 79 | }); 80 | test('Content can be posted by specifying an id', async () => { 81 | const data = await client.create({ 82 | endpoint: 'list-type', 83 | contentId: 'foo', 84 | content: { 85 | title: 'title', 86 | body: 'body', 87 | }, 88 | }); 89 | expect(data).toEqual({ id: 'foo' }); 90 | // Confirm PUT api was called 91 | expect(putApiMockFn).toHaveBeenCalledTimes(1); 92 | // Confirm that body is specified. 93 | expect(putApiMockFn).toHaveBeenCalledWith(null, { 94 | title: 'title', 95 | body: 'body', 96 | }); 97 | }); 98 | test('Draft content can be posted by specifying an id', async () => { 99 | const data = await client.create({ 100 | endpoint: 'list-type', 101 | contentId: 'foo', 102 | content: { 103 | title: 'title', 104 | body: 'body', 105 | }, 106 | isDraft: true, 107 | }); 108 | expect(data).toEqual({ id: 'foo' }); 109 | // Confirm PUT api was called 110 | expect(putApiMockFn).toHaveBeenCalledTimes(1); 111 | // Confirm that status=draft is specified in the query string and that body is specified. 112 | expect(putApiMockFn).toHaveBeenCalledWith('draft', { 113 | title: 'title', 114 | body: 'body', 115 | }); 116 | }); 117 | 118 | test('Returns an error message if `endpoint` is not specified', () => { 119 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 120 | // @ts-expect-error 121 | expect(client.create({})).rejects.toThrow( 122 | new Error('endpoint is required'), 123 | ); 124 | }); 125 | }); 126 | 127 | describe('update', () => { 128 | const patchListApiMockFn = jest.fn(); 129 | const patchObjectApiMockFn = jest.fn(); 130 | 131 | beforeEach(() => { 132 | server.use( 133 | http.patch(`${testBaseUrl}/list-type/foo`, async ({ request }) => { 134 | const body = await request.json(); 135 | patchListApiMockFn(body); 136 | return HttpResponse.json({ id: 'foo' }, { status: 200 }); 137 | }), 138 | http.patch(`${testBaseUrl}/object-type`, async ({ request }) => { 139 | const body = await request.json(); 140 | patchObjectApiMockFn(body); 141 | return HttpResponse.json({ id: 'foo' }, { status: 200 }); 142 | }), 143 | ); 144 | }); 145 | afterEach(() => { 146 | patchListApiMockFn.mockClear(); 147 | patchObjectApiMockFn.mockClear(); 148 | }); 149 | 150 | test('List format content can be updated', async () => { 151 | const data = await client.update({ 152 | endpoint: 'list-type', 153 | contentId: 'foo', 154 | content: { 155 | title: 'title', 156 | }, 157 | }); 158 | expect(data).toEqual({ id: 'foo' }); 159 | // Confirm PUT api was called 160 | expect(patchListApiMockFn).toHaveBeenCalledTimes(1); 161 | // Confirm that body is specified. 162 | expect(patchListApiMockFn).toHaveBeenCalledWith({ 163 | title: 'title', 164 | }); 165 | }); 166 | test('Object type content can be updated', async () => { 167 | const data = await client.update({ 168 | endpoint: 'object-type', 169 | content: { 170 | title: 'title', 171 | }, 172 | }); 173 | expect(data).toEqual({ id: 'foo' }); 174 | // Confirm PUT api was called 175 | expect(patchObjectApiMockFn).toHaveBeenCalledTimes(1); 176 | // Confirm that body is specified. 177 | expect(patchObjectApiMockFn).toHaveBeenCalledWith({ 178 | title: 'title', 179 | }); 180 | }); 181 | 182 | test('Returns an error message if `endpoint` is not specified', () => { 183 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 184 | // @ts-expect-error 185 | expect(client.update({})).rejects.toThrow( 186 | new Error('endpoint is required'), 187 | ); 188 | }); 189 | }); 190 | 191 | describe('delete', () => { 192 | const deleteApiMockFn = jest.fn(); 193 | 194 | beforeEach(() => { 195 | server.use( 196 | http.delete(`${testBaseUrl}/list-type/foo`, () => { 197 | deleteApiMockFn(); 198 | return new HttpResponse(null, { status: 202 }); 199 | }), 200 | ); 201 | }); 202 | afterEach(() => { 203 | deleteApiMockFn.mockClear(); 204 | }); 205 | 206 | test('List format content can be deleted', async () => { 207 | await client.delete({ endpoint: 'list-type', contentId: 'foo' }); 208 | expect(deleteApiMockFn).toHaveBeenCalledTimes(1); 209 | }); 210 | 211 | test('Returns an error message if `endpoint` is not specified', () => { 212 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 213 | // @ts-expect-error 214 | expect(client.delete({})).rejects.toThrow( 215 | new Error('endpoint is required'), 216 | ); 217 | }); 218 | test('Returns an error message if `contentId` is not specified', () => { 219 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 220 | // @ts-expect-error 221 | expect(client.delete({ endpoint: 'list-type' })).rejects.toThrow( 222 | new Error('contentId is required'), 223 | ); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es6", 5 | "allowJs": true, 6 | "resolveJsonModule": true, 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "lib": ["es6", "es2015", "es2016", "es2017", "dom"], 10 | "baseUrl": "./", 11 | "declaration": true, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "useUnknownInCatchVariables": false 15 | }, 16 | "include": ["src/**/*", "test/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig([ 4 | { 5 | name: 'main', 6 | entry: { 'microcms-js-sdk': './src/index.ts' }, 7 | format: ['cjs', 'esm'], 8 | legacyOutput: true, 9 | sourcemap: true, 10 | clean: true, 11 | bundle: true, 12 | splitting: false, 13 | dts: true, 14 | minify: true, 15 | }, 16 | { 17 | name: 'iife', 18 | entry: { 'microcms-js-sdk': './src/index.ts' }, 19 | legacyOutput: true, 20 | format: ['iife'], 21 | platform: 'browser', 22 | globalName: 'microcms', 23 | bundle: true, 24 | sourcemap: true, 25 | splitting: false, 26 | dts: false, 27 | minify: true, 28 | }, 29 | ]); 30 | --------------------------------------------------------------------------------