├── .eslintrc.json
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── rollup.config.js
├── src
├── createClient.ts
├── errorHandler.ts
├── index.ts
├── types.ts
└── utils
│ └── parseQuery.ts
├── test
├── createClient.test.ts
├── parseQuery.test.ts
└── tsconfig.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "parser": "@typescript-eslint/parser",
6 | "plugins": ["@typescript-eslint"],
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/recommended",
10 | "prettier"
11 | ],
12 | "rules": {
13 | "@typescript-eslint/explicit-module-boundary-types": "off"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | pull_request:
4 | types: [opened, synchronize, reopened]
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v4
11 | with:
12 | node-version: '22.13.1'
13 | cache: 'yarn'
14 | - run: yarn --frozen-lockfile
15 | - run: yarn lint:eslint
16 | - run: yarn lint:prettier
17 | - run: yarn build
18 | - run: yarn test
19 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: '22.13.1'
16 | registry-url: 'https://registry.npmjs.org'
17 | - run: yarn --frozen-lockfile
18 | - run: yarn lint:eslint
19 | - run: yarn lint:prettier
20 | - run: yarn build
21 | - run: yarn publish
22 | env:
23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | dist
5 | coverage
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | README.md
2 | CODE_OF_CONDUCT.md
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | info@newt.so.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 - 2022 Newt, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # End of Life Notice
2 |
3 | **The Newt headless CMS will be retired on 2026-11-24.**
4 |
5 | 2026年11月24日(火)をもちまして、Newt株式会社が提供しているヘッドレスCMS「Newt」のサービス提供を終了させていただくこととなりました。
6 |
7 | サービス終了後は、管理画面・APIともにご利用いただくことができなくなります。
8 |
9 | これまでヘッドレスCMS「Newt」をご支援・ご愛顧いただき、誠にありがとうございました。
10 |
11 | # JavaScript SDK for Newt's API
12 |
13 | JavaScript client for Newt. Works in Node.js and modern browsers.
14 |
15 | ## Supported browsers and Node.js versions
16 |
17 | - Chrome (>= 90)
18 | - Firefox (>= 90)
19 | - Edge (>= 90)
20 | - Safari (>= 13)
21 | - Node.js (>= 14)
22 |
23 | Other browsers should also work, but Newt Client requires ES6 Promise.
24 |
25 | ## Getting Started
26 |
27 | ### Installation
28 |
29 | Install the package with:
30 |
31 | ```sh
32 | npm install newt-client-js
33 |
34 | # or
35 |
36 | yarn add newt-client-js
37 | ```
38 |
39 | Using it directly in the browser:
40 |
41 | ```html
42 |
43 | ```
44 |
45 | ### Your first request
46 |
47 | The following code snippet is the most basic one you can use to get some content from Newt with this library:
48 |
49 | ```js
50 | const { createClient } = require('newt-client-js');
51 | const client = createClient({
52 | spaceUid: 'YOUR_SPACE_UID',
53 | token: 'YOUR_CDN_API_TOKEN',
54 | apiType: 'cdn' // You can specify "cdn" or "api".
55 | });
56 |
57 | client
58 | .getContent({
59 | appUid: 'YOUR_APP_UID',
60 | modelUid: 'YOUR_MODEL_UID',
61 | contentId: 'YOUR_CONTENT_ID'
62 | })
63 | .then((content) => console.log(content))
64 | .catch((err) => console.log(err));
65 | ```
66 |
67 | ## Documentation & References
68 |
69 | Please refer to the following documents.
70 |
71 | - [ガイド: SDK](https://www.newt.so/docs/js-sdk)
72 | - [ガイド: 利用可能なクエリ](https://www.newt.so/docs/cdn-api-newt-api-queries)
73 | - [チュートリアル: NewtのAPIで利用できるクエリパラメータを理解して、様々なクエリを実行する](https://www.newt.so/docs/tutorials/understanding-query-parameters)
74 | - [REST API reference](https://developers.newt.so/)
75 |
76 | ### Configuration
77 |
78 | The `createClient` method supports several options you may set to achieve the expected behavior:
79 |
80 | #### Options
81 |
82 | | Name | Default | Description |
83 | | :--- | :--- | :--- |
84 | | `spaceUid` | | **Required.** Your space uid. |
85 | | `token` | | **Required.** Your Newt CDN API token or Newt API token. |
86 | | `apiType` | `cdn` | You can specify `cdn` or `api`. Please specify `cdn` to send a request to the Newt CDN API, or `api` to send a request to the Newt API. |
87 | | `adapter` | `undefined` | Custom adapter to handle making the requests. Find further information in the axios request config documentation. |
88 | | `retryOnError` | `true` | By default, this client will retry if the response status is 429 too many requests or 500 server error. To turn off this behavior, set this to `false`. |
89 | | `retryLimit` | `3` | The number of times to retry before failure. Please specify a value less than or equal to `10`. |
90 | | `fetch` | `undefined` | You can specify a custom fetch function for the HTTP request like `globalThis.fetch` or `node-fetch`.
**Note that if you use the fetch option, the adapter option will be ignored and no retry will be performed.** |
91 |
92 | You can choose to use axios or fetch for your request, whichever you prefer.
93 |
94 | If you do not specify the `fetch` option, the request will be made with axios, in which case the values specified for the options `adapter`, `retryOnError`, and `retryLimit` will be taken into account.
95 |
96 | If you set a value for the `fetch` option, the request will be made using fetch instead of axios. Note that in that case, the `adapter` option will not be taken into account and retries will not be performed.
97 |
98 | ### Get contents
99 |
100 | ```js
101 | client
102 | .getContents({
103 | appUid: 'YOUR_APP_UID',
104 | modelUid: 'YOUR_MODEL_UID',
105 | query: {
106 | '_sys.createdAt': { gt: '2021-09-01' },
107 | category: 'news'
108 | }
109 | })
110 | .then((contents) => console.log(contents))
111 | .catch((err) => console.log(err));
112 |
113 | client
114 | .getContents({
115 | appUid: 'YOUR_APP_UID',
116 | modelUid: 'YOUR_MODEL_UID',
117 | query: {
118 | or: [
119 | { title: { match: 'update' } },
120 | { title: { match: 'アップデート' } }
121 | ],
122 | body: { fmt: 'text' },
123 | limit: 10
124 | }
125 | })
126 | .then((contents) => console.log(contents))
127 | .catch((err) => console.log(err));
128 | ```
129 |
130 | ### Get a content
131 |
132 | ```js
133 | client
134 | .getContent({
135 | appUid: 'YOUR_APP_UID',
136 | modelUid: 'YOUR_MODEL_UID',
137 | contentId: 'YOUR_CONTENT_ID'
138 | })
139 | .then((content) => console.log(content))
140 | .catch((err) => console.log(err));
141 |
142 | client
143 | .getContent({
144 | appUid: 'YOUR_APP_UID',
145 | modelUid: 'YOUR_MODEL_UID',
146 | contentId: 'YOUR_CONTENT_ID',
147 | query: { select: ['title', 'body'] }
148 | })
149 | .then((content) => console.log(content))
150 | .catch((err) => console.log(err));
151 | ```
152 |
153 | ### Get first content
154 |
155 | The getFirstContent method will return the first content that matches the condition specified in query.
156 | You can set the parameters available for getContents except for limit.
157 |
158 | ```js
159 | client
160 | .getFirstContent({
161 | appUid: 'YOUR_APP_UID',
162 | modelUid: 'YOUR_MODEL_UID',
163 | query: {
164 | slug: 'hello-world'
165 | }
166 | })
167 | .then((content) => console.log(content))
168 | .catch((err) => console.log(err));
169 | ```
170 |
171 | ### Get an app
172 |
173 | ```js
174 | client
175 | .getApp({
176 | appUid: 'YOUR_APP_UID'
177 | })
178 | .then((app) => console.log(app))
179 | .catch((err) => console.log(err));
180 | ```
181 |
182 | ### Usage with TypeScript
183 |
184 | #### Type definition
185 |
186 | By using the type Content, you can easily define the type.
187 |
188 | ```ts
189 | // Suppose you have defined a model named Post in the admin page.
190 |
191 | // Type definition
192 | /**
193 | * Content type
194 | *
195 | * {
196 | * _id: string;
197 | * _sys: {
198 | * createdAt: string;
199 | * updatedAt: string;
200 | * customOrder: number;
201 | * raw: {
202 | * createdAt: string;
203 | * updatedAt: string;
204 | * firstPublishedAt: string;
205 | * publishedAt: string;
206 | * };
207 | * };
208 | * }
209 | */
210 | const { Content } = require('newt-client-js');
211 | interface Post extends Content {
212 | title: string
213 | body: string
214 | }
215 | ```
216 |
217 | #### Request and Response
218 |
219 | The type of the content you want to get can be passed as a parameter.
220 |
221 | ```ts
222 | /**
223 | * getContents response type
224 | *
225 | * {
226 | * skip: number;
227 | * limit: number;
228 | * total: number;
229 | * items: Array; // an array of content defined by you
230 | * }
231 | */
232 | client.getContents({
233 | appUid: 'YOUR_APP_UID',
234 | modelUid: 'YOUR_MODEL_UID',
235 | })
236 | .then((posts) => console.log(posts))
237 | .catch((err) => console.log(err));
238 |
239 | /**
240 | * getContent response type
241 | *
242 | * {
243 | * _id: string;
244 | * _sys: {
245 | * createdAt: string;
246 | * updatedAt: string;
247 | * customOrder: number;
248 | * raw: {
249 | * createdAt: string;
250 | * updatedAt: string;
251 | * firstPublishedAt: string;
252 | * publishedAt: string;
253 | * };
254 | * };
255 | * title: string; // field defined by you
256 | * body: string; // field defined by you
257 | * }
258 | */
259 | client
260 | .getContent({
261 | appUid: 'YOUR_APP_UID',
262 | modelUid: 'YOUR_MODEL_UID',
263 | contentId: 'YOUR_CONTENT_ID'
264 | })
265 | .then((post) => console.log(post))
266 | .catch((err) => console.log(err));
267 |
268 | /**
269 | * getFirstContent response type
270 | *
271 | * {
272 | * _id: string;
273 | * _sys: {
274 | * createdAt: string;
275 | * updatedAt: string;
276 | * customOrder: number;
277 | * raw: {
278 | * createdAt: string;
279 | * updatedAt: string;
280 | * firstPublishedAt: string;
281 | * publishedAt: string;
282 | * };
283 | * };
284 | * title: string; // field defined by you
285 | * body: string; // field defined by you
286 | * }
287 | */
288 | client
289 | .getFirstContent({
290 | appUid: 'YOUR_APP_UID',
291 | modelUid: 'YOUR_MODEL_UID',
292 | })
293 | .then((post) => console.log(post))
294 | .catch((err) => console.log(err));
295 | ```
296 |
297 | #### Query Fields
298 |
299 | All fields are optional.
300 |
301 | | Field | Type | Description |
302 | | :--- | :--- | :--- |
303 | | `YOUR_FIELD` | - | You can define a query for a field that you define |
304 | | `select` | string[] | |
305 | | `order` | string[] | |
306 | | `limit` | number | |
307 | | `skip` | number| |
308 | | `depth` | number | |
309 | | `or` | Array | Connects each element in the array as an "or" condition. |
310 | | `and` | Array | Connects each element in the array as an "and" condition. |
311 |
312 | #### Query Operators
313 |
314 | | Operator | Type |
315 | | :--- | :--- |
316 | | `ne` | string / number / boolean |
317 | | `match` | string |
318 | | `in` | string[] / number[] |
319 | | `nin` | string[] / number[] |
320 | | `all` | string[] / number[] |
321 | | `exists` | boolean |
322 | | `lt` | string / number |
323 | | `lte` | string / number |
324 | | `gt` | string / number |
325 | | `gte` | string / number |
326 | | `fmt` | 'text' |
327 |
328 | ## License
329 |
330 | This repository is published under the [MIT License](https://github.com/Newt-Inc/newt-client-js/blob/main/LICENSE).
331 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | verbose: true,
5 | transform: {
6 | '^.+\\.ts$': [
7 | 'ts-jest',
8 | {
9 | tsconfig: 'test/tsconfig.json',
10 | },
11 | ],
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "newt-client-js",
3 | "version": "3.3.8",
4 | "description": "JavaScript SDK for Newt's API",
5 | "main": "./dist/cjs/newtClient.js",
6 | "module": "./dist/esm/newtClient.js",
7 | "jsdelivr": "dist/umd/newtClient.js",
8 | "types": "./dist/types/index.d.ts",
9 | "files": [
10 | "dist"
11 | ],
12 | "keywords": [
13 | "newt"
14 | ],
15 | "author": "Newt, Inc.",
16 | "license": "MIT",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/Newt-Inc/newt-client-js.git"
20 | },
21 | "engines": {
22 | "node": ">=14"
23 | },
24 | "scripts": {
25 | "dev": "rollup -c -w",
26 | "build": "yarn clean && rollup -c",
27 | "clean": "rimraf dist",
28 | "lint:eslint": "eslint ./src",
29 | "lint:prettier": "prettier --check ./src",
30 | "fix:eslint": "eslint --fix ./src",
31 | "fix:prettier": "prettier --write ./src",
32 | "test": "jest",
33 | "test:coverage": "jest --coverage=true"
34 | },
35 | "dependencies": {
36 | "axios": "^1.7.4",
37 | "axios-retry": "^3.2.4",
38 | "qs": "^6.10.1"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.17.0",
42 | "@babel/preset-env": "^7.16.11",
43 | "@rollup/plugin-babel": "^5.3.0",
44 | "@rollup/plugin-commonjs": "^21.0.1",
45 | "@rollup/plugin-node-resolve": "^13.1.3",
46 | "@rollup/plugin-terser": "^0.4.4",
47 | "@types/jest": "^29.5.11",
48 | "@types/node": "22.13.1",
49 | "@types/qs": "^6.9.7",
50 | "@typescript-eslint/eslint-plugin": "^6.19.0",
51 | "@typescript-eslint/parser": "^6.19.0",
52 | "eslint": "^7.32.0",
53 | "eslint-config-prettier": "^8.3.0",
54 | "jest": "^29.7.0",
55 | "prettier": "^2.3.2",
56 | "rimraf": "^3.0.2",
57 | "rollup": "^2.67.1",
58 | "rollup-plugin-typescript2": "^0.31.2",
59 | "ts-jest": "^29.1.1",
60 | "tslib": "^2.3.1",
61 | "typescript": "^5.3.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from '@rollup/plugin-babel'
2 | import commonjs from '@rollup/plugin-commonjs'
3 | import { nodeResolve } from '@rollup/plugin-node-resolve'
4 | import terser from '@rollup/plugin-terser'
5 | import typescript from 'rollup-plugin-typescript2'
6 | import pkg from './package.json'
7 |
8 | const name = 'newtClient'
9 |
10 | export default [
11 | // cjs
12 | {
13 | input: './src/index.ts',
14 | output: [
15 | {
16 | file: `./dist/cjs/${name}.js`,
17 | sourcemap: 'inline',
18 | format: 'cjs',
19 | },
20 | ],
21 | external: [...Object.keys(pkg.dependencies || {})],
22 | plugins: [
23 | typescript({ useTsconfigDeclarationDir: true }),
24 | babel({
25 | babelHelpers: 'bundled',
26 | extensions: ['.ts', '.js'],
27 | presets: [['@babel/preset-env', { targets: { node: '16' } }]],
28 | }),
29 | terser(),
30 | ],
31 | },
32 | // esm
33 | {
34 | input: './src/index.ts',
35 | output: [
36 | {
37 | file: `./dist/esm/${name}.js`,
38 | sourcemap: 'inline',
39 | format: 'esm',
40 | },
41 | ],
42 | external: [...Object.keys(pkg.dependencies || {})],
43 | plugins: [
44 | typescript({
45 | tsconfigOverride: { compilerOptions: { declaration: false } },
46 | }),
47 | babel({
48 | babelHelpers: 'bundled',
49 | extensions: ['.ts', '.js'],
50 | presets: [
51 | [
52 | '@babel/preset-env',
53 | {
54 | targets: {
55 | edge: '90',
56 | firefox: '90',
57 | chrome: '90',
58 | safari: '13',
59 | node: '14',
60 | },
61 | },
62 | ],
63 | ],
64 | }),
65 | terser(),
66 | ],
67 | },
68 | // umd
69 | {
70 | input: './src/index.ts',
71 | output: [
72 | {
73 | name: name,
74 | file: `./dist/umd/${name}.js`,
75 | format: 'umd',
76 | },
77 | ],
78 | plugins: [
79 | nodeResolve({ browser: true }),
80 | typescript({
81 | tsconfigOverride: { compilerOptions: { declaration: false } },
82 | }),
83 | commonjs({ extensions: ['.ts', '.js'] }),
84 | babel({
85 | babelHelpers: 'bundled',
86 | extensions: ['.ts', '.js'],
87 | presets: [
88 | [
89 | '@babel/preset-env',
90 | {
91 | targets: {
92 | edge: '90',
93 | firefox: '90',
94 | chrome: '90',
95 | safari: '13',
96 | },
97 | },
98 | ],
99 | ],
100 | }),
101 | terser(),
102 | ],
103 | },
104 | ]
105 |
--------------------------------------------------------------------------------
/src/createClient.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import axiosRetry from 'axios-retry'
3 | import { parseQuery } from './utils/parseQuery'
4 | import {
5 | CreateClientParams,
6 | GetContentsParams,
7 | GetContentParams,
8 | GetFirstContentParams,
9 | Contents,
10 | GetAppParams,
11 | AppMeta,
12 | } from './types'
13 | import { axiosErrorHandler, fetchErrorHandler } from './errorHandler'
14 |
15 | export const createClient = ({
16 | spaceUid,
17 | token,
18 | apiType = 'cdn',
19 | adapter = undefined,
20 | retryOnError = true,
21 | retryLimit = 3,
22 | fetch = undefined,
23 | }: CreateClientParams) => {
24 | if (!spaceUid) throw new Error('spaceUid parameter is required.')
25 | if (!token) throw new Error('token parameter is required.')
26 | if (!['cdn', 'api'].includes(apiType))
27 | throw new Error(
28 | `apiType parameter should be set to "cdn" or "api". apiType: ${apiType}`
29 | )
30 | if (retryLimit > 10)
31 | throw new Error('retryLimit should be a value less than or equal to 10.')
32 |
33 | const baseUrl = new URL(`https://${spaceUid}.${apiType}.newt.so`)
34 | const headers = { Authorization: `Bearer ${token}` }
35 | const axiosInstance = axios.create({
36 | baseURL: baseUrl.toString(),
37 | headers,
38 | adapter,
39 | })
40 |
41 | if (retryOnError) {
42 | axiosRetry(axiosInstance, {
43 | retries: retryLimit,
44 | retryCondition: (error) => {
45 | return error.response?.status === 429 || error.response?.status === 500
46 | },
47 | retryDelay: (retryCount) => {
48 | return retryCount * 1000
49 | },
50 | })
51 | }
52 |
53 | const getContents = async ({
54 | appUid,
55 | modelUid,
56 | query,
57 | }: GetContentsParams): Promise> => {
58 | if (!appUid) throw new Error('appUid parameter is required.')
59 | if (!modelUid) throw new Error('modelUid parameter is required.')
60 |
61 | const url = new URL(`/v1/${appUid}/${modelUid}`, baseUrl.toString())
62 | if (query && Object.keys(query).length) {
63 | const { encoded } = parseQuery(query)
64 | url.search = encoded
65 | }
66 |
67 | if (fetch) {
68 | const req = {
69 | method: 'get',
70 | headers,
71 | url: url.toString(),
72 | }
73 |
74 | try {
75 | const res = await fetch(url.toString(), { headers })
76 | const data = await res.json()
77 | if (!res.ok) {
78 | return fetchErrorHandler(data, req)
79 | }
80 | return data
81 | } catch (err) {
82 | return fetchErrorHandler(err, req)
83 | }
84 | } else {
85 | try {
86 | const { data } = await axiosInstance.get(url.pathname + url.search)
87 | return data
88 | } catch (err) {
89 | return axiosErrorHandler(err)
90 | }
91 | }
92 | }
93 |
94 | const getContent = async ({
95 | appUid,
96 | modelUid,
97 | contentId,
98 | query,
99 | }: GetContentParams): Promise => {
100 | if (!appUid) throw new Error('appUid parameter is required.')
101 | if (!modelUid) throw new Error('modelUid parameter is required.')
102 | if (!contentId) throw new Error('contentId parameter is required.')
103 |
104 | const url = new URL(
105 | `/v1/${appUid}/${modelUid}/${contentId}`,
106 | baseUrl.toString()
107 | )
108 | if (query && Object.keys(query).length) {
109 | const { encoded } = parseQuery(query)
110 | url.search = encoded
111 | }
112 |
113 | if (fetch) {
114 | const req = {
115 | method: 'get',
116 | headers,
117 | url: url.toString(),
118 | }
119 |
120 | try {
121 | const res = await fetch(url.toString(), { headers })
122 | const data = await res.json()
123 | if (!res.ok) {
124 | return fetchErrorHandler(data, req)
125 | }
126 | return data
127 | } catch (err) {
128 | return fetchErrorHandler(err, req)
129 | }
130 | } else {
131 | try {
132 | const { data } = await axiosInstance.get(url.pathname + url.search)
133 | return data
134 | } catch (err) {
135 | return axiosErrorHandler(err)
136 | }
137 | }
138 | }
139 |
140 | const getFirstContent = async ({
141 | appUid,
142 | modelUid,
143 | query,
144 | }: GetFirstContentParams): Promise => {
145 | if (query && query.limit) {
146 | throw new Error('query.limit parameter cannot have a value.')
147 | }
148 | const q = { ...query, limit: 1 }
149 |
150 | const { items } = await getContents({ appUid, modelUid, query: q })
151 | if (items.length === 0) return null
152 | return items[0]
153 | }
154 |
155 | const getApp = async ({ appUid }: GetAppParams): Promise => {
156 | if (!appUid) throw new Error('appUid parameter is required.')
157 | const url = new URL(`/v1/space/apps/${appUid}`, baseUrl.toString())
158 | if (fetch) {
159 | const req = {
160 | method: 'get',
161 | headers,
162 | url: url.toString(),
163 | }
164 |
165 | try {
166 | const res = await fetch(url.toString(), { headers })
167 | const data = await res.json()
168 | if (!res.ok) {
169 | return fetchErrorHandler(data, req)
170 | }
171 | return data
172 | } catch (err) {
173 | return fetchErrorHandler(err, req)
174 | }
175 | } else {
176 | try {
177 | const { data } = await axiosInstance.get(url.pathname)
178 | return data
179 | } catch (err) {
180 | return axiosErrorHandler(err)
181 | }
182 | }
183 | }
184 |
185 | return {
186 | getContents,
187 | getContent,
188 | getFirstContent,
189 | getApp,
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/errorHandler.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import type { ErrorRequest, ErrorResponse, NewtError } from './types'
3 |
4 | export const axiosErrorHandler = (errorResponse: unknown): never => {
5 | if (!axios.isAxiosError(errorResponse)) {
6 | throw errorResponse
7 | }
8 |
9 | const { config, response } = errorResponse
10 | if (!response?.data) {
11 | throw errorResponse
12 | }
13 |
14 | const { data } = response
15 | const errorData: ErrorResponse = {
16 | status: data.status,
17 | code: data.code,
18 | message: data.message,
19 | }
20 |
21 | if (config) {
22 | errorData.request = {
23 | method: config.method,
24 | headers: config.headers,
25 | }
26 | if (config.url) {
27 | const url = new URL(config.url, config.baseURL)
28 | errorData.request.url = url.toString()
29 | }
30 | }
31 |
32 | const error = new Error()
33 | error.name = `${data.status} ${data.code}`
34 | try {
35 | error.message = JSON.stringify(errorData, null, 2)
36 | } catch {
37 | error.message = data.message
38 | }
39 | throw error
40 | }
41 |
42 | const isNewtError = (value: unknown): value is NewtError => {
43 | if (typeof value !== 'object' || value === null) {
44 | return false
45 | }
46 | const email = value as Record
47 | if (typeof email.status !== 'number') {
48 | return false
49 | }
50 | if (typeof email.code !== 'string') {
51 | return false
52 | }
53 | if (typeof email.message !== 'string') {
54 | return false
55 | }
56 | return true
57 | }
58 |
59 | export const fetchErrorHandler = (
60 | errorResponse: unknown,
61 | errorRequest: ErrorRequest
62 | ): never => {
63 | if (!isNewtError(errorResponse)) {
64 | throw errorResponse
65 | }
66 |
67 | const { status, code, message } = errorResponse
68 | const errorData: ErrorResponse = {
69 | status,
70 | code,
71 | message,
72 | request: errorRequest,
73 | }
74 |
75 | const error = new Error()
76 | error.name = `${status} ${code}`
77 | try {
78 | error.message = JSON.stringify(errorData, null, 2)
79 | } catch {
80 | error.message = message
81 | }
82 | throw error
83 | }
84 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { createClient } from './createClient'
2 | export {
3 | CreateClientParams,
4 | Client,
5 | GetContentsParams,
6 | GetContentParams,
7 | GetFirstContentParams,
8 | GetContentsQuery,
9 | GetContentQuery,
10 | GetFirstContentQuery,
11 | Contents,
12 | Content,
13 | Image,
14 | File,
15 | Media,
16 | AppMeta,
17 | AppCover,
18 | AppIcon,
19 | GetAppParams,
20 | NewtError,
21 | ErrorRequest,
22 | ErrorResponse,
23 | } from './types'
24 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { AxiosAdapter } from 'axios'
2 |
3 | export interface CreateClientParams {
4 | spaceUid: string
5 | token: string
6 | apiType?: 'cdn' | 'api'
7 | adapter?: AxiosAdapter
8 | retryOnError?: boolean
9 | retryLimit?: number
10 | fetch?: typeof fetch
11 | }
12 |
13 | export interface Client {
14 | getContents: (params: GetContentsParams) => Promise>
15 | getContent: (params: GetContentParams) => Promise
16 | getFirstContent: (params: GetFirstContentParams) => Promise
17 | getApp: (params: GetAppParams) => Promise
18 | }
19 |
20 | export interface GetContentsParams {
21 | appUid: string
22 | modelUid: string
23 | query?: GetContentsQuery
24 | }
25 |
26 | export interface GetContentParams {
27 | appUid: string
28 | modelUid: string
29 | contentId: string
30 | query?: GetContentQuery
31 | }
32 |
33 | export interface GetFirstContentParams {
34 | appUid: string
35 | modelUid: string
36 | query?: GetFirstContentQuery
37 | }
38 |
39 | type OperatorValue = {
40 | ne?: string | number | boolean
41 | match?: string
42 | in?: string[] | number[]
43 | nin?: string[] | number[]
44 | all?: string[] | number[]
45 | exists?: boolean
46 | lt?: string | number
47 | lte?: string | number
48 | gt?: string | number
49 | gte?: string | number
50 | fmt?: 'text'
51 | }
52 |
53 | type QueryValue = string | number | boolean | OperatorValue
54 |
55 | export type FilterQuery = {
56 | or?: Array
57 | and?: Array
58 | [key: string]: QueryValue | Array | undefined
59 | }
60 |
61 | export type Query = {
62 | select?: string[]
63 | order?: string[]
64 | limit?: number
65 | skip?: number
66 | depth?: number
67 | or?: Array
68 | and?: Array
69 | [key: string]: QueryValue | string[] | Array | undefined
70 | }
71 |
72 | export type GetContentsQuery = Query
73 |
74 | type ExceptFormat = {
75 | select?: string[]
76 | depth?: number
77 | }
78 |
79 | type Format = {
80 | [key: string]: {
81 | fmt: 'text'
82 | }
83 | }
84 |
85 | export type GetContentQuery = ExceptFormat | Format
86 |
87 | export type GetFirstContentQuery = Omit
88 |
89 | export interface Contents {
90 | skip: number
91 | limit: number
92 | total: number
93 | items: Array
94 | }
95 |
96 | export interface Content {
97 | _id: string
98 | _sys: {
99 | createdAt: string
100 | updatedAt: string
101 | customOrder: number
102 | raw: {
103 | createdAt: string
104 | updatedAt: string
105 | firstPublishedAt: string
106 | publishedAt: string
107 | }
108 | }
109 | }
110 |
111 | export interface Image {
112 | _id: string
113 | src: string
114 | fileName: string
115 | fileType: string
116 | fileSize: number
117 | width: number
118 | height: number
119 | title: string
120 | description: string
121 | altText: string
122 | metadata: Record
123 | }
124 |
125 | export interface File {
126 | _id: string
127 | src: string
128 | fileName: string
129 | fileType: string
130 | fileSize: number
131 | width: number | null
132 | height: number | null
133 | title: string
134 | description: string
135 | altText: string
136 | metadata: Record
137 | }
138 |
139 | // deprecated
140 | export interface Media {
141 | _id: string
142 | src: string
143 | fileName: string
144 | fileType: string
145 | fileSize: number
146 | width: number | null
147 | height: number | null
148 | title: string
149 | description: string
150 | altText: string
151 | metadata: Record
152 | }
153 |
154 | export type AppIcon = {
155 | type: string
156 | value: string
157 | }
158 |
159 | export type AppCover = {
160 | type: string
161 | value: string
162 | }
163 |
164 | export interface AppMeta {
165 | name: string
166 | uid: string
167 | icon?: AppIcon
168 | cover?: AppCover
169 | }
170 |
171 | export interface GetAppParams {
172 | appUid: string
173 | }
174 |
175 | export interface NewtError {
176 | status: number
177 | code: string
178 | message: string
179 | }
180 |
181 | export interface ErrorRequest {
182 | method?: string
183 | headers?: Record
184 | url?: string
185 | }
186 |
187 | export interface ErrorResponse extends NewtError {
188 | request?: ErrorRequest
189 | }
190 |
--------------------------------------------------------------------------------
/src/utils/parseQuery.ts:
--------------------------------------------------------------------------------
1 | import { stringify } from 'qs'
2 | import { FilterQuery, Query } from '../types'
3 |
4 | const parseAndQuery = (andQuery: FilterQuery[]) => {
5 | if (!andQuery) throw new Error('invalid query')
6 | const rawAndConditions: string[] = []
7 | const encodedAndConditions: string[] = []
8 |
9 | andQuery.forEach((query: FilterQuery) => {
10 | const { raw, encoded } = parseQuery(query)
11 | rawAndConditions.push(raw)
12 | encodedAndConditions.push(encoded)
13 | })
14 | const rawQ = rawAndConditions.join('&')
15 | const encodedQ = encodedAndConditions.join('&')
16 | return { raw: rawQ, encoded: encodedQ }
17 | }
18 |
19 | const parseOrQuery = (orQuery: FilterQuery[]) => {
20 | if (!orQuery) throw new Error('invalid query')
21 | const rawOrConditions: string[] = []
22 |
23 | orQuery.forEach((query: FilterQuery) => {
24 | const { raw } = parseQuery(query)
25 | rawOrConditions.push(raw)
26 | })
27 | const params = new URLSearchParams()
28 | params.set('[or]', `(${rawOrConditions.join(';')})`)
29 | const rawQ = `[or]=(${rawOrConditions.join(';')})`
30 | return { raw: rawQ, encoded: params.toString() }
31 | }
32 |
33 | export const parseQuery = (query: Query) => {
34 | let andQuery = { raw: '', encoded: '' }
35 | if (query.and) {
36 | andQuery = parseAndQuery(query.and)
37 | delete query.and
38 | }
39 |
40 | let orQuery = { raw: '', encoded: '' }
41 | if (query.or) {
42 | orQuery = parseOrQuery(query.or)
43 | delete query.or
44 | }
45 |
46 | const rawQuery = stringify(query, { encode: false, arrayFormat: 'comma' })
47 | const encodedQuery = stringify(query, { arrayFormat: 'comma' })
48 | const raw = [rawQuery, orQuery.raw, andQuery.raw]
49 | .filter((queryString) => queryString)
50 | .join('&')
51 | const encoded = [encodedQuery, orQuery.encoded, andQuery.encoded]
52 | .filter((queryString) => queryString)
53 | .join('&')
54 | return { raw, encoded }
55 | }
56 |
--------------------------------------------------------------------------------
/test/createClient.test.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from '../src'
2 |
3 | describe('createClient', (): void => {
4 | test('Throws if no spaceUid is defined', () => {
5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6 | // @ts-expect-error
7 | expect(() => createClient({ token: 'token' })).toThrow(
8 | /spaceUid parameter is required/
9 | )
10 | })
11 | test('Throws if no token is defined', () => {
12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13 | // @ts-expect-error
14 | expect(() => createClient({ spaceUid: 'spaceUid' })).toThrow(
15 | /token parameter is required/
16 | )
17 | })
18 | test('Throws if apiType is neither cdn nor api', () => {
19 | expect(() =>
20 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
21 | // @ts-expect-error
22 | createClient({ spaceUid: 'spaceUid', token: 'token', apiType: 'xxx' })
23 | ).toThrow(/apiType parameter should be set to "cdn" or "api"/)
24 | })
25 | test('Throws if retryLimit is greater than 10', () => {
26 | expect(() =>
27 | createClient({
28 | spaceUid: 'spaceUid',
29 | token: 'token',
30 | apiType: 'cdn',
31 | retryLimit: 11,
32 | })
33 | ).toThrow(/retryLimit should be a value less than or equal to 10/)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/test/parseQuery.test.ts:
--------------------------------------------------------------------------------
1 | import { parseQuery } from '../src/utils/parseQuery';
2 | import { GetContentsQuery } from '../src/types';
3 | import { parse } from 'qs';
4 |
5 | describe('parseQuery', (): void => {
6 | test('should connect multiple conditions', (): void => {
7 | const query: GetContentsQuery = {
8 | '_sys.createdAt': { gt: '2021-12-01' },
9 | category: 'news',
10 | };
11 | const { encoded } = parseQuery(query);
12 |
13 | const parsed = parse(encoded);
14 | expect(parsed['_sys.createdAt']).toEqual({ gt: '2021-12-01' });
15 | expect(parsed.category).toBe('news');
16 | });
17 |
18 | test('should connect multiple conditions for a single key', (): void => {
19 | const query: GetContentsQuery = {
20 | '_sys.createdAt': {
21 | gt: '2021-11-01',
22 | lt: '2021-12-01',
23 | },
24 | };
25 | const { encoded } = parseQuery(query);
26 |
27 | const parsed = parse(encoded);
28 | expect(parsed['_sys.createdAt']).toEqual({
29 | gt: '2021-11-01',
30 | lt: '2021-12-01',
31 | });
32 | });
33 |
34 | test('should connect conditions including "or"', (): void => {
35 | const query: GetContentsQuery = {
36 | body: { fmt: 'text' },
37 | limit: 10,
38 | or: [
39 | { title: { match: 'update' } },
40 | { title: { match: 'アップデート' } },
41 | ],
42 | };
43 | const { encoded } = parseQuery(query);
44 | const { body, limit, or } = parse(encoded);
45 | expect(body).toEqual({ fmt: 'text' });
46 | expect(limit).toBe('10');
47 | expect(or).toBe('(title[match]=update;title[match]=アップデート)');
48 | });
49 |
50 | test('should connect nested "or"', (): void => {
51 | const query: GetContentsQuery = {
52 | body: { fmt: 'text' },
53 | limit: 10,
54 | or: [
55 | { title: { match: 'update' } },
56 | {
57 | '_sys.createdAt': { gt: '2021-11-01' },
58 | '_sys.updatedAt': { gt: '2021-12-01' },
59 | },
60 | {
61 | or: [{ title: { match: 'release' } }, { category: 'release' }],
62 | },
63 | {
64 | '_sys.createdAt': { gt: '2021-12-01' },
65 | or: [{ title: { match: 'news' } }, { category: 'news' }],
66 | },
67 | ],
68 | };
69 | const { encoded } = parseQuery(query);
70 |
71 | const { body, limit, or } = parse(encoded);
72 | expect(body).toEqual({ fmt: 'text' });
73 | expect(limit).toBe('10');
74 | expect(or).toBe(
75 | '(title[match]=update;_sys.createdAt[gt]=2021-11-01&_sys.updatedAt[gt]=2021-12-01;[or]=(title[match]=release;category=release);_sys.createdAt[gt]=2021-12-01&[or]=(title[match]=news;category=news))'
76 | );
77 | });
78 |
79 | test('should connect each elements in the and parameter', (): void => {
80 | const query: GetContentsQuery = {
81 | body: { fmt: 'text' },
82 | limit: 10,
83 | or: [
84 | { title: { match: 'update' } },
85 | {
86 | '_sys.createdAt': { gt: '2021-11-01' },
87 | or: [{ title: { match: 'news' } }, { category: 'news' }],
88 | },
89 | {
90 | '_sys.createdAt': { gt: '2021-12-01' },
91 | and: [
92 | { category: 'important' },
93 | { or: [{ title: { match: 'support' } }, { category: 'support' }] },
94 | { or: [{ title: { match: 'help' } }, { category: 'help' }] },
95 | ],
96 | },
97 | ],
98 | and: [
99 | { body: { exists: true } },
100 | {
101 | or: [{ body: { match: 'cool' } }, { body: { match: 'great' } }],
102 | },
103 | {
104 | or: [{ description: { exists: true } }, { detail: { exists: true } }],
105 | },
106 | ],
107 | };
108 | const { encoded } = parseQuery(query);
109 | const { body, limit, or } = parse(encoded);
110 | expect(body).toEqual({ fmt: 'text', exists: 'true' });
111 | expect(limit).toBe('10');
112 | expect(or).toEqual([
113 | '(title[match]=update;_sys.createdAt[gt]=2021-11-01&[or]=(title[match]=news;category=news);_sys.createdAt[gt]=2021-12-01&category=important&[or]=(title[match]=support;category=support)&[or]=(title[match]=help;category=help))',
114 | '(body[match]=cool;body[match]=great)',
115 | '(description[exists]=true;detail[exists]=true)',
116 | ]);
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "../"
5 | },
6 | "include": ["../src", "../test"]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "target": "esnext",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "moduleResolution": "node",
10 | "outDir": "./dist",
11 | "rootDir": "./src",
12 | "declaration": true,
13 | "declarationDir": "./dist/types"
14 | },
15 | "include": ["./src/**/*"]
16 | }
17 |
--------------------------------------------------------------------------------