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