├── .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 |
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 |
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 |
--------------------------------------------------------------------------------