[
  {
    "path": ".browserslistrc",
    "content": "# Browsers that we support\n\nChrome >=87\nFirefox >=78\nSafari >=13\nEdge >=88\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: npm\n    directory: /\n    schedule:\n      interval: daily\n      time: '04:00'\n    commit-message:\n      prefix: fix\n      prefix-development: chore\n      include: scope\n    labels:\n      - dependabot\n    groups:\n      security-updates:\n        patterns:\n          - '*'\n        exclude-patterns:\n          - 'storyblok*'\n        update-types:\n          - patch\n    ignore:\n      - dependency-name: '*'\n        update-types:\n          - version-update:semver-minor\n          - version-update:semver-major\n"
  },
  {
    "path": ".github/issue.bug.md",
    "content": "---\nname: Create an issue\nabout: Create an issue to help us improve\n---\n\n[storyblokurl]: https://www.storyblok.com?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client\n\n[![storyblok.com](https://a.storyblok.com/f/88751/1776x360/4d075611c6/sb-js-sdk.png)][storyblokurl]\n\n<!--- Please provide a general summary of the bug in the Title above -->\n\n---\n\n## Expected Behavior\n\n<!--- Please tell us what should happen -->\n\n## Current Behavior\n\n<!--- Please tell us what happens instead of the expected behavior -->\n\n## Steps to Reproduce\n\n<!--- Please provide us with all steps needed to reproduce this bug. Screenshots and videos are also welcome. -->\n<!--- Please include code to reproduce as well, if possible. -->\n\n1.\n2.\n3.\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--- Please provide a general summary of your changes in the title above -->\n\n## Pull request type\n\nJira Link: [INT-](url)\n\n<!-- Please try to limit your pull request to one type, submit multiple pull requests if needed.\n\nPlease check the type of change your PR introduces:-->\n\n- [ ] Bugfix\n- [ ] Feature\n- [ ] Code style update (formatting, renaming)\n- [ ] Refactoring (no functional changes, no api changes)\n- [ ] Other (please describe):\n\n## How to test this PR\n\n<!-- Please provide the steps on how to test this PR. -->\n\n## What is the new behavior?\n\n<!-- Please describe the behavior or changes that are being added by this PR. -->\n\n-\n-\n-\n\n## Other information\n"
  },
  {
    "path": ".github/workflows/commitlint.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\n\njobs:\n  commitlint:\n    # Skip job if PR is from Dependabot\n    if: github.actor != 'dependabot[bot]'\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Validate current commit (last commit) with commitlint\n        if: github.event_name == 'push'\n        run: pnpm commitlint --last --verbose\n      - name: Validate PR commits with commitlint\n        if: github.event_name == 'pull_request'\n        run: pnpm commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose\n"
  },
  {
    "path": ".github/workflows/dependabot-autoapprove.yml",
    "content": "name: Dependabot auto-approve\non: pull_request\n\npermissions:\n  pull-requests: write\n\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'storyblok/storyblok-js-client'\n    steps:\n      - name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@v2\n        with:\n          github-token: '${{ secrets.GITHUB_TOKEN }}'\n          alert-lookup: true\n      - uses: actions/checkout@v4\n      - name: Approve a PR if not already approved\n        run: |\n          gh pr checkout \"$PR_URL\" # sets the upstream metadata for `gh pr status`\n          if [ \"$(gh pr status --json reviewDecision -q .currentBranch.reviewDecision)\" != \"APPROVED\" ];\n          then gh pr review --approve \"$PR_URL\"\n          else echo \"PR already approved, skipping additional approvals to minimize emails/notification noise.\";\n          fi\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/license-checker.yml",
    "content": "name: Check licenses\non:\n  pull_request:\n    types:\n      - opened\n    paths:\n      - .npmrc\n      - package.json\n      - pnpm-lock.yaml\n  push:\n    branches:\n      - '**'\n    paths:\n      - .npmrc\n      - package.json\n      - pnpm-lock.yaml\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\njobs:\n  check-licenses:\n    name: Check licenses\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        node-version: [20]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Run License Checker\n        run: pnpm run check-licenses\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Run linters\non:\n  pull_request:\n    types:\n      - opened\n      - reopened\n  push:\n    branches:\n      - '**'\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        node-version: [20]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Run Lint\n        run: pnpm run lint\n"
  },
  {
    "path": ".github/workflows/pkg.pr.new.yml",
    "content": "name: Publish Any Commit\non:\n  push:\n    branches:\n      - '**'\n    tags:\n      - '!**'\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\npermissions: {}\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number }}\n  cancel-in-progress: true\njobs:\n  build:\n    # Skip job if PR is from Dependabot\n    if: github.actor != 'dependabot[bot]'\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [20]\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - run: npm i -g --force corepack && corepack enable\n      - uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Build\n        run: pnpm build\n      - run: pnpx pkg-pr-new publish --compact --pnpm\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release CI\n\non:\n  push:\n    branches: [main, next, beta]\n\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\n\njobs:\n  publish:\n    name: Publish to npm\n    runs-on: ubuntu-latest\n    environment: production\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Build lib\n        run: pnpm build\n      - name: Semantic Release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n        run: npx semantic-release@24.2.0\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Run Tests\non:\n  pull_request:\n    types:\n      - opened\n      - reopened\n    paths:\n      - '!README.md'\n      - '!LICENSE'\n      - '!changelog.md'\n      - '!scripts/**'\n      - '!.vscode/**'\n      - '!.github/**'\n  push:\n    branches:\n      - '**'\n\nenv:\n  PNPM_CACHE_FOLDER: .pnpm-store\n  SKIP_INSTALL_SIMPLE_GIT_HOOKS: 1 # Skip installing simple-git-hooks\n\njobs:\n  test:\n    name: Tests\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        node-version: [20]\n    environment: test\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: pnpm\n      - name: Install dependencies\n        run: pnpm install\n      - name: Run Unit tests\n        run: pnpm run test:unit:ci\n      - name: Run E2E tests\n        env:\n          VITE_ACCESS_TOKEN: ${{ secrets.VITE_ACCESS_TOKEN }}\n          VITE_OAUTH_TOKEN: ${{ secrets.VITE_OAUTH_TOKEN }}\n          VITE_SPACE_ID: ${{ vars.VITE_SPACE_ID }}\n        run: pnpm run build && pnpm run test:e2e\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ncoverage\ntest.js\ntest.ts\ntest-manager.js\ndist/\nexample-dist/\n*.log\n.DS_Store\n.nuxt\n.idea\ngitcommit.fish\n\n.env.test\n.env\n.next\n\nold-tests/"
  },
  {
    "path": ".npmrc",
    "content": "registry=https://registry.npmjs.org/\npublic-hoist-pattern[]=@commitlint*\npublic-hoist-pattern[]=commitlint\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Debug Vitest Tests\",\n      \"program\": \"${workspaceFolder}/node_modules/vitest/vitest.mjs\",\n      \"args\": [\"run\"],\n      \"autoAttachChildProcesses\": true,\n      \"smartStep\": true,\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\",\n      \"skipFiles\": [\"<node_internals>/**\"]\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Debug Vitest E2E Tests\",\n      \"program\": \"${workspaceFolder}/node_modules/vitest/vitest.mjs\",\n      \"args\": [\"run\", \"-c\", \"vitest.config.e2e.ts\"],\n      \"autoAttachChildProcesses\": true,\n      \"smartStep\": true,\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\",\n      \"skipFiles\": [\"<node_internals>/**\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Storyblok GmbH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "> [!IMPORTANT]\n> **📦 Package Migration Notice**\n> \n> This package has been migrated to the [Storyblok monorepo](https://github.com/storyblok/monoblok). \n> \n> **⚠️ This repository has been archived and is no longer maintained. Development has moved to the monorepo.**\n> \n> **New Location**: You can now find this package at [packages/js-client](https://github.com/storyblok/monoblok/tree/main/packages/js-client)\n> \n> Please visit the monorepo for the latest updates, issues, and contributions.\n<div align=\"center\">\n    <a  href=\"https://www.storyblok.com?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client\"  align=\"center\">\n        <img  src=\"https://a.storyblok.com/f/88751/1776x360/4d075611c6/sb-js-sdk.png\"  alt=\"Storyblok Logo\">\n    </a>\n    <h1 align=\"center\">Universal JavaScript Client for Storyblok's API</h1>\n    <p align=\"center\">This client is a thin wrapper for the <a href=\"http://www.storyblok.com?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client\" target=\"_blank\">Storyblok</a> API's to use in Node.js and the browser.</p>\n</div>\n\n<p align=\"center\">\n  <a href=\"https://npmjs.com/package/storyblok-js-client\">\n    <img src=\"https://img.shields.io/npm/v/storyblok-js-client/latest.svg?style=flat-square&color=09b3af\" alt=\"Storyblok JS Client\" />\n  </a>\n  <a href=\"https://npmjs.com/package/storyblok-js-client\" rel=\"nofollow\">\n    <img src=\"https://img.shields.io/npm/dt/storyblok-js-client.svg?style=appveyor&color=09b3af\" alt=\"npm\">\n  </a>\n  <a href=\"https://discord.gg/jKrbAMz\">\n   <img src=\"https://img.shields.io/discord/700316478792138842?label=Join%20Our%20Discord%20Community&style=appveyor&logo=discord&color=09b3af\">\n   </a>\n  <a href=\"https://twitter.com/intent/follow?screen_name=storyblok\">\n    <img src=\"https://img.shields.io/badge/Follow-%40storyblok-09b3af?style=appveyor&logo=twitter\" alt=\"Follow @Storyblok\" />\n  </a><br/>\n  <a href=\"https://app.storyblok.com/#!/signup?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client\">\n    <img src=\"https://img.shields.io/badge/Try%20Storyblok-Free-09b3af?style=appveyor&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAHqADAAQAAAABAAAAHgAAAADpiRU/AAACRElEQVRIDWNgGGmAEd3D3Js3LPrP8D8WXZwSPiMjw6qvPoHhyGYwIXNAbGpbCjbzP0MYuj0YFqMroBV/wCxmIeSju64eDNzMBJUxvP/9i2Hnq5cM1devMnz984eQsQwETeRhYWHgIcJiXqC6VHlFBjUeXgav40cIWkz1oLYXFmGwFBImaDFBHyObcOzdW4aSq5eRhRiE2dgYlpuYoYSKJi8vw3GgWnyAJIs/AuPu4scPGObd/fqVQZ+PHy7+6udPOBsXgySLDfn5GRYYmaKYJcXBgWLpsx8/GPa8foWiBhuHJIsl2DkYQqWksZkDFgP5PObcKYYff//iVAOTIDlx/QPqRMb/YSYBaWlOToZIaVkGZmAZSQiQ5OPtwHwacuo4iplMQEu6tXUZMhSUGDiYmBjylFQYvv/7x9B04xqKOnQOyT5GN+Df//8M59ASXKyMHLoyDD5JPtbj42OYrm+EYgg70JfuYuIoYmLs7AwMjIzA+uY/zjAnyWJpDk6GOFnCvrn86SOwmsNtKciVFAc1ileBHFDC67lzG10Yg0+SjzF0ownsf/OaofvOLYaDQJoQIGix94ljv1gIZI8Pv38zPvj2lQWYf3HGKbpDCFp85v07NnRN1OBTPY6JdRSGxcCw2k6sZuLVMZ5AV4s1TozPnGGFKbz+/PE7IJsHmC//MDMyhXBw8e6FyRFLv3Z0/IKuFqvFyIqAzd1PwBzJw8jAGPfVx38JshwlbIygxmYY43/GQmpais0ODDHuzevLMARHBcgIAQAbOJHZW0/EyQAAAABJRU5ErkJggg==\" alt=\"Follow @Storyblok\" />\n  </a>\n</p>\n\n## Kickstart a new project\n\nAre you eager to dive into coding? **[Follow these steps to kickstart a new project with Storyblok and a JavaScript frontend framework](https://www.storyblok.com/technologies?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js)**, and get started in just a few minutes!\n\n## Installation\n\n```sh\nnpm install storyblok-js-client # yarn add storyblok-js-client\n```\n\n#### Compatibility\n\n| Version to install                                                                                                              | Support                                              |\n| ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| Latest `storyblok-js-client`                                                                                                    | Modern browsers + Node 18+                           |\n| Latest `storyblok-js-client` <br> + Fetch polyfill like [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) | Browsers and Node versions with no Fetch API support |\n| [Version 4](https://github.com/storyblok/storyblok-js-client/tree/v4.5.8) `storyblok-js-client@4`                               | Internet Explorer support                            |\n\n## How to use it\n\n### Using the Content Delivery API\n\n```javascript\n// 1. Import the Storyblok client\nimport StoryblokClient from \"storyblok-js-client\";\n\n// 2. Initialize the client with the preview token\n// from your space dashboard at https://app.storyblok.com\nconst Storyblok = new StoryblokClient({\n  accessToken: <YOUR_SPACE_ACCESS_TOKEN>,\n});\n```\n\n### Using the Management API\n\n```javascript\n// 1. Import the Storyblok client\nimport StoryblokClient from \"storyblok-js-client\";\nconst spaceId = <YOUR_SPACE_ID>;\n\n// 2. Initialize the client with the oauth token\n// from the my account area at https://app.storyblok.com\nconst Storyblok = new StoryblokClient({\n  oauthToken: <YOUR_OAUTH_TOKEN>,\n});\n\nStoryblok.post(`spaces/${spaceId}/stories`, {\n  story: { name: \"xy\", slug: \"xy\" },\n});\nStoryblok.put(`spaces/${spaceId}/stories/1`, {\n  story: { name: \"xy\", slug: \"xy\" },\n});\nStoryblok.delete(`spaces/${spaceId}/stories/1`);\n```\n\n\n## NEW BRANCHES AND VERSIONS\n\nThe old master branch containing version `4.x.y` has been moved to the `v4` branch.\nWe've renamed the `master` branch to `main` and now it contains version >= 5.0.0.\nIf you wish to continue using the non Typescript version with `axios`, please use version `4`. You can install it by running `npm install https://github.com/storyblok/storyblok-js-client.git#4.x.x`.\n\n### BREAKING CHANGES - FROM VERSION 6\n\nError handling from fetch has changed. Exceptions will be thrown as an object with the following structure:\n\n```javascript\n{\n  message: string\n  status: number\n  response: ISbResponse\n}\n```\n\nYou don't need to parse the error from the client's side.\n\n### BREAKING CHANGES - FROM VERSION 5\n\n#### Added TypeScript - Version 5\n\nWe added TypeScript to our codebase, improving our code quality and assuring the correct implementation from the client's side. This change will probably break your code, because your Storyblok client's current implementation is possibly sending the wrong types to the source.\nIf you use an IDE to code, you'll be able to hover the problematic cause and see what is being expected from the type. Yet, you can keep using our version without TypeScript.\n\n#### Axios removal - Version 5\n\nWe removed our dependency on axios in Version `5`. If you want to continue using our SDK with axios, please use version `4`.\nThe proxy feature was also removed in this version.\n\n#### Fetch (use polyfill if needed) - Version 5\n\nVersion 5 is using native `fetch` API, supported by modern browsers and Node >= 18. If you are using an environment with no `fetch` API support, you can use a polyfill like [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) at the very beginning of your app entry point:\n\n```js\nimport 'isomorphic-fetch'\nrequire('isomorphic-fetch') // in CJS environments\n```\n\n## Documentation\n\n#### Assets structure compatibility\n\nWe added retro-compatibility when using `resolve_assets: 1` parameter under V2. Now, if you are using our V2 client, you should receive the assets structure just the same as V1.\n\n### Class `Storyblok`\n\n**Parameters**\n\n- `config` Object\n  - (`accessToken` String, optional - The preview token you can find in your space dashboard at https://app.storyblok.com. This is mandatory only if you are using the CDN API.)\n  - (`oauthToken` String, optional - The personal access token you can find in your account at https://app.storyblok.com/#/me/account?tab=token. This is mandatory only if you are using the Management API.)\n  - (`cache` Object, optional)\n    - (`type` String, optional - `none` or `memory`)\n  - (`responseInterceptor` Function, optional - You can pass a function and return the result. For security reasons, Storyblok client will deal only with the response interceptor.)\n  - (`region` String, optional)\n  - (`https` Boolean, optional)\n  - (`rateLimit` Integer, optional, defaults to 3 for management api and 5 for cdn api)\n  - (`timeout` Integer, optional)\n  - (`maxRetries` Integer, optional, defaults to 5)\n  - (`resolveNestedRelations` Boolean, optional - By default is true)\n- (`endpoint` String, optional)\n\n### Activating request cache\n\nThe Storyblok client comes with a caching mechanism.\nWhen initializing the Storyblok client you can define a cache provider for caching the requests in memory.\n\nThe default behavior of the cache is `clear: 'manual'`, that is, if you need to clear the cache, you need to call `Storyblok.flushCache()` or activate the automatic clear with `clear: 'auto'`, as in the example below.\n\nTo only clear the cache automatically when requests to the draft version happens you can set the config to `clear: 'onpreview'`.\n\n```javascript\nlet Storyblok = new StoryblokClient({\n  accessToken: <YOUR_SPACE_ACCESS_TOKEN>,\n  cache: {\n    clear: \"auto\",\n    type: \"memory\",\n  },\n});\n```\n\n### Passing response interceptor\n\nThe Storyblok client lets you pass a function that serves as a response interceptor to it.\nUsage:\n\n```javascript\nlet Storyblok = new StoryblokClient({\n  accessToken: <YOUR_SPACE_ACCESS_TOKEN>,\n  cache: {\n    clear: \"auto\",\n    type: \"memory\",\n  },\n  responseInterceptor: (response) => {\n    // one can handle status codes and more with the response\n    if (response.status === 200) {\n      // handle your status here\n    }\n    // ALWAYS return the response\n    return response;\n  },\n});\n```\n\n### Removing response interceptor\n\nOne can remove the reponseInterceptor at any time, by calling the function `ejectInterceptor` as shown below:\n\n```javascript\nStoryblok.ejectInterceptor()\n```\n\n### Error handling\n\nExceptions will be thrown as an object with the following structure:\n\n```javascript\n{\n  message: Error // an Error object with the error message\n  status: number\n  response: ISbResponse\n}\n```\n\nwhere,\n\n```typescript\ninterface ISbResponse {\n  data: any\n  status: number\n  statusText: string\n  headers: any\n  config: any\n  request: any\n}\n```\n\nOne should catch the exception and handle it accordingly.\n\n### Resolve relations using the Storyblok Bridge\n\nWith this parameter, you can resolve relations with live updates in the Storyblok JavaScript Bridge input event. It is possible to resolve content entries that are two levels deep, such as `resolve_relations=page.author,page.products`. Resolved relations can be found in the root of the API response, in the property `rels`. You can learn more about `resolve_relations` in [this tutorial](https://www.storyblok.com/tp/using-relationship-resolving-to-include-other-content-entries)\n\n> It is important to note that when using the `storyblok-js-client` and other framework-specific SDKs, you don't need to look for the `rels` array after resolving relations. The resolved relations are injected into the properties and, hence, are directly accessible through the properties. For example, you can access the authors array directly with `page.author` once it is resolved.\n\n```javascript\nwindow.storyblok.resolveRelations(\n  storyObject,\n  relationsToResolve,\n  callbackWhenResolved\n)\n```\n\n**Example**\n\n```javascript\nwindow.storyblok.on('input', (event) => {\n  window.storyblok.addComments(event.story.content, event.story.id)\n  window.storyblok.resolveRelations(\n    event.story,\n    ['post.author', 'post.categories'],\n    () => {}\n  )\n})\n```\n\n### Custom Fetch parameter\n\nYou can now pass an aditional paramater to the following calls: `get`, `getAll`, `post`, `put`, `delete`, `getStory` and `getStories`. This parameter is optional and it is the same as the Fetch API [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) parameter.\n**_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nconst data = {\n  story: {\n    name: 'xy',\n    slug: 'xy',\n  },\n}\n\nStoryblok.get(\n  'cdn/stories/home',\n  {\n    version: 'draft',\n  },\n  {\n    mode: 'cors',\n    cache: 'no-cache',\n    body: JSON.stringify(data),\n  }\n)\n  .then((response) => {\n    console.log(response)\n  })\n  .catch((error) => {\n    console.error(error)\n  })\n```\n\n### Method `Storyblok#get`\n\nWith this method you can get single or multiple items. The multiple items are paginated and you will receive 25 items per page by default. If you want to get all items at once use the `getAll` method.\n\n**Parameters**\n\n- `[return]` Promise, Object `response`\n- `slug` String, _required_. Path (can be `cdn/stories`, `cdn/tags`, `cdn/datasources`, `cdn/links`)\n- `params` Object, _optional_. Options can be found in the [API documentation](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\n- `fetchOptions` Object, _optional_, Fetch options can be found in the [Fetch API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). **_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nStoryblok.get('cdn/stories/home', {\n  version: 'draft',\n})\n  .then((response) => {\n    console.log(response)\n  })\n  .catch((error) => {\n    console.log(error)\n  })\n```\n\n#### Method `Storyblok#getAll`\n\nWith this method you can get all items at once.\n\n**Parameters**\n\n- `[return]` Promise, Array of entities\n- `slug` String, _required_. Path (can be `cdn/stories`, `cdn/tags`, `cdn/datasources`, `cdn/links`)\n- `params` Object, _required_. Options can be found in the [API documentation](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\n- `entity` String, _optional_. Storyblok entity like stories, links or datasource. It's optional.\n- `fetchOptions` Object, _optional_, Fetch options can be found in the [Fetch API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). **_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nStoryblok.getAll('cdn/stories', {\n  version: 'draft',\n})\n  .then((stories) => {\n    console.log(stories) // an array\n  })\n  .catch((error) => {\n    console.log(error)\n  })\n```\n\n#### Method `Storyblok#post` (only management api)\n\n**Parameters**\n\n- `[return]` Promise, Object `response`\n- `slug` String, _required_. Path (can be `cdn/stories`, `cdn/tags`, `cdn/datasources`, `cdn/links`)\n- `params` Object, _required_. Options can be found in the [API documentation](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\n- `fetchOptions` Object, _optional_, Fetch options can be found in the [Fetch API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). **_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nStoryblok.post('spaces/<YOUR_SPACE_ID>/stories', {\n  story: { name: 'xy', slug: 'xy' },\n})\n  .then((response) => {\n    console.log(response)\n  })\n  .catch((error) => {\n    console.log(error)\n  })\n```\n\n#### Method `Storyblok#put` (only management api)\n\n**Parameters**\n\n- `[return]` Promise, Object `response`\n- `slug` String, _required_. Path (can be `cdn/stories`, `cdn/tags`, `cdn/datasources`, `cdn/links`)\n- `params` Object, _required_. Options can be found in the [API documentation](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\n- `fetchOptions` Object, _optional_, Fetch options can be found in the [Fetch API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). **_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nStoryblok.put('spaces/<YOUR_SPACE_ID>/stories/1', {\n  story: { name: 'xy', slug: 'xy' },\n})\n  .then((response) => {\n    console.log(response)\n  })\n  .catch((error) => {\n    console.log(error)\n  })\n```\n\n#### Method `Storyblok#delete` (only management api)\n\n**Parameters**\n\n- `[return]` Promise, Object `response`\n- `slug` String, _required_. Path (can be `cdn/stories`, `cdn/tags`, `cdn/datasources`, `cdn/links`)\n- `params` Object, _required_. Options can be found in the [API documentation](https://www.storyblok.com/docs/api/content-delivery/v2?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\n- `fetchOptions` Object, _optional_, Fetch options can be found in the [Fetch API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). **_It's important to note that we extended the `RequestInit` interface omitting the `method` parameter. This is because the method is already defined by the Storyblok client._**\n\n**Example**\n\n```javascript\nStoryblok.delete('spaces/<YOUR_SPACE_ID>/stories/1')\n  .then((response) => {\n    console.log(response)\n  })\n  .catch((error) => {\n    console.log(error)\n  })\n```\n\n#### Method `Storyblok#flushCache`\n\n**Parameters**\n\n- `[return]` Promise, Object returns the Storyblok client\n\n**Example**\n\n```javascript\nStoryblok.flushCache()\n```\n\n\n## Code examples\n\n### Define a custom cache for fine-grained control caching\n\nSometimes you want a custom cache implemention, for instance, when you want to host it on Redis for a distributed cache.\n\nIn such cases, you can use the `custom` cache and redefine the methods:\n\n```js\nnew StoryblokClient({\n  accessToken: <YOUR_SPACE_ACCESS_TOKEN>,\n  cache: {\n    clear: \"manual\",\n    type: \"custom\",\n    custom: {\n      get () {\n        // example: get here cache from Redis\n        return Promise.resolve(0);\n      },\n      getAll () {\n        return Promise.resolve(0);\n      },\n      set () {\n        return Promise.resolve(0);\n      },\n      flush () {\n        return Promise.resolve(0);\n      },\n    }\n  }\n}\n```\n\n### Filter by content type values and path\n\n```javascript\nimport StoryblokClient from 'storyblok-js-client'\n\nlet client = new StoryblokClient({\n  accessToken: '<YOUR_SPACE_ACCESS_TOKEN>',\n})\n\n// Filter by boolean value in content type\nclient\n  .get('cdn/stories', {\n    version: 'draft',\n    filter_query: {\n      is_featured: {\n        in: true,\n      },\n    },\n  })\n  .then((res) => {\n    console.log(res.data.stories)\n  })\n\n// Get all news and author contents\nclient\n  .get('cdn/stories', {\n    version: 'draft',\n    filter_query: {\n      component: {\n        in: 'news,author',\n      },\n    },\n  })\n  .then((res) => {\n    console.log(res.data.stories)\n  })\n\n// Get all content from the news folder\nclient\n  .get('cdn/stories', {\n    version: 'draft',\n    starts_with: 'news/',\n  })\n  .then((res) => {\n    console.log(res.data.stories)\n  })\n```\n\n### Download all content from Storyblok\n\nFollowing a code example using the storyblok-js-client to back up all content on your local filesystem inside a 'backup' folder.\n\n```javascript\nimport StoryblokClient from 'storyblok-js-client'\nimport fs from 'fs'\n\nlet client = new StoryblokClient({\n  accessToken: '<YOUR_SPACE_ACCESS_TOKEN>',\n})\n\nlet lastPage = 1\nlet getStories = (page) => {\n  client\n    .get('cdn/stories', {\n      version: 'draft',\n      per_page: 25,\n      page: page,\n    })\n    .then((res) => {\n      let stories = res.data.stories\n      stories.forEach((story) => {\n        fs.writeFile(\n          './backup/' + story.id + '.json',\n          JSON.stringify(story),\n          (err) => {\n            if (err) throw err\n\n            console.log(story.full_slug + ' backed up')\n          }\n        )\n      })\n\n      let total = res.total\n      lastPage = Math.ceil(res.total / res.perPage)\n\n      if (page <= lastPage) {\n        page++\n        getStories(page)\n      }\n    })\n}\n\ngetStories(1)\n```\n\n\n### Handling access token overwrite\n\nYou can overwrite an access token, and prevent errors from the function call by adding a `.catch()` method for each access token as shown below.\n\n```javascript\nconst public = 'token1'\nconst preview = 'token2'\n```\n\nYou can pass the tokens as follows:\n\n```javascript\nclient.getStories({token: 'preview'...}).then(previewResponse => ... ).catch()\nclient.getStories({token: 'public'...}).then(publicResponse => ... ).catch()\n```\n\n## Further Resources\n\n- [Quick Start](https://www.storyblok.com/technologies?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client)\n- [API Documentation](https://www.storyblok.com/docs/api?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client)\n- [Developer Tutorials](https://www.storyblok.com/tutorials?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client)\n- [Developer Guides](https://www.storyblok.com/docs/guide/introduction?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client)\n- [FAQs](https://www.storyblok.com/faqs?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client)\n\n## Support\n\n- Bugs or Feature Requests? [Submit an issue](../../../issues/new);\n\n- Do you have questions about Storyblok or you need help? [Join our Discord Community](https://discord.gg/jKrbAMz).\n\n## Contributing\n\nPlease see our [contributing guidelines](https://github.com/storyblok/.github/blob/master/contributing.md) and our [code of conduct](https://www.storyblok.com/trust-center#code-of-conduct?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js-client).\nThis project use [semantic-release](https://semantic-release.gitbook.io/semantic-release/) for generate new versions by using commit messages and we use the Angular Convention to naming the commits. Check [this question](https://semantic-release.gitbook.io/semantic-release/support/faq#how-can-i-change-the-type-of-commits-that-trigger-a-release) about it in semantic-release FAQ.\n"
  },
  {
    "path": "changelog.md",
    "content": "# Change Log (deprecated)\n\nThis file is **no longer maintained**. To learn about changes made in specific versions of this library, check out its [GitHub releases](https://github.com/storyblok/storyblok-js-client/releases).\n\n## [6.6.4] - 2024-01-26\n\n### Added\n\n- Extending ISbStoriesParams interface with ISbMultipleStoriesData types.\n\n## [6.5.0] - 2024-01-11\n\n### Added\n\n- New custom fetch function added.\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/releases/tag/v6.5.0)\n\n## [6.0.0] - 2023-08-15\n\n### Changed\n\n- Error handling changed to expose the error object instead of the error message as a string.\n\n## [5.4.0] - 2023-02-02\n\n### Added\n\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/releases/tag/v5.4.0)\n- Custom cache provider\n\n## [5.3.4] - 2023-01-24\n\n### Fixed\n\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/releases/tag/v5.3.4)\n- Error handling is return the correct reject/resolve to the client\n\n## [5.2.1] - 2022-12-20\n\n### Fixed\n\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/releases/tag/v5.2.1)\n- Added content type header to fix a bug from the api calls.\n\n## [5.2.0] - 2022-12-19\n\n### Added\n\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/releases/tag/v5.2.0)\n- Added optional fetch function to constructor\n\n## [5.1.0] - 2022-11-24\n\n### Changed\n\n- Update browsers compatibility\n- Remove isomorphic fetch from dependencies\n- Added a simple playground for manual testing\n- Build setup improvements\n- Added svelte + ts and Nuxt 3 playgrounds\n\n## [5.0.4] - 2022-10-28\n\n### Fixed\n\n- Ci: run prettier in CI\n- Merge conflicting ESLint + Prettier configuration\n\n## [5.0.3] - 2022-10-26\n\n### Fixed\n\n- Remove & ignore stray .DS_Store file\n\n## [5.0.2] - 2022-10-21\n\n### Fixed\n\n- Added the correct function return to get, getAll, set and flush functions\n\n## [5.0.1] - 2022-10-21\n\n### Added\n\n- Added dimensions related features to ISbStoriesParams interface.\n\n## [5.0.0] - 2022-10-17 - BREAKING CHANGE\n\n### Added\n\n- [Storyblok-JS-client](https://github.com/storyblok/storyblok-js-client/compare/v4.5.6...v5.0.0)\n- BREAKING CHANGE\n- Added Typescript to codebase\n\n### Changed\n\n- BREAKING CHANGE\n- Removed Axios as dependency\n- All JS codebase was refactored to Typescript\n\n### Fixed\n\n- Fixing application to match unit tests\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { storyblokLintConfig } from '@storyblok/eslint-config';\n\nexport default storyblokLintConfig({\n  rules: {\n    // @TODO: remove all of them after fixing and proper testing in v7\n    '@typescript-eslint/no-this-alias': 'off',\n    'ts/no-this-alias': 'off',\n    'no-async-promise-executor': 'off',\n  },\n  ignores: ['**/node_modules/**', 'playground', 'README.md'],\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"storyblok-js-client\",\n  \"version\": \"7.0.0\",\n  \"packageManager\": \"pnpm@10.11.0\",\n  \"description\": \"Universal JavaScript SDK for Storyblok's API\",\n  \"author\": \"Alexander Feiglstorfer <delooks@gmail.com>\",\n  \"license\": \"MIT\",\n  \"homepage\": \"https://github.com/storyblok/storyblok-js-client#readme\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/storyblok/storyblok-js-client.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/storyblok/storyblok-js-client/issues\"\n  },\n  \"keywords\": [\n    \"storyblok\",\n    \"javascript\",\n    \"api\"\n  ],\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/types/entry.esm.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.umd.js\"\n    }\n  },\n  \"main\": \"./dist/index.umd.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"unpkg\": \"./dist/index.umd.js\",\n  \"jsdelivr\": \"./dist/index.umd.js\",\n  \"types\": \"./dist/types/entry.esm.d.ts\",\n  \"source\": \"src/index.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"tests\"\n  ],\n  \"scripts\": {\n    \"dev\": \"vite build --watch\",\n    \"build\": \"node vite.build.mjs\",\n    \"test\": \"pnpm run test:unit:ci && pnpm run test:e2e\",\n    \"test:unit\": \"vitest\",\n    \"test:unit:ci\": \"vitest run\",\n    \"test:unit:ui\": \"vitest --ui\",\n    \"test:e2e\": \"vitest run -c vitest.config.e2e.ts\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"playground\": \"pnpm run --filter ./playground/vanilla dev\",\n    \"playground:svelte\": \"pnpm run --filter ./playground/svelte dev\",\n    \"playground:next\": \"pnpm run --filter ./playground/nextjs dev\",\n    \"dev:umd\": \"npx serve ./\",\n    \"coverage\": \"vitest run --coverage\",\n    \"prepare\": \"pnpm simple-git-hooks\",\n    \"prepublishOnly\": \"pnpm build\",\n    \"check-licenses\": \"node scripts/license-checker.mjs\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^19.7.1\",\n    \"@commitlint/config-conventional\": \"^19.7.1\",\n    \"@storyblok/eslint-config\": \"^0.3.0\",\n    \"@tsconfig/recommended\": \"^1.0.8\",\n    \"@vitest/coverage-v8\": \"^3.0.5\",\n    \"@vitest/ui\": \"^3.0.5\",\n    \"eslint\": \"^9.19.0\",\n    \"kolorist\": \"^1.8.0\",\n    \"license-checker\": \"^25.0.1\",\n    \"simple-git-hooks\": \"^2.11.1\",\n    \"typescript\": \"^5.7.3\",\n    \"vite\": \"^5.4.15\",\n    \"vite-plugin-banner\": \"^0.8.0\",\n    \"vite-plugin-dts\": \"^4.5.0\",\n    \"vitest\": \"^3.0.5\"\n  },\n  \"release\": {\n    \"branches\": [\n      \"main\",\n      {\n        \"name\": \"next\",\n        \"prerelease\": true\n      },\n      {\n        \"name\": \"beta\",\n        \"prerelease\": true\n      }\n    ]\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ],\n    \"rules\": {\n      \"body-max-line-length\": [\n        2,\n        \"never\",\n        200\n      ],\n      \"footer-max-line-length\": [\n        2,\n        \"never\",\n        200\n      ]\n    }\n  },\n  \"simple-git-hooks\": {\n    \"pre-commit\": \"pnpm lint\",\n    \"pre-push\": \"pnpm commitlint --last --verbose\"\n  }\n}\n"
  },
  {
    "path": "playground/nextjs/app/layout.tsx",
    "content": "export default function RootLayout({ children }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "playground/nextjs/app/page.tsx",
    "content": "import StoryblokClient from 'storyblok-js-client'\n\nexport default async function Home() {\n  const { data } = await fetchData()\n\n  return (\n    <div>\n      <h1>Story: {data.story.content.headline}</h1>\n    </div>\n  )\n}\n\nexport async function fetchData() {\n  const storyblokApi = new StoryblokClient({\n    accessToken: 'OurklwV5XsDJTIE1NJaD2wtt',\n  })\n\n  const res = await storyblokApi.get(\n    `cdn/stories/home`,\n    { version: 'draft' },\n    {\n      // cache: 'no-store',\n      next: {\n        revalidate: 3600,\n      },\n    }\n  )\n  const { date, etag } = res.headers as any\n\n  console.log(date, etag)\n\n  return res\n}\n\n/**\n * 1. When should we use `cache: no-store`?\n *  - Edit environments (preview, staging) always `no-store`\n *  - Prod environments -> can they revalidate the cache? either by time, or by new version of the page\n *\n * 2. How to revalidate the Next.js cache?\n *  - By time? YES\n *  - By version? YES - by generated etag\n *\n * 3. How to revalidate Next.js on demand WHEN a storyblok story has changed?\n *  - Webhooks\n *\n *\n * NEXT STEPS\n *  - Release this\n *     - Publish announcement of custom Fetch options (Discord, socials, etc)\n *     - Review conversations, GH issues, tickets, etc\n *     - (Alex) - give the go to Thiago, msg on SDK channels\n *     - (Chakit) - announcement and converstations\n *  - Knowledge share: Facundo (Alex record video)\n *  - What docs do we need? Check Manuel\n *     - Give per-env recommendations on last part UT tutorial\n */\n"
  },
  {
    "path": "playground/nextjs/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "playground/nextjs/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  /* config options here */\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "playground/nextjs/package.json",
    "content": "{\n  \"name\": \"next13-live-editing\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"18.2.47\",\n    \"eslint\": \"8.55.0\",\n    \"eslint-config-next\": \"14.0.4\",\n    \"next\": \"^13.5.7\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"storyblok-js-client\": \"file:..\",\n    \"swr\": \"^2.2.5\"\n  }\n}\n"
  },
  {
    "path": "playground/nextjs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "playground/svelte/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Vite App</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "playground/svelte/package.json",
    "content": "{\n  \"name\": \"@storyblok/playground\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"vite\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/vite-plugin-svelte\": \"^3.1.2\",\n    \"@tsconfig/svelte\": \"^5.0.4\",\n    \"pathe\": \"^1.1.2\",\n    \"svelte\": \"^4.2.19\",\n    \"vite\": \"^5.4.11\",\n    \"vite-plugin-qrcode\": \"^0.2.3\"\n  },\n  \"dependencies\": {\n    \"storyblok-js-client\": \"file:..\"\n  }\n}\n"
  },
  {
    "path": "playground/svelte/src/App.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount } from \"svelte\";\n  import StoryblokClient from \"storyblok-js-client\";\n\n  let story = null;\n  let client = new StoryblokClient({ accessToken: \"OurklwV5XsDJTIE1NJaD2wtt\"})\n\n  onMount(async () => {\n    const { data } = await client.get(\"cdn/stories/svelte\", {\n      version: \"draft\",\n    });\n    story = data.story;\n  });\n</script>\n\n<div>\n  <pre><code>{ JSON.stringify(story, null, 2) }</code></pre>\n</div>\n\n<style>\n  :root {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen,\n      Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\n  }\n</style>\n"
  },
  {
    "path": "playground/svelte/src/main.ts",
    "content": "import App from './App.svelte'\n\nconst app = new App({\n  target: document.getElementById('app'),\n})\n\nexport default app\n"
  },
  {
    "path": "playground/svelte/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"isolatedModules\": true,\n    \"target\": \"ESNext\",\n    \"strict\": false,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"declaration\": true,\n    \"declarationDir\": \"dist\",\n    \"emitDeclarationOnly\": true\n  },\n  \"extends\": \"@tsconfig/svelte/tsconfig.json\",\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Svelte\",\n  \"include\": [\"./*.svelte\", \"./**/*.ts\"],\n  \"exclude\": [\"node_modules/*\"]\n}\n"
  },
  {
    "path": "playground/svelte/vite.config.mts",
    "content": "import { defineConfig } from 'vite'\nimport { svelte } from '@sveltejs/vite-plugin-svelte'\nimport { qrcode } from 'vite-plugin-qrcode'\nimport { resolve } from 'pathe'\n\nexport default defineConfig({\n  plugins: [\n    svelte(),\n    qrcode(), // only applies in dev mode\n  ],\n  resolve: {\n    alias: {\n      'storyblok-js-client': resolve(__dirname, '../../src/index.ts'),\n    },\n  },\n})\n"
  },
  {
    "path": "playground/vanilla/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\nexample-dist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "playground/vanilla/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Storyblok-js-client - Vanilla</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "playground/vanilla/package.json",
    "content": "{\n  \"name\": \"vanilla\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/vite\": \"^4.1.4\",\n    \"pathe\": \"^1.1.2\",\n    \"tailwindcss\": \"^4.1.4\",\n    \"typescript\": \"^5.7.2\",\n    \"vite\": \"^5.4.11\",\n    \"vite-plugin-qrcode\": \"^0.2.3\"\n  },\n  \"dependencies\": {\n    \"storyblok-js-client\": \"workspace:^\"\n  }\n}\n"
  },
  {
    "path": "playground/vanilla/src/main.ts",
    "content": "import StoryblokClient from 'storyblok-js-client'\nimport './style.css'\n\nconst capi = new StoryblokClient({\n  accessToken: import.meta.env.VITE_ACCESS_TOKEN as string,\n  version: 'draft',\n  inlineAssets: true,\n})\n\nconst mapi = new StoryblokClient({\n  oauthToken: import.meta.env.VITE_OAUTH_TOKEN as string,\n  region: 'eu',\n})\n\n\n// Function to check if tokens are available\nconst checkTokens = () => {\n  const accessToken = import.meta.env.VITE_ACCESS_TOKEN as string\n  const oauthToken = import.meta.env.VITE_OAUTH_TOKEN as string\n  \n  const missingTokens = []\n  \n  if (!accessToken) {\n    missingTokens.push('VITE_ACCESS_TOKEN')\n  }\n  \n  if (!oauthToken) {\n    missingTokens.push('VITE_OAUTH_TOKEN')\n  }\n  \n  return missingTokens\n}\n\n// Function to display results in the UI\nconst displayResult = (result: any) => {\n  document.querySelector<HTMLDivElement>('#result')!.innerHTML = `\n    <pre class=\"p-4 m-0 whitespace-pre-wrap\">\n      <code class=\"font-mono text-sm\">\n        ${JSON.stringify(result, null, 2)}\n      </code>\n    </pre>\n  `\n}\n\n// Function to handle errors\nconst handleError = (error: any) => {\n  console.error(error)\n  document.querySelector<HTMLDivElement>('#result')!.innerHTML = `\n    <pre class=\"p-4 m-0 whitespace-pre-wrap bg-red-100 text-red-600\">\n      <code class=\"font-mono text-sm\">\n        ${JSON.stringify(error, null, 2)}\n      </code>\n    </pre>\n  `\n}\n\n// API call functions\n/**\n * Fetches a specific story with draft version\n * @returns Promise with the story data\n */\nconst getStories = async () => {\n  /* return await capi.get('cdn/stories/', {\n    version: 'draft',\n    resolve_relations: 'root.author',\n  }) */\n  return await capi.getStories()\n}\n\n/**\n * Fetches all links with published version\n * @returns Promise with the links data\n */\nconst getLinks = async () => {\n  return await capi.getAll('cdn/links')\n}\n\n/**\n * Creates a new story using the management API\n * @returns Promise with the created story data\n */\nconst createComponent = async () => {\n  return await mapi.post('spaces/295017/components', {\n    component: {\n      name: 'js-client-mapi-post-test',\n      slug: 'js-client-mapi-post-test',\n    },\n  })\n}\n\n// Check for missing tokens\nconst missingTokens = checkTokens()\nconst tokenWarning = missingTokens.length > 0 \n  ? `<div class=\"bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6 rounded\">\n      <p class=\"font-bold\">Warning: Missing API Tokens</p>\n      <p>The following environment variables are missing: ${missingTokens.join(', ')}</p>\n      <p>Please add them to your .env file to use all features.</p>\n    </div>`\n  : ''\n\n// Create UI with buttons for different API calls\ndocument.querySelector<HTMLDivElement>('#app')!.innerHTML = `\n  <div class=\"max-w-3xl mx-auto p-8\">\n    <h1 class=\"text-3xl font-bold text-center text-purple-500 mb-8\">Storyblok Client Playground</h1>\n    ${tokenWarning}\n    <div class=\"flex gap-4 justify-center mb-8\">\n      <button id=\"get-stories\" class=\"!bg-purple-500 hover:!bg-purple-600 text-white font-semibold py-3 px-6 rounded\">Get Stories</button>\n      <button id=\"get-links\" class=\"!bg-purple-500 hover:!bg-purple-600 text-white font-semibold py-3 px-6 rounded\">Get Links</button>\n      <button id=\"post\" class=\"!bg-purple-500 hover:!bg-purple-600 text-white font-semibold py-3 px-6 rounded\">Create Component</button>\n    </div>\n    <div id=\"result\" class=\"bg-gray-100 dark:bg-gray-800 rounded-lg overflow-auto max-h-[500px]\">\n      <p class=\"p-4\">Results will appear here...</p>\n    </div>\n  </div>\n`\n\n// Add event listeners to buttons\ndocument.getElementById('get-stories')?.addEventListener('click', async () => {\n  try {\n    const result = await getStories()\n    displayResult(result)\n  } catch (error) {\n    handleError(error)\n  }\n})\n\ndocument.getElementById('get-links')?.addEventListener('click', async () => {\n  try {\n    const links = await getLinks()\n    displayResult(links)\n  } catch (error) {\n    handleError(error)\n  }\n})\n\ndocument.getElementById('post')?.addEventListener('click', async () => {\n  try {\n    const result = await createComponent()\n    displayResult(result)\n  } catch (error) {\n    handleError(error)\n  }\n})\n\n\n"
  },
  {
    "path": "playground/vanilla/src/style.css",
    "content": "/* You can add global styles to this file, and also import other style files */\n@import \"tailwindcss\";\n\n:root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\n#app {\n  max-width: 1280px;\n  margin: 0 auto;\n  padding: 2rem;\n}\n\n.logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.vanilla:hover {\n  filter: drop-shadow(0 0 2em #3178c6aa);\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n\nbutton {\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 0.6em 1.2em;\n  font-size: 1em;\n  font-weight: 500;\n  font-family: inherit;\n  background-color: #1a1a1a;\n  cursor: pointer;\n  transition: border-color 0.25s;\n}\nbutton:hover {\n  border-color: #646cff;\n}\nbutton:focus,\nbutton:focus-visible {\n  outline: 4px auto -webkit-focus-ring-color;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n  button {\n    background-color: #f9f9f9;\n  }\n}\n\n/* Remove custom CSS classes that are now replaced by Tailwind */\n"
  },
  {
    "path": "playground/vanilla/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "playground/vanilla/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "playground/vanilla/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport tailwindcss from '@tailwindcss/vite'\n\nimport { resolve } from 'pathe'\n\nimport { qrcode } from 'vite-plugin-qrcode'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    qrcode(), // only applies in dev mode\n    tailwindcss(),\n  ],\n  resolve: {\n    alias: {\n      'storyblok-js-client': resolve(__dirname, '../../src/index.ts'),\n    },\n  },\n})\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - playground/*\n"
  },
  {
    "path": "scripts/license-checker.mjs",
    "content": "import licenseChecker from 'license-checker';\nimport { resolve } from 'node:path';\n\n// valid excluded licenses\nconst EXCLUDED_LICENSES = [\n  'MIT',\n  'ISC',\n  'Apache-2.0',\n  'BSD-3-Clause',\n  'BSD-2-Clause',\n  'BlueOak-1.0.0',\n  'CC0-1.0',\n  '0BSD',\n  'CC-BY-4.0',\n  'MIT*',\n  'WTFPL',\n  'MIT-0',\n  'Python-2.0',\n  'Public Domain',\n  'CC-BY-3.0',\n  'BSD*',\n  'Unlicense',\n];\n\nconsole.log(\n  'Licenser-checker: Starting to check if project uses only allowed licenses',\n);\n\nlicenseChecker.init(\n  {\n    start: resolve(import.meta.dirname, '../'),\n    production: true,\n    json: true,\n    exclude: EXCLUDED_LICENSES.join(','),\n  },\n  (err, packages) => {\n    if (err) {\n      console.error(err);\n      process.exit(1);\n    }\n\n    // we have licenses for @tipatap-pro and intro.js\n    const packagesWithInvalidLicenses = Object.entries(packages);\n\n    if (packagesWithInvalidLicenses.length > 0) {\n      console.error('Invalid licenses found:');\n      console.error(packagesWithInvalidLicenses);\n      process.exit(1);\n    }\n    else {\n      console.log('All licenses are valid');\n    }\n  },\n);\n"
  },
  {
    "path": "src/constants.ts",
    "content": "const _METHOD = {\n  GET: 'get',\n  DELETE: 'delete',\n  POST: 'post',\n  PUT: 'put',\n} as const;\n\ntype ObjectValues<T> = T[keyof T];\ntype Method = ObjectValues<typeof _METHOD>;\n\nexport default Method;\n\nexport const STORYBLOK_AGENT = 'SB-Agent';\n\nexport const STORYBLOK_JS_CLIENT_AGENT = {\n  defaultAgentName: 'SB-JS-CLIENT',\n  defaultAgentVersion: 'SB-Agent-Version',\n  packageVersion: '6.0.0',\n};\n\nexport const StoryblokContentVersion = {\n  DRAFT: 'draft',\n  PUBLISHED: 'published',\n} as const;\n\nexport type StoryblokContentVersionKeys =\n  typeof StoryblokContentVersion[keyof typeof StoryblokContentVersion];\n\nexport const StoryblokContentVersionValues = Object.values(\n  StoryblokContentVersion,\n) as StoryblokContentVersionKeys[];\n"
  },
  {
    "path": "src/entry.esm.ts",
    "content": "import Client from './index';\n\n// All default and named exports, including types for ESM bundle\nexport default Client;\nexport * from './constants';\nexport * from './interfaces';\nexport { default as SbFetch } from './sbFetch';\nexport * from './utils';\n"
  },
  {
    "path": "src/entry.umd.ts",
    "content": "import Client from './index';\nimport SbFetch from './sbFetch';\nimport * as utils from './utils';\n\nconst extend = (to: Record<any, any>, _from: Record<any, any>) => {\n  for (const key in _from) {\n    to[key] = _from[key];\n  }\n};\n\nextend(Client, { SbFetch });\nextend(Client, utils);\n\n// Single default export object for UMD friendly bundle\nexport default Client;\n"
  },
  {
    "path": "src/index.test.ts",
    "content": "import StoryblokClient from '.';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { ResponseFn } from './sbFetch';\nimport SbFetch from './sbFetch';\nimport type { ISbLink, ISbStoryData } from './interfaces';\n\n// Mocking external dependencies\nvi.mock('../src/sbFetch', () => {\n  const mockGet = vi.fn().mockResolvedValue({\n    data: {\n      links: 'Test data',\n    },\n    headers: {},\n    status: 200,\n  });\n  const mockPost = vi.fn();\n  const mockSetFetchOptions = vi.fn();\n\n  // Define a mock class with baseURL property\n  class MockSbFetch {\n    private baseURL: string;\n    private timeout?: number;\n    private headers: Headers;\n    private responseInterceptor?: ResponseFn;\n    constructor(config: any) {\n      this.baseURL = config.baseURL || 'https://api.storyblok.com/v2';\n      this.responseInterceptor = config.responseInterceptor;\n    }\n\n    public get = mockGet;\n    public post = mockPost;\n    public setFetchOptions = mockSetFetchOptions;\n  }\n\n  return {\n    default: MockSbFetch,\n  };\n});\n\ndescribe('storyblokClient', () => {\n  let client;\n\n  beforeEach(() => {\n    // Setup default mocks\n    client = new StoryblokClient({\n      accessToken: 'test-token',\n      /* fetch: mockFetch, */\n    });\n  });\n\n  describe('initialization', () => {\n    it('should initialize a client instance', () => {\n      expect(client).toBeDefined();\n      expect(client).toBeInstanceOf(StoryblokClient);\n    });\n\n    it('should initialize with default values', () => {\n      expect(client.maxRetries).toBe(10);\n      expect(client.retriesDelay).toBe(300);\n      expect(client.cache).toEqual({\n        clear: 'manual',\n      });\n      expect(client.relations).toEqual({});\n      expect(client.links).toEqual({});\n\n      expect(client.resolveCounter).toBe(0);\n      expect(client.resolveNestedRelations).toBeTruthy();\n      expect(client.stringifiedStoriesCache).toEqual({});\n      expect(client.version).toBe('draft');\n    });\n\n    it('should set an accessToken', () => {\n      expect(client.accessToken).toBe('test-token');\n    });\n\n    it('should set a version', () => {\n      expect(client.version).toBe('draft');\n    });\n\n    it('should set an endpoint', () => {\n      expect(client.client.baseURL).toBe('https://api.storyblok.com/v2');\n    });\n\n    it('should set a fetch instance', () => {\n      expect(client.client).toBeInstanceOf(SbFetch);\n    });\n  });\n\n  describe('configuration via options', () => {\n    it('should set a custom endpoint', () => {\n      client = new StoryblokClient({\n        endpoint: 'https://api-custom.storyblok.com/v2',\n      });\n\n      expect(client.client.baseURL).toBe('https://api-custom.storyblok.com/v2');\n    });\n    it('https: should set the http endpoint if option is set to false', () => {\n      client = new StoryblokClient({\n        accessToken: 'test-token',\n        https: false,\n      });\n\n      expect(client.client.baseURL).toBe('http://api.storyblok.com/v2');\n    });\n    it('should set the management endpoint v1 if oauthToken is available', () => {\n      client = new StoryblokClient({\n        oauthToken: 'test-token',\n      });\n\n      expect(client.client.baseURL).toBe('https://api.storyblok.com/v1');\n    });\n    it('should set the correct region endpoint', () => {\n      client = new StoryblokClient({\n        region: 'us',\n      });\n\n      expect(client.client.baseURL).toBe('https://api-us.storyblok.com/v2');\n    });\n    it('should set maxRetries', () => {\n      client = new StoryblokClient({\n        maxRetries: 5,\n      });\n\n      expect(client.maxRetries).toBe(5);\n    });\n    // TODO: seems like implmentation is missing\n    it.skip('should desactivate resolveNestedRelations', () => {\n      client = new StoryblokClient({\n        resolveNestedRelations: false,\n      });\n\n      expect(client.resolveNestedRelations).toBeFalsy();\n    });\n\n    it('should set automatic cache clearing', () => {\n      client = new StoryblokClient({\n        cache: {\n          clear: 'auto',\n        },\n      });\n\n      expect(client.cache.clear).toBe('auto');\n    });\n\n    it('should set a responseInterceptor', async () => {\n      const responseInterceptor = (response) => {\n        return response;\n      };\n\n      client = new StoryblokClient({\n        responseInterceptor,\n      });\n      await client.getAll('cdn/links');\n      expect(client.client.responseInterceptor).toBe(responseInterceptor);\n    });\n\n    it('should set a version', () => {\n      client = new StoryblokClient({\n        version: 'published',\n      });\n\n      expect(client.version).toBe('published');\n    });\n  });\n\n  describe('cache', () => {\n    it('should return cacheVersions', async () => {\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          stories: [{ id: 1, title: 'Update' }],\n          cv: 1645521118,\n        },\n        headers: {},\n        status: 200,\n      });\n      client.throttle = mockThrottle;\n      await client.get('test', { version: 'draft', token: 'test-token' });\n\n      expect(client.cacheVersions()).toEqual({\n        'test-token': 1645521118,\n      });\n    });\n\n    it('should return cacheVersion', async () => {\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          stories: [{ id: 1, title: 'Update' }],\n          cv: 1645521118,\n        },\n        headers: {},\n        status: 200,\n      });\n      client.throttle = mockThrottle;\n      await client.get('test', { version: 'draft', token: 'test-token' });\n\n      expect(client.cacheVersion('test-token')).toBe(1645521118);\n    });\n\n    it('should set the cache version', async () => {\n      client.setCacheVersion(1645521118);\n      expect(client.cacheVersions()).toEqual({\n        'test-token': 1645521118,\n      });\n    });\n\n    it('should clear the cache', async () => {\n      // Mock the cacheProvider and its flush method\n      client.cacheProvider = vi.fn().mockReturnValue({\n        flush: vi.fn().mockResolvedValue(undefined),\n      });\n      // Mock the clearCacheVersion method\n      client.clearCacheVersion = vi.fn();\n      await client.flushCache();\n\n      expect(client.cacheProvider().flush).toHaveBeenCalled();\n      expect(client.clearCacheVersion).toHaveBeenCalled();\n    });\n\n    it('should clear the cache version', async () => {\n      client.clearCacheVersion('test-token');\n      expect(client.cacheVersion()).toEqual(0);\n    });\n  });\n\n  describe('get', () => {\n    it('should handle API errors gracefully', async () => {\n      const mockGet = vi.fn().mockRejectedValue({\n        status: 404,\n        statusText: 'Not Found',\n      });\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      await expect(client.get('cdn/stories/non-existent'))\n        .rejects\n        .toMatchObject({\n          status: 404,\n        });\n    });\n\n    it('should fetch and return a complex story object correctly', async () => {\n      const mockComplexStory = {\n        data: {\n          story: {\n            id: 123456,\n            uuid: 'story-uuid-123',\n            name: 'Complex Page',\n            slug: 'complex-page',\n            full_slug: 'folder/complex-page',\n            created_at: '2023-01-01T12:00:00.000Z',\n            published_at: '2023-01-02T12:00:00.000Z',\n            first_published_at: '2023-01-02T12:00:00.000Z',\n            content: {\n              _uid: 'content-123',\n              component: 'page',\n              title: 'Complex Page Title',\n              subtitle: 'Complex Page Subtitle',\n              intro: {\n                _uid: 'intro-123',\n                component: 'intro',\n                heading: 'Welcome to our page',\n                text: 'Some introduction text',\n              },\n              body: [\n                {\n                  _uid: 'text-block-123',\n                  component: 'text_block',\n                  text: 'First paragraph of content',\n                },\n                {\n                  _uid: 'image-block-123',\n                  component: 'image',\n                  src: 'https://example.com/image.jpg',\n                  alt: 'Example image',\n                },\n                {\n                  _uid: 'related-items-123',\n                  component: 'related_items',\n                  items: ['uuid1', 'uuid2'], // Relations that we won't resolve in this test\n                },\n              ],\n              seo: {\n                _uid: 'seo-123',\n                component: 'seo',\n                title: 'SEO Title',\n                description: 'SEO Description',\n                og_image: 'https://example.com/og-image.jpg',\n              },\n            },\n            position: 1,\n            is_startpage: false,\n            parent_id: 654321,\n            group_id: '789-group',\n            alternates: [],\n            translated_slugs: [],\n            default_full_slug: null,\n            lang: 'default',\n          },\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn().mockResolvedValue(mockComplexStory);\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/folder/complex-page');\n\n      // Verify the complete story structure is returned correctly\n      expect(result.data.story).toMatchObject({\n        id: 123456,\n        uuid: 'story-uuid-123',\n        name: 'Complex Page',\n        slug: 'complex-page',\n        full_slug: 'folder/complex-page',\n        content: expect.objectContaining({\n          _uid: 'content-123',\n          component: 'page',\n          title: 'Complex Page Title',\n          subtitle: 'Complex Page Subtitle',\n          intro: expect.objectContaining({\n            _uid: 'intro-123',\n            component: 'intro',\n          }),\n          body: expect.arrayContaining([\n            expect.objectContaining({\n              component: 'text_block',\n            }),\n            expect.objectContaining({\n              component: 'image',\n            }),\n            expect.objectContaining({\n              component: 'related_items',\n            }),\n          ]),\n        }),\n      });\n\n      // Verify specific nested properties\n      expect(result.data.story.content.seo).toEqual({\n        _uid: 'seo-123',\n        component: 'seo',\n        title: 'SEO Title',\n        description: 'SEO Description',\n        og_image: 'https://example.com/og-image.jpg',\n      });\n\n      // Verify that relations array exists but remains unresolved\n      expect(result.data.story.content.body[2].items).toEqual(['uuid1', 'uuid2']);\n\n      // Verify the API was called only once (no relation resolution)\n      expect(mockGet).toHaveBeenCalledTimes(1);\n    });\n\n    describe('cdn/links endpoint', () => {\n      it('should fetch links with dates when include_dates is set to 1', async () => {\n        const mockLinksResponse = {\n          data: {\n            links: {\n              'story-1': {\n                id: 1,\n                uuid: 'story-1-uuid',\n                slug: 'story-1',\n                name: 'Story 1',\n                is_folder: false,\n                parent_id: 0,\n                published: true,\n                position: 0,\n                // Date fields included because of include_dates: 1\n                created_at: '2024-01-01T10:00:00.000Z',\n                published_at: '2024-01-01T11:00:00.000Z',\n                updated_at: '2024-01-02T10:00:00.000Z',\n              },\n              'story-2': {\n                id: 2,\n                uuid: 'story-2-uuid',\n                slug: 'story-2',\n                name: 'Story 2',\n                is_folder: false,\n                parent_id: 0,\n                published: true,\n                position: 1,\n                created_at: '2024-01-03T10:00:00.000Z',\n                published_at: '2024-01-03T11:00:00.000Z',\n                updated_at: '2024-01-04T10:00:00.000Z',\n              },\n            },\n          },\n          headers: {},\n          status: 200,\n        };\n\n        const mockGet = vi.fn().mockResolvedValue(mockLinksResponse);\n\n        client.client = {\n          get: mockGet,\n          post: vi.fn(),\n          setFetchOptions: vi.fn(),\n          baseURL: 'https://api.storyblok.com/v2',\n        };\n\n        const response = await client.get('cdn/links', {\n          version: 'draft',\n          include_dates: 1,\n        });\n\n        // Verify the structure of the response\n        expect(response).toHaveProperty('data.links');\n\n        // Check if links are present and have the correct structure\n        expect(response.data.links['story-1']).toBeDefined();\n        expect(response.data.links['story-2']).toBeDefined();\n\n        // Verify date fields are present in the response\n        const link: ISbLink = response.data.links['story-1'];\n        expect(link).toHaveProperty('created_at');\n        expect(link).toHaveProperty('published_at');\n        expect(link).toHaveProperty('updated_at');\n\n        // Verify the date formats\n        const DATETIME_FORMAT = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n        expect(link.created_at).toMatch(DATETIME_FORMAT);\n        expect(link.published_at).toMatch(DATETIME_FORMAT);\n        expect(link.updated_at).toMatch(DATETIME_FORMAT);\n\n        // Verify the API was called with correct parameters\n        expect(mockGet).toHaveBeenCalledWith('/cdn/links', {\n          cv: 0,\n          token: 'test-token',\n          version: 'draft',\n          include_dates: 1,\n        });\n        expect(mockGet).toHaveBeenCalledTimes(1);\n      });\n\n      it('should handle links response without dates when include_dates is not set', async () => {\n        const mockResponse = {\n          data: {\n            links: {\n              'story-1': {\n                id: 1,\n                uuid: 'story-1-uuid',\n                slug: 'story-1',\n                name: 'Story 1',\n                is_folder: false,\n                parent_id: 0,\n                published: true,\n                position: 0,\n                // No date fields\n              },\n            },\n          },\n          headers: {},\n          status: 200,\n        };\n\n        const mockGet = vi.fn().mockResolvedValue(mockResponse);\n        client.client.get = mockGet;\n\n        const response = await client.get('cdn/links', { version: 'draft' });\n\n        expect(response.data.links['story-1']).not.toHaveProperty('created_at');\n        expect(response.data.links['story-1']).not.toHaveProperty('published_at');\n        expect(response.data.links['story-1']).not.toHaveProperty('updated_at');\n      });\n\n      it('should handle errors gracefully', async () => {\n        const mockGet = vi.fn().mockRejectedValue({\n          status: 404,\n        });\n        client.client.get = mockGet;\n\n        await expect(client.get('cdn/links', {\n          version: 'draft',\n        })).rejects.toMatchObject({\n          status: 404,\n        });\n      });\n    });\n  });\n\n  describe('getAll', () => {\n    it('should fetch all data from the API', async () => {\n      const mockMakeRequest = vi.fn().mockResolvedValue({\n        data: {\n          links: [\n            { id: 1, name: 'Test 1' },\n            { id: 2, name: 'Test 2' },\n          ],\n        },\n        headers: {},\n        status: 200,\n      });\n      client.makeRequest = mockMakeRequest;\n      const result = await client.getAll('links', { version: 'draft' });\n      expect(result).toEqual([\n        { id: 1, name: 'Test 1' },\n        { id: 2, name: 'Test 2' },\n      ]);\n    });\n\n    it('should resolve using entity option', async () => {\n      const mockMakeRequest = vi.fn().mockResolvedValue({\n        data: {\n          custom: [\n            { id: 1, name: 'Test 1' },\n            { id: 2, name: 'Test 2' },\n          ],\n        },\n        headers: {},\n        status: 200,\n      });\n      client.makeRequest = mockMakeRequest;\n      const result = await client.getAll(\n        'cdn/links',\n        { version: 'draft' },\n        'custom',\n      );\n      expect(result).toEqual([\n        { id: 1, name: 'Test 1' },\n        { id: 2, name: 'Test 2' },\n      ]);\n    });\n\n    it('should make a request for each page', async () => {\n      const mockMakeRequest = vi.fn().mockResolvedValue({\n        data: {\n          links: [\n            { id: 1, name: 'Test 1' },\n            { id: 2, name: 'Test 2' },\n          ],\n        },\n        total: 2,\n        status: 200,\n      });\n      client.makeRequest = mockMakeRequest;\n      await client.getAll('links', { per_page: 1 });\n      expect(mockMakeRequest).toBeCalledTimes(2);\n    });\n\n    it('should get all stories if the slug is passed with the trailing slash', async () => {\n      const mockMakeRequest = vi.fn().mockResolvedValue({\n        data: {\n          stories: [\n            { id: 1, name: 'Test Story 1' },\n            { id: 2, name: 'Test Story 2' },\n          ],\n        },\n        total: 2,\n        status: 200,\n      });\n      client.makeRequest = mockMakeRequest;\n      const result = await client.getAll('cdn/stories/', { version: 'draft' });\n      expect(result).toEqual([\n        { id: 1, name: 'Test Story 1' },\n        { id: 2, name: 'Test Story 2' },\n      ]);\n    });\n  });\n\n  describe('post', () => {\n    it('should post data to the API', async () => {\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          stories: [{ id: 1, title: 'Keep me posted' }],\n        },\n        headers: {},\n        status: 200,\n      });\n      client.throttle = mockThrottle;\n      const result = await client.post('test', { data: 'test' });\n      expect(result).toEqual({\n        data: {\n          stories: [{ id: 1, title: 'Keep me posted' }],\n        },\n        headers: {},\n        status: 200,\n      });\n    });\n  });\n\n  describe('put', () => {\n    it('should put data to the API', async () => {\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          stories: [{ id: 1, title: 'Update' }],\n        },\n        headers: {},\n        status: 200,\n      });\n      client.throttle = mockThrottle;\n      const result = await client.put('test', { data: 'test' });\n      expect(result).toEqual({\n        data: {\n          stories: [{ id: 1, title: 'Update' }],\n        },\n        headers: {},\n        status: 200,\n      });\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete data from the API', async () => {\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          stories: [{ id: 1, title: 'Delete' }],\n        },\n        headers: {},\n        status: 200,\n      });\n      client.throttle = mockThrottle;\n      const result = await client.delete('test');\n      expect(result).toEqual({\n        data: {\n          stories: [{ id: 1, title: 'Delete' }],\n        },\n        headers: {},\n        status: 200,\n      });\n    });\n  });\n\n  it('should resolve stories when response contains a story or stories', async () => {\n    const mockThrottle = vi.fn().mockResolvedValue({\n      data: { stories: [{ id: 1, title: 'Test Story' }] },\n      headers: {},\n      status: 200,\n    });\n    client.throttle = mockThrottle;\n    client.resolveStories = vi.fn().mockResolvedValue({\n      id: 1,\n      title: 'Test Story',\n    });\n\n    await client.cacheResponse('/test-url', {\n      token: 'test-token',\n      version: 'published',\n    });\n\n    expect(client.resolveStories).toHaveBeenCalled();\n    expect(client.resolveCounter).toBe(1);\n  });\n\n  it('should return access token', () => {\n    expect(client.getToken()).toBe('test-token');\n  });\n\n  describe('relation resolution', () => {\n    it('should resolve more than 50 relations correctly', async () => {\n      // Create 60 UUIDs to exceed the 50 relation limit\n      const TEST_UUIDS = Array.from({ length: 60 }, (_, i) => `test-uuid-${i}`);\n\n      // Mock story with multiple relation fields\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'root-uid',\n              component: 'page',\n              items: TEST_UUIDS.slice(0, 30), // First 30 UUIDs\n              otherItems: TEST_UUIDS.slice(30), // Next 30 UUIDs\n            },\n          },\n          // Include rel_uuids but not rels to simulate API behavior\n          rel_uuids: TEST_UUIDS,\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      // Create first chunk response (first 50 relations)\n      const mockFirstChunkResponse = {\n        data: {\n          stories: TEST_UUIDS.slice(0, 50).map(uuid => ({\n            uuid,\n            name: `Story ${uuid}`,\n            content: { component: 'test-component', _uid: uuid },\n            full_slug: `stories/${uuid}`,\n          })),\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      // Create second chunk response (remaining relations)\n      const mockSecondChunkResponse = {\n        data: {\n          stories: TEST_UUIDS.slice(50).map(uuid => ({\n            uuid,\n            name: `Story ${uuid}`,\n            content: { component: 'test-component', _uid: uuid },\n            full_slug: `stories/${uuid}`,\n          })),\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      // Setup the mock client's get method\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse))\n        .mockImplementationOnce(() => Promise.resolve(mockFirstChunkResponse))\n        .mockImplementationOnce(() => Promise.resolve(mockSecondChunkResponse));\n\n      // Replace the client's fetch instance\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: ['page.items', 'page.otherItems'],\n      });\n\n      // Ensure all relations were resolved\n      const story = result.data.story;\n      expect(story.content.items).toBeInstanceOf(Array);\n      expect(story.content.items.length).toBe(30);\n      expect(story.content.otherItems).toBeInstanceOf(Array);\n      expect(story.content.otherItems.length).toBe(30);\n\n      // Check that first and last items from each array were properly resolved\n      // First array items should be objects, not UUIDs\n      expect(typeof story.content.items[0]).toBe('object');\n      expect(story.content.items[0].uuid).toBe('test-uuid-0');\n      expect(story.content.items[0].name).toBe('Story test-uuid-0');\n      expect(story.content.items[0].content.component).toBe('test-component');\n\n      // Last item in first array\n      expect(typeof story.content.items[29]).toBe('object');\n      expect(story.content.items[29].uuid).toBe('test-uuid-29');\n\n      // First item in second array\n      expect(typeof story.content.otherItems[0]).toBe('object');\n      expect(story.content.otherItems[0].uuid).toBe('test-uuid-30');\n\n      // Last item in second array\n      expect(typeof story.content.otherItems[29]).toBe('object');\n      expect(story.content.otherItems[29].uuid).toBe('test-uuid-59');\n\n      // Ensure rel_uuids was removed after resolution\n      expect(result.data.rel_uuids).toBeUndefined();\n\n      // Verify the API was called correctly for chunking\n      expect(mockGet).toHaveBeenCalledTimes(3);\n\n      // Check the parameters in second call (first chunk)\n      const firstChunkParams = mockGet.mock.calls[1][1];\n      expect(firstChunkParams).toHaveProperty('by_uuids');\n      expect(firstChunkParams.by_uuids).toContain('test-uuid-0');\n\n      // Check the parameters in third call (second chunk)\n      const secondChunkParams = mockGet.mock.calls[2][1];\n      expect(secondChunkParams).toHaveProperty('by_uuids');\n      expect(secondChunkParams.by_uuids).toContain('test-uuid-50');\n    });\n\n    it('should resolve nested relations within content blocks', async () => {\n      const TEST_UUID = 'this-is-a-test-uuid';\n\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'parent-uid',\n              component: 'page',\n              body: [{\n                _uid: 'slider-uid',\n                component: 'event_slider',\n                spots: [{\n                  _uid: 'event-uid',\n                  component: 'event',\n                  content: {\n                    _uid: 'content-uid',\n                    component: 'event',\n                    event_type: TEST_UUID,\n                  },\n                }],\n              }],\n            },\n          },\n          rel_uuids: [TEST_UUID],\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockRelationsResponse = {\n        data: {\n          stories: [{\n            _uid: 'type-uid',\n            uuid: TEST_UUID,\n            content: {\n              name: 'Test Event Type',\n              component: 'event_type',\n            },\n          }],\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      // Setup the mock client's get method\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse))\n        .mockImplementationOnce(() => Promise.resolve(mockRelationsResponse));\n\n      // Replace the client's fetch instance\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: [\n          'event.event_type',\n          'event_slider.spots',\n        ],\n        version: 'draft',\n      });\n\n      // Verify that the UUID was replaced with the resolved object\n      const resolvedEventType = result.data.story.content.body[0].spots[0].content.event_type;\n      expect(resolvedEventType).toEqual({\n        _uid: 'type-uid',\n        uuid: TEST_UUID,\n        content: {\n          name: 'Test Event Type',\n          component: 'event_type',\n        },\n        _stopResolving: true,\n      });\n\n      // Verify that get was called two times\n      expect(mockGet).toHaveBeenCalledTimes(2);\n    });\n\n    it('should resolve an array of relations', async () => {\n      const TEST_UUIDS = ['tag-1-uuid', 'tag-2-uuid'];\n\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'root-uid',\n              component: 'post',\n              tags: TEST_UUIDS,\n            },\n          },\n          rel_uuids: TEST_UUIDS,\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockRelationsResponse = {\n        data: {\n          stories: [\n            {\n              _uid: 'tag-1-uid',\n              uuid: TEST_UUIDS[0],\n              content: {\n                name: 'Tag 1',\n                component: 'tag',\n              },\n            },\n            {\n              _uid: 'tag-2-uid',\n              uuid: TEST_UUIDS[1],\n              content: {\n                name: 'Tag 2',\n                component: 'tag',\n              },\n            },\n          ],\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse))\n        .mockImplementationOnce(() => Promise.resolve(mockRelationsResponse));\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: ['post.tags'],\n        version: 'draft',\n      });\n\n      expect(result.data.story.content.tags).toEqual([\n        {\n          _uid: 'tag-1-uid',\n          uuid: TEST_UUIDS[0],\n          content: {\n            name: 'Tag 1',\n            component: 'tag',\n          },\n          _stopResolving: true,\n        },\n        {\n          _uid: 'tag-2-uid',\n          uuid: TEST_UUIDS[1],\n          content: {\n            name: 'Tag 2',\n            component: 'tag',\n          },\n          _stopResolving: true,\n        },\n      ]);\n    });\n\n    it('should resolve multiple relation patterns simultaneously', async () => {\n      const AUTHOR_UUID = 'author-uuid';\n      const CATEGORY_UUID = 'category-uuid';\n\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'root-uid',\n              component: 'post',\n              author: AUTHOR_UUID,\n              category: CATEGORY_UUID,\n            },\n          },\n          rel_uuids: [AUTHOR_UUID, CATEGORY_UUID],\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockRelationsResponse = {\n        data: {\n          stories: [\n            {\n              _uid: 'author-uid',\n              uuid: AUTHOR_UUID,\n              content: {\n                name: 'John Doe',\n                component: 'author',\n              },\n            },\n            {\n              _uid: 'category-uid',\n              uuid: CATEGORY_UUID,\n              content: {\n                name: 'Technology',\n                component: 'category',\n              },\n            },\n          ],\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse))\n        .mockImplementationOnce(() => Promise.resolve(mockRelationsResponse));\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: ['post.author', 'post.category'],\n        version: 'draft',\n      });\n\n      expect(result.data.story.content.author).toEqual({\n        _uid: 'author-uid',\n        uuid: AUTHOR_UUID,\n        content: {\n          name: 'John Doe',\n          component: 'author',\n        },\n        _stopResolving: true,\n      });\n\n      expect(result.data.story.content.category).toEqual({\n        _uid: 'category-uid',\n        uuid: CATEGORY_UUID,\n        content: {\n          name: 'Technology',\n          component: 'category',\n        },\n        _stopResolving: true,\n      });\n    });\n\n    it('should handle content with no relations to resolve', async () => {\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'test-story-uid',\n              component: 'page',\n              title: 'Simple Page',\n              text: 'Just some text content',\n              number: 42,\n              boolean: true,\n            },\n          },\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse));\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: ['page.author'], // Even with resolve_relations, nothing should change\n        version: 'draft',\n      });\n\n      // Verify the content remains unchanged\n      expect(result.data.story.content).toEqual({\n        _uid: 'test-story-uid',\n        component: 'page',\n        title: 'Simple Page',\n        text: 'Just some text content',\n        number: 42,\n        boolean: true,\n      });\n\n      // Verify that only one API call was made (no relations to resolve)\n      expect(mockGet).toHaveBeenCalledTimes(1);\n    });\n\n    it('should handle invalid relation patterns gracefully', async () => {\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'test-uid',\n              component: 'page',\n              relation_field: 'some-uuid',\n            },\n          },\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse));\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: ['invalid.pattern'],\n        version: 'draft',\n      });\n\n      // Should not throw and return original content\n      expect(result.data.story.content.relation_field).toBe('some-uuid');\n    });\n\n    it('should handle empty resolve_relations array', async () => {\n      const mockResponse = {\n        data: {\n          story: {\n            content: {\n              _uid: 'test-uid',\n              component: 'page',\n              relation_field: 'some-uuid',\n            },\n          },\n        },\n        headers: {},\n        status: 200,\n        statusText: 'OK',\n      };\n\n      const mockGet = vi.fn()\n        .mockImplementationOnce(() => Promise.resolve(mockResponse));\n\n      client.client = {\n        get: mockGet,\n        post: vi.fn(),\n        setFetchOptions: vi.fn(),\n        baseURL: 'https://api.storyblok.com/v2',\n      };\n\n      const result = await client.get('cdn/stories/test', {\n        resolve_relations: [],\n        version: 'draft',\n      });\n\n      expect(result.data.story.content.relation_field).toBe('some-uuid');\n      expect(mockGet).toHaveBeenCalledTimes(1);\n    });\n\n    it('should pass starts_with parameter when resolving relations and links', async () => {\n      // Setup mocks\n      const TEST_UUID = 'test-uuid';\n      const STARTS_WITH = 'folder/';\n\n      // Mock the throttle function that handles API calls\n      const mockThrottle = vi.fn().mockResolvedValue({\n        data: {\n          story: { content: {} },\n          rel_uuids: [TEST_UUID],\n          link_uuids: [TEST_UUID],\n        },\n        status: 200,\n      });\n\n      client.throttle = mockThrottle;\n\n      // Mock the resolveRelations and resolveLinks methods\n      client.resolveRelations = vi.fn();\n      client.resolveLinks = vi.fn();\n\n      // Make the request with starts_with parameter\n      await client.get('cdn/stories/test', {\n        resolve_relations: 'component.field',\n        resolve_links: '1',\n        starts_with: STARTS_WITH,\n      });\n\n      // Verify params were passed correctly to relation and link resolution\n      expect(client.resolveRelations).toHaveBeenCalledWith(\n        expect.anything(),\n        expect.objectContaining({ starts_with: STARTS_WITH }),\n        expect.anything(),\n      );\n\n      expect(client.resolveLinks).toHaveBeenCalledWith(\n        expect.anything(),\n        expect.objectContaining({ starts_with: STARTS_WITH }),\n        expect.anything(),\n      );\n    });\n  });\n\n  // eslint-disable-next-line test/prefer-lowercase-title\n  describe('ISbStoryData interface implementation', () => {\n    it('should validate a complete story object structure', () => {\n      const storyData: ISbStoryData = {\n        alternates: [],\n        content: {\n          _uid: 'test-uid',\n          component: 'test',\n        },\n        created_at: '2024-01-01T00:00:00.000Z',\n        deleted_at: undefined,\n        full_slug: 'test/story',\n        group_id: 'test-group',\n        id: 1,\n        is_startpage: false,\n        lang: 'default',\n        meta_data: {},\n        name: 'Test Story',\n        parent_id: null,\n        position: 0,\n        published_at: null,\n        slug: 'test-story',\n        sort_by_date: null,\n        tag_list: [],\n        uuid: 'test-uuid',\n      };\n\n      expect(storyData).toBeDefined();\n      expect(storyData).toMatchObject({\n        alternates: expect.any(Array),\n        content: expect.objectContaining({\n          _uid: expect.any(String),\n          component: expect.any(String),\n        }),\n        created_at: expect.any(String),\n        full_slug: expect.any(String),\n        group_id: expect.any(String),\n        id: expect.any(Number),\n        lang: expect.any(String),\n        name: expect.any(String),\n        position: expect.any(Number),\n        slug: expect.any(String),\n        uuid: expect.any(String),\n      });\n    });\n\n    it('should handle optional properties correctly', () => {\n      const storyData: ISbStoryData = {\n        alternates: [],\n        content: {\n          _uid: 'test-uid',\n          component: 'test',\n        },\n        created_at: '2024-01-01T00:00:00.000Z',\n        full_slug: 'test/story',\n        group_id: 'test-group',\n        id: 1,\n        lang: 'default',\n        meta_data: {},\n        name: 'Test Story',\n        position: 0,\n        published_at: null,\n        slug: 'test-story',\n        sort_by_date: null,\n        tag_list: [],\n        uuid: 'test-uuid',\n        parent_id: null,\n        // Optional properties\n        preview_token: {\n          token: 'test-token',\n          timestamp: '2024-01-01T00:00:00.000Z',\n        },\n        localized_paths: [\n          {\n            path: '/en/test',\n            name: 'Test EN',\n            lang: 'en',\n            published: true,\n          },\n        ],\n      };\n\n      expect(storyData.preview_token).toBeDefined();\n      expect(storyData.localized_paths).toBeDefined();\n    });\n  });\n\n  describe('getStory', () => {\n    it('should handle undefined resolve_relations parameter gracefully', async () => {\n      const storySlug = 'test-story';\n      const mockStoryResponse = {\n        data: {\n          story: {\n            id: 123,\n            uuid: 'test-uuid',\n            name: 'Test Story',\n            content: {\n              _uid: 'test-uid',\n              component: 'test',\n              title: 'Test Title',\n            },\n          },\n        },\n        headers: {},\n        status: 200,\n      };\n\n      // Mock the get method which getStory calls internally\n      client.get = vi.fn().mockResolvedValue(mockStoryResponse);\n\n      // Call getStory without resolve_relations\n      const result = await client.getStory(storySlug, {\n        version: 'published',\n        // No resolve_relations parameter\n      });\n\n      // Verify the function executed without errors\n      expect(result).toEqual(mockStoryResponse);\n\n      // Verify that get was called with the right parameters\n      expect(client.get).toHaveBeenCalledWith(\n        `cdn/stories/${storySlug}`,\n        {\n          version: 'published',\n          // resolve_level should not be added since resolve_relations was undefined\n        },\n        undefined,\n      );\n    });\n\n    it('should add resolve_level when resolve_relations is provided', async () => {\n      const storySlug = 'test-story';\n      const mockStoryResponse = {\n        data: {\n          story: {\n            id: 123,\n            uuid: 'test-uuid',\n            name: 'Test Story',\n            content: {\n              _uid: 'test-uid',\n              component: 'test',\n              title: 'Test Title',\n            },\n          },\n        },\n        headers: {},\n        status: 200,\n      };\n\n      // Mock the get method\n      client.get = vi.fn().mockResolvedValue(mockStoryResponse);\n\n      // Call getStory with resolve_relations\n      await client.getStory(storySlug, {\n        version: 'published',\n        resolve_relations: 'test.relation',\n      });\n\n      // Verify that get was called with resolve_level added\n      expect(client.get).toHaveBeenCalledWith(\n        `cdn/stories/${storySlug}`,\n        {\n          version: 'published',\n          resolve_relations: 'test.relation',\n          resolve_level: 2,\n        },\n        undefined,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/index.ts",
    "content": "import throttledQueue from './throttlePromise';\nimport {\n  asyncMap,\n  delay,\n  flatMap,\n  getOptionsPage,\n  getRegionURL,\n  isCDNUrl,\n  range,\n  stringify,\n} from './utils';\nimport SbFetch from './sbFetch';\nimport type Method from './constants';\nimport type { StoryblokContentVersionKeys } from './constants';\nimport { STORYBLOK_AGENT, STORYBLOK_JS_CLIENT_AGENT, StoryblokContentVersion } from './constants';\n\nimport type {\n  ICacheProvider,\n  IMemoryType,\n  ISbCache,\n  ISbComponentType,\n  ISbConfig,\n  ISbContentMangmntAPI,\n  ISbCustomFetch,\n  ISbField,\n  ISbLinksParams,\n  ISbLinksResult,\n  ISbLinkURLObject,\n  ISbResponse,\n  ISbResponseData,\n  ISbResult,\n  ISbStories,\n  ISbStoriesParams,\n  ISbStory,\n  ISbStoryData,\n  ISbStoryParams,\n} from './interfaces';\n\nlet memory: Partial<IMemoryType> = {};\n\nconst cacheVersions = {} as CachedVersions;\n\ninterface CachedVersions {\n  [key: string]: number;\n}\n\ninterface LinksType {\n  [key: string]: any;\n}\n\ninterface RelationsType {\n  [key: string]: any;\n}\n\ninterface ISbFlatMapped {\n  data: any;\n}\n\nconst _VERSION = {\n  V1: 'v1',\n  V2: 'v2',\n} as const;\n\ntype ObjectValues<T> = T[keyof T];\ntype Version = ObjectValues<typeof _VERSION>;\n\nclass Storyblok {\n  private client: SbFetch;\n  private maxRetries: number;\n  private retriesDelay: number;\n  private throttle: ReturnType<typeof throttledQueue>;\n  private accessToken: string;\n  private cache: ISbCache;\n  private resolveCounter: number;\n  public relations: RelationsType;\n  public links: LinksType;\n  public version: StoryblokContentVersionKeys | undefined;\n  /**\n   * @deprecated This property is deprecated. Use the standalone `richTextResolver` from `@storyblok/richtext` instead.\n   * @see https://github.com/storyblok/richtext\n   */\n  public richTextResolver: unknown;\n  public resolveNestedRelations: boolean;\n  private stringifiedStoriesCache: Record<string, string>;\n  private inlineAssets: boolean;\n\n  /**\n   *\n   * @param config ISbConfig interface\n   * @param pEndpoint string, optional\n   */\n  public constructor(config: ISbConfig, pEndpoint?: string) {\n    let endpoint = config.endpoint || pEndpoint;\n\n    if (!endpoint) {\n      const protocol = config.https === false ? 'http' : 'https';\n\n      if (!config.oauthToken) {\n        endpoint = `${protocol}://${getRegionURL(config.region)}/${'v2' as Version}`;\n      }\n      else {\n        endpoint = `${protocol}://${getRegionURL(config.region)}/${'v1' as Version}`;\n      }\n    }\n\n    const headers: Headers = new Headers();\n\n    headers.set('Content-Type', 'application/json');\n    headers.set('Accept', 'application/json');\n\n    if (config.headers) {\n      const entries\n        = config.headers.constructor.name === 'Headers'\n          ? config.headers.entries().toArray()\n          : Object.entries(config.headers);\n\n      entries.forEach(([key, value]: [string, string]) => {\n        headers.set(key, value);\n      });\n    }\n\n    if (!headers.has(STORYBLOK_AGENT)) {\n      headers.set(STORYBLOK_AGENT, STORYBLOK_JS_CLIENT_AGENT.defaultAgentName);\n      headers.set(\n        STORYBLOK_JS_CLIENT_AGENT.defaultAgentVersion,\n        STORYBLOK_JS_CLIENT_AGENT.packageVersion,\n      );\n    }\n\n    let rateLimit = 5; // per second for cdn api\n\n    if (config.oauthToken) {\n      headers.set('Authorization', config.oauthToken);\n      rateLimit = 3; // per second for management api\n    }\n\n    if (config.rateLimit) {\n      rateLimit = config.rateLimit;\n    }\n\n    this.maxRetries = config.maxRetries || 10;\n    this.retriesDelay = 300;\n    this.throttle = throttledQueue(\n      this.throttledRequest.bind(this),\n      rateLimit,\n      1000,\n    );\n\n    this.accessToken = config.accessToken || '';\n    this.relations = {} as RelationsType;\n    this.links = {} as LinksType;\n    this.cache = config.cache || { clear: 'manual' };\n    this.resolveCounter = 0;\n    this.resolveNestedRelations = config.resolveNestedRelations || true;\n    this.stringifiedStoriesCache = {} as Record<string, string>;\n    this.version = config.version || StoryblokContentVersion.DRAFT;\n    this.inlineAssets = config.inlineAssets || false;\n\n    this.client = new SbFetch({\n      baseURL: endpoint,\n      timeout: config.timeout || 0,\n      headers,\n      responseInterceptor: config.responseInterceptor,\n      fetch: config.fetch,\n    });\n  }\n\n  private parseParams(params: ISbStoriesParams): ISbStoriesParams {\n    if (!params.token) {\n      params.token = this.getToken();\n    }\n\n    if (!params.cv) {\n      params.cv = cacheVersions[params.token];\n    }\n\n    if (Array.isArray(params.resolve_relations)) {\n      params.resolve_relations = params.resolve_relations.join(',');\n    }\n\n    if (typeof params.resolve_relations !== 'undefined') {\n      params.resolve_level = 2;\n    }\n\n    return params;\n  }\n\n  private factoryParamOptions(\n    url: string,\n    params: ISbStoriesParams,\n  ): ISbStoriesParams {\n    if (isCDNUrl(url)) {\n      return this.parseParams(params);\n    }\n\n    return params;\n  }\n\n  private makeRequest(\n    url: string,\n    params: ISbStoriesParams,\n    per_page: number,\n    page: number,\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResult> {\n    const query = this.factoryParamOptions(\n      url,\n      getOptionsPage(params, per_page, page),\n    );\n\n    return this.cacheResponse(url, query, undefined, fetchOptions);\n  }\n\n  public get(\n    slug: 'cdn/links',\n    params?: ISbLinksParams,\n    fetchOptions?: ISbCustomFetch\n  ): Promise<ISbLinksResult>;\n\n  public get(\n    slug: string,\n    params?: ISbStoriesParams,\n    fetchOptions?: ISbCustomFetch\n  ): Promise<ISbResult>;\n\n  public get(\n    slug: string,\n    params: ISbStoriesParams | ISbLinksParams = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResult | ISbLinksResult> {\n    if (!params) {\n      params = {} as ISbStoriesParams;\n    }\n    const url = `/${slug}`;\n    params.version = params.version || this.version;\n    const query = this.factoryParamOptions(url, params);\n\n    return this.cacheResponse(url, query, undefined, fetchOptions);\n  }\n\n  public async getAll(\n    slug: string,\n    params: ISbStoriesParams = {},\n    entity?: string,\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<any[]> {\n    const perPage = params?.per_page || 25;\n    const url = `/${slug}`.replace(/\\/$/, '');\n    const e = entity ?? url.substring(url.lastIndexOf('/') + 1);\n    params.version = params.version || this.version;\n\n    const firstPage = 1;\n    const firstRes = await this.makeRequest(\n      url,\n      params,\n      perPage,\n      firstPage,\n      fetchOptions,\n    );\n    const lastPage = firstRes.total ? Math.ceil(firstRes.total / perPage) : 1;\n\n    const restRes: any = await asyncMap(\n      range(firstPage, lastPage),\n      (i: number) => {\n        return this.makeRequest(url, params, perPage, i + 1, fetchOptions);\n      },\n    );\n\n    return flatMap([firstRes, ...restRes], (res: ISbFlatMapped) =>\n      Object.values(res.data[e]));\n  }\n\n  public post(\n    slug: string,\n    params: ISbStoriesParams | ISbContentMangmntAPI = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResponseData> {\n    const url = `/${slug}`;\n\n    return this.throttle('post', url, params, fetchOptions) as Promise<ISbResponseData>;\n  }\n\n  public put(\n    slug: string,\n    params: ISbStoriesParams | ISbContentMangmntAPI = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResponseData> {\n    const url = `/${slug}`;\n\n    return this.throttle('put', url, params, fetchOptions) as Promise<ISbResponseData>;\n  }\n\n  public delete(\n    slug: string,\n    params: ISbStoriesParams | ISbContentMangmntAPI = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResponseData> {\n    if (!params) {\n      params = {} as ISbStoriesParams;\n    }\n    const url = `/${slug}`;\n\n    return this.throttle('delete', url, params, fetchOptions) as Promise<ISbResponseData>;\n  }\n\n  public getStories(\n    params: ISbStoriesParams = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbStories> {\n    this._addResolveLevel(params);\n\n    return this.get('cdn/stories', params, fetchOptions);\n  }\n\n  public getStory(\n    slug: string,\n    params: ISbStoryParams = {},\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbStory> {\n    this._addResolveLevel(params);\n\n    return this.get(`cdn/stories/${slug}`, params, fetchOptions);\n  }\n\n  private getToken(): string {\n    return this.accessToken;\n  }\n\n  public ejectInterceptor(): void {\n    this.client.eject();\n  }\n\n  private _addResolveLevel(params: ISbStoriesParams | ISbStoryParams): void {\n    if (typeof params.resolve_relations !== 'undefined') {\n      params.resolve_level = 2;\n    }\n  }\n\n  private _cleanCopy(value: LinksType): JSON {\n    return JSON.parse(JSON.stringify(value));\n  }\n\n  private _insertLinks(\n    jtree: ISbStoriesParams,\n    treeItem: keyof ISbStoriesParams,\n    resolveId: string,\n  ): void {\n    const node = jtree[treeItem];\n\n    if (\n      node\n      && node.fieldtype === 'multilink'\n      && node.linktype === 'story'\n      && typeof node.id === 'string'\n      && this.links[resolveId][node.id]\n    ) {\n      node.story = this._cleanCopy(this.links[resolveId][node.id]);\n    }\n    else if (\n      node\n      && node.linktype === 'story'\n      && typeof node.uuid === 'string'\n      && this.links[resolveId][node.uuid]\n    ) {\n      node.story = this._cleanCopy(this.links[resolveId][node.uuid]);\n    }\n  }\n\n  /**\n   *\n   * @param resolveId A counter number as a string\n   * @param uuid The uuid of the story\n   * @returns string | object\n   */\n  private getStoryReference(resolveId: string, uuid: string): string | JSON {\n    const result = this.relations[resolveId][uuid]\n      ? JSON.parse(this.stringifiedStoriesCache[uuid] || JSON.stringify(this.relations[resolveId][uuid]))\n      : uuid;\n    return result;\n  }\n\n  /**\n   * Resolves a field's value by replacing UUIDs with their corresponding story references\n   * @param jtree - The JSON tree object containing the field to resolve\n   * @param treeItem - The key of the field to resolve\n   * @param resolveId - The unique identifier for the current resolution context\n   *\n   * This method handles both single string UUIDs and arrays of UUIDs:\n   * - For single strings: directly replaces the UUID with the story reference\n   * - For arrays: maps through each UUID and replaces with corresponding story references\n   */\n  private _resolveField(\n    jtree: ISbStoriesParams,\n    treeItem: keyof ISbStoriesParams,\n    resolveId: string,\n  ): void {\n    const item = jtree[treeItem];\n    if (typeof item === 'string') {\n      jtree[treeItem] = this.getStoryReference(resolveId, item);\n    }\n    else if (Array.isArray(item)) {\n      jtree[treeItem] = item.map(uuid =>\n        this.getStoryReference(resolveId, uuid),\n      ).filter(Boolean);\n    }\n  }\n\n  /**\n   * Inserts relations into the JSON tree by resolving references\n   * @param jtree - The JSON tree object to process\n   * @param treeItem - The current field being processed\n   * @param fields - The relation patterns to resolve (string or array of strings)\n   * @param resolveId - The unique identifier for the current resolution context\n   *\n   * This method handles two types of relation patterns:\n   * 1. Nested relations: matches fields that end with the current field name\n   *    Example: If treeItem is \"event_type\", it matches patterns like \"*.event_type\"\n   *\n   * 2. Direct component relations: matches exact component.field patterns\n   *    Example: \"event.event_type\" for component \"event\" and field \"event_type\"\n   *\n   * The method supports both string and array formats for the fields parameter,\n   * allowing flexible specification of relation patterns.\n   */\n  private _insertRelations(\n    jtree: ISbStoriesParams,\n    treeItem: keyof ISbStoriesParams,\n    fields: string | string[],\n    resolveId: string,\n  ): void {\n    // Check for nested relations (e.g., \"*.event_type\" or \"spots.event_type\")\n    const fieldPattern = Array.isArray(fields)\n      ? fields.find(f => f.endsWith(`.${treeItem}`))\n      : fields.endsWith(`.${treeItem}`);\n\n    if (fieldPattern) {\n      // If we found a matching pattern, resolve this field\n      this._resolveField(jtree, treeItem, resolveId);\n      return;\n    }\n\n    // If no nested pattern matched, check for direct component.field pattern\n    // e.g., \"event.event_type\" for a field within its immediate parent component\n    const fieldPath = jtree.component ? `${jtree.component}.${treeItem}` : treeItem;\n    // Check if this exact pattern exists in the fields to resolve\n    if (Array.isArray(fields) ? fields.includes(fieldPath) : fields === fieldPath) {\n      this._resolveField(jtree, treeItem, resolveId);\n    }\n  }\n\n  /**\n   * Recursively traverses and resolves relations in the story content tree\n   * @param story - The story object containing the content to process\n   * @param fields - The relation patterns to resolve\n   * @param resolveId - The unique identifier for the current resolution context\n   */\n  private iterateTree(\n    story: ISbStoryData,\n    fields: string | Array<string>,\n    resolveId: string,\n  ): void {\n    // Internal recursive function to process each node in the tree\n    const enrich = (jtree: ISbStoriesParams | any, path = '') => {\n      // Skip processing if node is null/undefined or marked to stop resolving\n      if (!jtree || jtree._stopResolving) {\n        return;\n      }\n\n      // Handle arrays by recursively processing each element\n      // Maintains path context by adding array indices\n      if (Array.isArray(jtree)) {\n        jtree.forEach((item, index) => enrich(item, `${path}[${index}]`));\n      }\n      // Handle object nodes\n      else if (typeof jtree === 'object') {\n        // Process each property in the object\n        for (const key in jtree) {\n          // Build the current path for the context\n          const newPath = path ? `${path}.${key}` : key;\n\n          // If this is a component (has component and _uid) or a link,\n          // attempt to resolve its relations and links\n          if ((jtree.component && jtree._uid) || jtree.type === 'link') {\n            this._insertRelations(jtree, key as keyof ISbStoriesParams, fields, resolveId);\n            this._insertLinks(jtree, key as keyof ISbStoriesParams, resolveId);\n          }\n\n          // Continue traversing deeper into the tree\n          // This ensures we process nested components and their relations\n          enrich(jtree[key], newPath);\n        }\n      }\n    };\n\n    // Start the traversal from the story's content\n    enrich(story.content);\n  }\n\n  private async resolveLinks(\n    responseData: ISbResponseData,\n    params: ISbStoriesParams,\n    resolveId: string,\n  ): Promise<void> {\n    let links: (ISbStoryData | ISbLinkURLObject | string)[] = [];\n\n    if (responseData.link_uuids) {\n      const relSize = responseData.link_uuids.length;\n      const chunks = [];\n      const chunkSize = 50;\n\n      for (let i = 0; i < relSize; i += chunkSize) {\n        const end = Math.min(relSize, i + chunkSize);\n        chunks.push(responseData.link_uuids.slice(i, end));\n      }\n\n      for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {\n        const linksRes = await this.getStories({\n          per_page: chunkSize,\n          language: params.language,\n          version: params.version,\n          starts_with: params.starts_with,\n          by_uuids: chunks[chunkIndex].join(','),\n        });\n\n        linksRes.data.stories.forEach(\n          (rel: ISbStoryData | ISbLinkURLObject | string) => {\n            links.push(rel);\n          },\n        );\n      }\n    }\n    else {\n      links = responseData.links;\n    }\n\n    links.forEach((story: ISbStoryData | any) => {\n      this.links[resolveId][story.uuid] = {\n        ...story,\n        ...{ _stopResolving: true },\n      };\n    });\n  }\n\n  private async resolveRelations(\n    responseData: ISbResponseData,\n    params: ISbStoriesParams,\n    resolveId: string,\n  ): Promise<void> {\n    let relations: ISbStoryData<ISbComponentType<string> & { [index: string]: any }>[] = [];\n\n    if (responseData.rel_uuids) {\n      const relSize = responseData.rel_uuids.length;\n      const chunks = [];\n      const chunkSize = 50;\n\n      for (let i = 0; i < relSize; i += chunkSize) {\n        const end = Math.min(relSize, i + chunkSize);\n        chunks.push(responseData.rel_uuids.slice(i, end));\n      }\n\n      for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {\n        const relationsRes = await this.getStories({\n          per_page: chunkSize,\n          language: params.language,\n          version: params.version,\n          starts_with: params.starts_with,\n          by_uuids: chunks[chunkIndex].join(','),\n          excluding_fields: params.excluding_fields,\n        });\n\n        relationsRes.data.stories.forEach((rel: ISbStoryData) => {\n          relations.push(rel);\n        });\n      }\n\n      // Replace rel_uuids with the fully resolved stories and clear it\n      if (relations.length > 0) {\n        responseData.rels = relations;\n        delete responseData.rel_uuids;\n      }\n    }\n    else {\n      relations = responseData.rels;\n    }\n\n    if (relations && relations.length > 0) {\n      relations.forEach((story: ISbStoryData) => {\n        this.relations[resolveId][story.uuid] = {\n          ...story,\n          ...{ _stopResolving: true },\n        };\n      });\n    }\n  }\n\n  /**\n   *\n   * @param responseData\n   * @param params\n   * @param resolveId\n   * @description Resolves the relations and links of the stories\n   * @returns Promise<void>\n   *\n   */\n  private async resolveStories(\n    responseData: ISbResponseData,\n    params: ISbStoriesParams,\n    resolveId: string,\n  ): Promise<void> {\n    let relationParams: string[] = [];\n\n    this.links[resolveId] = {};\n    this.relations[resolveId] = {};\n\n    if (\n      typeof params.resolve_relations !== 'undefined'\n      && params.resolve_relations.length > 0\n    ) {\n      if (typeof params.resolve_relations === 'string') {\n        relationParams = params.resolve_relations.split(',');\n      }\n      await this.resolveRelations(responseData, params, resolveId);\n    }\n\n    if (\n      params.resolve_links\n      && ['1', 'story', 'url', 'link'].includes(params.resolve_links)\n      && (responseData.links?.length || responseData.link_uuids?.length)\n    ) {\n      await this.resolveLinks(responseData, params, resolveId);\n    }\n\n    if (this.resolveNestedRelations) {\n      for (const relUuid in this.relations[resolveId]) {\n        this.iterateTree(\n          this.relations[resolveId][relUuid],\n          relationParams,\n          resolveId,\n        );\n      }\n    }\n\n    if (responseData.story) {\n      this.iterateTree(responseData.story, relationParams, resolveId);\n    }\n    else {\n      responseData.stories.forEach((story: ISbStoryData) => {\n        this.iterateTree(story, relationParams, resolveId);\n      });\n    }\n\n    this.stringifiedStoriesCache = {};\n\n    delete this.links[resolveId];\n    delete this.relations[resolveId];\n  }\n\n  private async cacheResponse(\n    url: string,\n    params: ISbStoriesParams,\n    retries?: number,\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<ISbResult> {\n    const cacheKey = stringify({ url, params });\n    const provider = this.cacheProvider();\n\n    if (params.version === 'published' && url !== '/cdn/spaces/me') {\n      const cache = await provider.get(cacheKey);\n      if (cache) {\n        return Promise.resolve(cache);\n      }\n    }\n\n    return new Promise(async (resolve, reject) => {\n      try {\n        const res = (await this.throttle(\n          'get',\n          url,\n          params,\n          fetchOptions,\n        )) as ISbResponse;\n        if (res.status !== 200) {\n          return reject(res);\n        }\n\n        let response = { data: res.data, headers: res.headers } as ISbResult;\n\n        if (res.headers?.['per-page']) {\n          response = Object.assign({}, response, {\n            perPage: res.headers['per-page']\n              ? Number.parseInt(res.headers['per-page'])\n              : 0,\n            total: res.headers['per-page']\n              ? Number.parseInt(res.headers.total)\n              : 0,\n          });\n        }\n\n        if (response.data.story || response.data.stories) {\n          const resolveId = (this.resolveCounter\n            = ++this.resolveCounter % 1000);\n          await this.resolveStories(response.data, params, `${resolveId}`);\n          response = await this.processInlineAssets(response);\n        }\n\n        if (params.version === 'published' && url !== '/cdn/spaces/me') {\n          await provider.set(cacheKey, response);\n        }\n\n        const isCacheClearable = (this.cache.clear === 'onpreview' && params.version === 'draft')\n          || this.cache.clear === 'auto';\n\n        if (params.token && response.data.cv) {\n          if (isCacheClearable\n            && cacheVersions[params.token] // there is a cache\n            && cacheVersions[params.token] !== response.data.cv // a new cv is incoming\n          ) {\n            await this.flushCache();\n          }\n          cacheVersions[params.token] = response.data.cv;\n        }\n\n        return resolve(response);\n      }\n      catch (error: Error | any) {\n        if (error.response && error.status === 429) {\n          retries = typeof retries === 'undefined' ? 0 : retries + 1;\n\n          if (retries < this.maxRetries) {\n            // eslint-disable-next-line no-console\n            console.log(\n              `Hit rate limit. Retrying in ${this.retriesDelay / 1000} seconds.`,\n            );\n            await delay(this.retriesDelay);\n            return this.cacheResponse(url, params, retries)\n              .then(resolve)\n              .catch(reject);\n          }\n        }\n        reject(error);\n      }\n    });\n  }\n\n  private throttledRequest(\n    type: Method,\n    url: string,\n    params: ISbStoriesParams,\n    fetchOptions?: ISbCustomFetch,\n  ): Promise<unknown> {\n    this.client.setFetchOptions(fetchOptions);\n    return this.client[type](url, params);\n  }\n\n  public cacheVersions(): CachedVersions {\n    return cacheVersions;\n  }\n\n  public cacheVersion(): number {\n    return cacheVersions[this.accessToken];\n  }\n\n  public setCacheVersion(cv: number): void {\n    if (this.accessToken) {\n      cacheVersions[this.accessToken] = cv;\n    }\n  }\n\n  public clearCacheVersion(): void {\n    if (this.accessToken) {\n      cacheVersions[this.accessToken] = 0;\n    }\n  }\n\n  public cacheProvider(): ICacheProvider {\n    switch (this.cache.type) {\n      case 'memory':\n        return {\n          get(key: string) {\n            return Promise.resolve(memory[key]);\n          },\n          getAll() {\n            return Promise.resolve(memory as IMemoryType);\n          },\n          set(key: string, content: ISbResult) {\n            memory[key] = content;\n            return Promise.resolve(undefined);\n          },\n          flush() {\n            memory = {};\n            return Promise.resolve(undefined);\n          },\n        };\n      case 'custom':\n        if (this.cache.custom) {\n          return this.cache.custom;\n        }\n      // eslint-disable-next-line no-fallthrough\n      default:\n        return {\n          get() {\n            return Promise.resolve();\n          },\n          getAll() {\n            return Promise.resolve(undefined);\n          },\n          set() {\n            return Promise.resolve(undefined);\n          },\n          flush() {\n            return Promise.resolve(undefined);\n          },\n        };\n    }\n  }\n\n  public async flushCache(): Promise<this> {\n    await this.cacheProvider().flush();\n    this.clearCacheVersion();\n    return this;\n  }\n\n  private async processInlineAssets(response: ISbResult): Promise<ISbResult> {\n    if (!this.inlineAssets) {\n      return response;\n    }\n\n    const processNode = (node: ISbField): unknown => {\n      if (!node || typeof node !== 'object') {\n        return node;\n      }\n\n      // Handle arrays\n      if (Array.isArray(node)) {\n        return node.map(item => processNode(item));\n      }\n\n      // Process object\n      let processedNode = { ...node };\n\n      // Check if this is an asset field\n      if (processedNode.fieldtype === 'asset' && Array.isArray(response.data.assets)) {\n        // Replace the assets array with the actual asset objects\n        processedNode = {\n          ...processedNode,\n          ...response.data.assets.find((asset: any) => asset.id === processedNode.id),\n        };\n      }\n\n      // Recursively process all properties\n      for (const key in processedNode) {\n        if (typeof processedNode[key] === 'object') {\n          processedNode[key] = processNode(processedNode[key] as ISbField);\n        }\n      }\n\n      return processedNode;\n    };\n\n    // Process the story content\n    if (response.data.story) {\n      response.data.story.content = processNode(response.data.story.content);\n    }\n\n    // Process all stories if present\n    if (response.data.stories) {\n      response.data.stories = response.data.stories.map((story: any) => {\n        story.content = processNode(story.content);\n        return story;\n      });\n    }\n\n    return response;\n  }\n}\n\nexport default Storyblok;\n"
  },
  {
    "path": "src/interfaces.ts",
    "content": "import type { ResponseFn } from './sbFetch';\nimport type Method from './constants';\nimport type { StoryblokContentVersionKeys } from './constants';\n\nexport interface ISbStoriesParams\n  extends Partial<ISbStoryData>,\n  ISbMultipleStoriesData,\n  ISbAssetsParams {\n  resolve_level?: number;\n  _stopResolving?: boolean;\n  by_slugs?: string;\n  by_uuids?: string;\n  by_uuids_ordered?: string;\n  component?: string;\n  content_type?: string;\n  cv?: number;\n  datasource?: string;\n  dimension?: string;\n  excluding_fields?: string;\n  excluding_ids?: string;\n  excluding_slugs?: string;\n  fallback_lang?: string;\n  filename?: string;\n  filter_query?: any;\n  first_published_at_gt?: string;\n  first_published_at_lt?: string;\n  from_release?: string;\n  is_startpage?: boolean;\n  language?: string;\n  level?: number;\n  page?: number;\n  per_page?: number;\n  published_at_gt?: string;\n  published_at_lt?: string;\n  resolve_assets?: number;\n  resolve_links?: 'link' | 'url' | 'story' | '0' | '1' | 'link';\n  resolve_links_level?: 1 | 2;\n  resolve_relations?: string | string[];\n  search_term?: string;\n  size?: string;\n  sort_by?: string;\n  starts_with?: string;\n  token?: string;\n  version?: StoryblokContentVersionKeys;\n  with_tag?: string;\n}\n\nexport interface ISbStoryParams {\n  resolve_level?: number;\n  token?: string;\n  find_by?: 'uuid';\n  version?: StoryblokContentVersionKeys;\n  resolve_assets?: number;\n  resolve_links?: 'link' | 'url' | 'story' | '0' | '1';\n  resolve_links_level?: 1 | 2;\n  resolve_relations?: string | string[];\n  cv?: number;\n  from_release?: string;\n  language?: string;\n  fallback_lang?: string;\n}\n\ninterface Dimension {\n  id: number;\n  name: string;\n  entry_value: string;\n  datasource_id: number;\n  created_at: string;\n  updated_at: string;\n}\n\n/**\n * @interface ISbDimensions\n * @description Storyblok Dimensions Interface auxiliary interface\n * @description One use it to handle the API response\n */\nexport interface ISbDimensions {\n  dimensions: Dimension[];\n}\n\nexport interface ISbComponentType<T extends string> {\n  _uid?: string;\n  component?: T;\n  _editable?: string;\n}\n\nexport interface PreviewToken {\n  token: string;\n  timestamp: string;\n}\n\nexport interface LocalizedPath {\n  path: string;\n  name: string | null;\n  lang: string;\n  published: boolean;\n}\n\nexport interface ISbStoryData<\n  Content = ISbComponentType<string> & { [index: string]: any },\n> extends ISbMultipleStoriesData {\n  alternates: ISbAlternateObject[];\n  breadcrumbs?: ISbLinkURLObject[];\n  content: Content;\n  created_at: string;\n  deleted_at?: string;\n  default_full_slug?: string | null;\n  default_root?: string;\n  disble_fe_editor?: boolean;\n  favourite_for_user_ids?: number[] | null;\n  first_published_at?: string | null;\n  full_slug: string;\n  group_id: string;\n  id: number;\n  imported_at?: string;\n  is_folder?: boolean;\n  is_startpage?: boolean;\n  lang: string;\n  last_author?: {\n    id: number;\n    userid: string;\n  };\n  last_author_id?: number;\n  localized_paths?: LocalizedPath[] | null;\n  meta_data: any;\n  name: string;\n  parent?: ISbStoryData;\n  parent_id: number | null;\n  path?: string;\n  pinned?: '1' | boolean;\n  position: number;\n  preview_token?: PreviewToken;\n  published?: boolean;\n  published_at: string | null;\n  release_id?: number | null;\n  scheduled_date?: string | null;\n  slug: string;\n  sort_by_date: string | null;\n  tag_list: string[];\n  translated_slugs?: {\n    path: string;\n    name: string | null;\n    lang: ISbStoryData['lang'];\n  }[] | null;\n  unpublished_changes?: boolean;\n  updated_at?: string;\n  uuid: string;\n}\n\nexport interface ISbMultipleStoriesData {\n  by_ids?: string;\n  by_uuids?: string;\n  contain_component?: string;\n  excluding_ids?: string;\n  filter_query?: any;\n  folder_only?: boolean;\n  full_slug?: string;\n  in_release?: string;\n  in_trash?: boolean;\n  is_published?: boolean;\n  in_workflow_stages?: string;\n  page?: number;\n  pinned?: '1' | boolean;\n  search?: string;\n  sort_by?: string;\n  starts_with?: string;\n  story_only?: boolean;\n  text_search?: string;\n  with_parent?: number;\n  with_slug?: string;\n  with_tag?: string;\n}\n\nexport interface ISbAlternateObject {\n  id: number;\n  name: string;\n  slug: string;\n  published: boolean;\n  full_slug: string;\n  is_folder: boolean;\n  parent_id: number;\n}\n\nexport interface ISbLinkURLObject {\n  id: number;\n  name: string;\n  slug: string;\n  full_slug: string;\n  url: string;\n  uuid: string;\n}\n\nexport interface ISbStories<\n  Content = ISbComponentType<string> & { [index: string]: any },\n> {\n  data: {\n    cv: number;\n    links: (ISbStoryData | ISbLinkURLObject)[];\n    rels: ISbStoryData[];\n    stories: ISbStoryData<Content>[];\n  };\n  perPage: number;\n  total: number;\n  headers: any;\n}\n\nexport interface ISbStory<\n  Content = ISbComponentType<string> & { [index: string]: any },\n> {\n  data: {\n    cv: number;\n    links: (ISbStoryData | ISbLinkURLObject)[];\n    rels: ISbStoryData[];\n    story: ISbStoryData<Content>;\n  };\n  headers: any;\n}\n\nexport interface IMemoryType extends ISbResult {\n  [key: string]: any;\n}\n\nexport interface ICacheProvider {\n  get: (key: string) => Promise<IMemoryType | void>;\n  set: (key: string, content: ISbResult) => Promise<void>;\n  getAll: () => Promise<IMemoryType | void>;\n  flush: () => Promise<void>;\n}\n\nexport interface ISbCache {\n  type?: 'none' | 'memory' | 'custom';\n  clear?: 'auto' | 'manual' | 'onpreview';\n  custom?: ICacheProvider;\n}\n\nexport interface ISbConfig {\n  accessToken?: string;\n  oauthToken?: string;\n  resolveNestedRelations?: boolean;\n  cache?: ISbCache;\n  responseInterceptor?: ResponseFn;\n  fetch?: typeof fetch;\n  timeout?: number;\n  headers?: any;\n  region?: string;\n  maxRetries?: number;\n  https?: boolean;\n  rateLimit?: number;\n  endpoint?: string;\n  version?: StoryblokContentVersionKeys | undefined;\n  inlineAssets?: boolean;\n}\n\nexport interface ISbResult {\n  data: any;\n  perPage: number;\n  total: number;\n  headers: Headers;\n}\n\nexport interface ISbLinksResult extends ISbResult {\n  data: ISbLinks;\n}\n\nexport interface ISbResponse {\n  data: any;\n  status: number;\n  statusText: string;\n  headers: any;\n}\n\nexport interface ISbError {\n  message?: string;\n  status?: number;\n  response?: ISbResponse;\n}\n\nexport interface ISbContentMangmntAPI<\n  Content = ISbComponentType<string> & { [index: string]: any },\n> {\n  story: {\n    name: string;\n    slug: string;\n    content?: Content;\n    default_root?: boolean;\n    is_folder?: boolean;\n    parent_id?: string;\n    disble_fe_editor?: boolean;\n    path?: string;\n    is_startpage?: boolean;\n    position?: number;\n    first_published_at?: string;\n    sort_by_date?: string;\n    translated_slugs_attributes?: {\n      path: string;\n      name: string | null;\n      lang: ISbContentMangmntAPI['lang'];\n    }[];\n  };\n  force_update?: '1' | unknown;\n  release_id?: number;\n  publish?: '1' | unknown;\n  lang?: string;\n}\n\nexport interface ISbManagmentApiResult {\n  data: any;\n  headers: any;\n}\n\nexport interface ISbSchema {\n  nodes: any;\n  marks: any;\n}\n\nexport interface LinkCustomAttributes {\n  rel?: string;\n  title?: string;\n  [key: string]: any;\n}\n\nexport interface ISbLink {\n  id?: number;\n  slug?: string;\n  name?: string;\n  is_folder?: boolean;\n  parent_id?: number;\n  published?: boolean;\n  position?: number;\n  uuid?: string;\n  is_startpage?: boolean;\n  path?: string;\n  real_path?: string;\n  published_at?: string;\n  created_at?: string;\n  updated_at?: string;\n}\n\nexport interface ISbLinksParams {\n  starts_with?: string;\n  version?: StoryblokContentVersionKeys;\n  paginated?: number;\n  per_page?: number;\n  page?: number;\n  sort_by?: string;\n  include_dates?: 0 | 1;\n  with_parent?: number;\n}\n\nexport interface ISbLinks {\n  links?: {\n    [key: string]: ISbLink;\n  };\n}\n\nexport interface Queue<T> {\n  resolve: (value: unknown) => void;\n  reject: (reason?: unknown) => void;\n  args: T;\n}\n\nexport interface ISbResponseData {\n  link_uuids: string[];\n  links: string[];\n  rel_uuids?: string[];\n  rels: any;\n  story: ISbStoryData;\n  stories: Array<ISbStoryData>;\n}\n\nexport interface ISbThrottle<\n  T extends (...args: Parameters<T>) => ReturnType<T>,\n> {\n  abort?: () => void;\n  (...args: Parameters<T>): Promise<unknown>;\n}\n\nexport type ISbThrottledRequest = (\n  type: Method,\n  url: string,\n  params: ISbStoriesParams,\n  fetchOptions?: ISbCustomFetch\n) => Promise<unknown>;\n\nexport type AsyncFn = (...args: any) => [] | Promise<ISbResult>;\n\nexport type ArrayFn = (...args: any) => void;\n\nexport interface HtmlEscapes {\n  [key: string]: string;\n}\n\nexport interface ISbCustomFetch extends Omit<RequestInit, 'method'> {}\n\nexport interface ISbAssetsParams {\n  in_folder?: string;\n  is_private?: boolean;\n  by_alt?: string;\n  by_copyright?: string;\n  by_title?: string;\n}\n\nexport interface ISbField {\n  fieldtype: string;\n  id: string;\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "src/sbFetch.test.ts",
    "content": "import { afterEach, describe, expect, it, vi } from 'vitest';\nimport type { ISbFetch } from './sbFetch';\nimport SbFetch from './sbFetch';\nimport { headersToObject } from '../tests/utils';\n\ndescribe('sbFetch', () => {\n  let sbFetch: SbFetch;\n  const mockFetch = vi.fn();\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it('should initialize', () => {\n    sbFetch = new SbFetch({} as ISbFetch);\n    expect(sbFetch).toBeInstanceOf(SbFetch);\n  });\n\n  describe('get', () => {\n    it('should correctly construct URLs for GET requests', async () => {\n      sbFetch = new SbFetch({\n        baseURL: 'https://api.storyblok.com/v2/',\n        fetch: mockFetch,\n      } as ISbFetch);\n      const response = new Response(JSON.stringify({ data: 'test' }), {\n        status: 200,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      mockFetch.mockResolvedValue(response);\n      await sbFetch.get('test', {\n        is_startpage: false,\n        search_term: 'test',\n      });\n      expect(mockFetch).toHaveBeenCalledWith(\n        'https://api.storyblok.com/v2/test?is_startpage=false&search_term=test',\n        expect.anything(),\n      );\n    });\n  });\n\n  describe('post', () => {\n    it('should handle POST requests correctly', async () => {\n      const testPayload = { title: 'New Story' };\n      const response = new Response(JSON.stringify({ data: 'test' }), {\n        status: 200,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      mockFetch.mockResolvedValue(response);\n      await sbFetch.post('stories', testPayload);\n      expect(mockFetch).toHaveBeenCalledWith(\n        'https://api.storyblok.com/v2/stories',\n        {\n          method: 'post',\n          body: JSON.stringify(testPayload),\n          headers: expect.any(Headers),\n          signal: expect.any(AbortSignal),\n        },\n      );\n    });\n\n    it('should set specific headers for POST requests', async () => {\n      sbFetch = new SbFetch({\n        baseURL: 'https://api.storyblok.com/v2/',\n        headers: new Headers({\n          'Content-Type': 'application/json',\n        }),\n        fetch: mockFetch,\n      } as ISbFetch);\n      const testPayload = { title: 'New Story' };\n      const response = new Response(JSON.stringify({ data: 'test' }), {\n        status: 200,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      mockFetch.mockResolvedValue(response);\n\n      await sbFetch.post('stories', testPayload);\n\n      // Get the last call to fetch and extract the headers\n      const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];\n      const actualHeaders = headersToObject(lastCall[1].headers);\n      expect(actualHeaders['content-type']).toBe('application/json');\n    });\n  });\n\n  describe('put', () => {\n    it('should handle PUT requests correctly', async () => {\n      const testPayload = { title: 'Updated Story' };\n      const response = new Response(JSON.stringify({ data: 'test' }), {\n        status: 200,\n        headers: { 'Content-Type': 'application/json' },\n      });\n      mockFetch.mockResolvedValue(response);\n      await sbFetch.put('stories/1', testPayload);\n      expect(mockFetch).toHaveBeenCalledWith(\n        'https://api.storyblok.com/v2/stories/1',\n        {\n          method: 'put',\n          body: JSON.stringify(testPayload),\n          headers: expect.any(Headers),\n          signal: expect.any(AbortSignal),\n        },\n      );\n    });\n  });\n\n  describe('delete', () => {\n    it('should handle DELETE requests correctly', async () => {\n      const response = new Response(null, {\n        status: 204, // Typically, DELETE operations might not return content\n      });\n      mockFetch.mockResolvedValue(response);\n      await sbFetch.delete('stories/1');\n      expect(mockFetch).toHaveBeenCalledWith(\n        'https://api.storyblok.com/v2/stories/1',\n        {\n          method: 'delete',\n          body: '{}', // Ensuring no body is sent\n          headers: expect.any(Headers),\n          signal: expect.any(AbortSignal),\n        },\n      );\n    });\n  });\n\n  it('should handle network errors gracefully', async () => {\n    const mockFetch = vi.fn().mockRejectedValue(new Error('Network Failure'));\n    const sbFetch = new SbFetch({\n      baseURL: 'https://api.example.com',\n      headers: new Headers(),\n      fetch: mockFetch,\n    });\n\n    // Assuming your implementation wraps the error message inside an object under `message`.\n    const result = await sbFetch.get('/test', {});\n\n    // Check if the error object format matches your implementation.\n    expect(result).toEqual({\n      message: expect.any(Error), // Checks if `message` is an instance of Error\n    });\n\n    // If you want to be more specific and check the message of the error:\n    expect(result.message.message).toEqual('Network Failure'); // This path needs to match the structure you actually use.\n  });\n});\n"
  },
  {
    "path": "src/sbFetch.ts",
    "content": "import { stringify } from './utils';\n\nimport type {\n  ISbCustomFetch,\n  ISbError,\n  ISbResponse,\n  ISbStoriesParams,\n} from './interfaces';\nimport type Method from './constants';\n\nexport interface ResponseFn {\n  (arg?: ISbResponse | any): any;\n}\n\nexport interface ISbFetch {\n  baseURL: string;\n  timeout?: number;\n  headers: Headers;\n  responseInterceptor?: ResponseFn;\n  fetch?: typeof fetch;\n}\n\nclass SbFetch {\n  private baseURL: string;\n  private timeout?: number;\n  private headers: Headers;\n  private responseInterceptor?: ResponseFn;\n  private fetch: typeof fetch;\n  private ejectInterceptor?: boolean;\n  private url: string;\n  private parameters: ISbStoriesParams;\n  private fetchOptions: ISbCustomFetch;\n\n  public constructor($c: ISbFetch) {\n    this.baseURL = $c.baseURL;\n    this.headers = $c.headers || new Headers();\n    this.timeout = $c?.timeout ? $c.timeout * 1000 : 0;\n    this.responseInterceptor = $c.responseInterceptor;\n    this.fetch = (...args: [any]) =>\n      $c.fetch ? $c.fetch(...args) : fetch(...args);\n    this.ejectInterceptor = false;\n    this.url = '';\n    this.parameters = {} as ISbStoriesParams;\n    this.fetchOptions = {};\n  }\n\n  /**\n   *\n   * @param url string\n   * @param params ISbStoriesParams\n   * @returns Promise<ISbResponse | Error>\n   */\n  public get(url: string, params: ISbStoriesParams) {\n    this.url = url;\n    this.parameters = params;\n    return this._methodHandler('get');\n  }\n\n  public post(url: string, params: ISbStoriesParams) {\n    this.url = url;\n    this.parameters = params;\n    return this._methodHandler('post');\n  }\n\n  public put(url: string, params: ISbStoriesParams) {\n    this.url = url;\n    this.parameters = params;\n    return this._methodHandler('put');\n  }\n\n  public delete(url: string, params?: ISbStoriesParams) {\n    this.url = url;\n    this.parameters = params ?? {} as ISbStoriesParams;\n    return this._methodHandler('delete');\n  }\n\n  private async _responseHandler(res: Response) {\n    const headers: string[] = [];\n    const response = {\n      data: {},\n      headers: {},\n      status: 0,\n      statusText: '',\n    };\n\n    if (res.status !== 204) {\n      await res.json().then(($r) => {\n        response.data = $r;\n      });\n    }\n\n    for (const pair of res.headers.entries()) {\n      headers[pair[0] as any] = pair[1];\n    }\n\n    response.headers = { ...headers };\n    response.status = res.status;\n    response.statusText = res.statusText;\n\n    return response;\n  }\n\n  private async _methodHandler(\n    method: Method,\n  ): Promise<ISbResponse | ISbError> {\n    let urlString = `${this.baseURL}${this.url}`;\n\n    let body = null;\n\n    if (method === 'get') {\n      urlString = `${this.baseURL}${this.url}?${stringify(this.parameters)}`;\n    }\n    else {\n      body = JSON.stringify(this.parameters);\n    }\n\n    const url = new URL(urlString);\n\n    const controller = new AbortController();\n    const { signal } = controller;\n\n    let timeout;\n\n    if (this.timeout) {\n      timeout = setTimeout(() => controller.abort(), this.timeout);\n    }\n\n    try {\n      const fetchResponse = await this.fetch(`${url}`, {\n        method,\n        headers: this.headers,\n        body,\n        signal,\n        ...this.fetchOptions,\n      });\n\n      if (this.timeout) {\n        clearTimeout(timeout);\n      }\n\n      const response = (await this._responseHandler(\n        fetchResponse,\n      )) as ISbResponse;\n\n      if (this.responseInterceptor && !this.ejectInterceptor) {\n        return this._statusHandler(this.responseInterceptor(response));\n      }\n      else {\n        return this._statusHandler(response);\n      }\n    }\n    catch (err: any) {\n      const error: ISbError = {\n        message: err,\n      };\n      return error;\n    }\n  }\n\n  public setFetchOptions(fetchOptions: ISbCustomFetch = {}) {\n    if (Object.keys(fetchOptions).length > 0 && 'method' in fetchOptions) {\n      delete fetchOptions.method;\n    }\n    this.fetchOptions = { ...fetchOptions };\n  }\n\n  public eject() {\n    this.ejectInterceptor = true;\n  }\n\n  /**\n   * Normalizes error messages from different response structures\n   * @param data The response data that might contain error information\n   * @returns A normalized error message string\n   */\n  private _normalizeErrorMessage(data: any): string {\n    // Handle array of error messages\n    if (Array.isArray(data)) {\n      return data[0] || 'Unknown error';\n    }\n\n    // Handle object with error property\n    if (data && typeof data === 'object') {\n      // Check for common error message patterns\n      if (data.error) {\n        return data.error;\n      }\n\n      // Handle nested error objects (like { name: ['has already been taken'] })\n      for (const key in data) {\n        if (Array.isArray(data[key])) {\n          return `${key}: ${data[key][0]}`;\n        }\n        if (typeof data[key] === 'string') {\n          return `${key}: ${data[key]}`;\n        }\n      }\n\n      // If we have a slug, it might be an error message\n      if (data.slug) {\n        return data.slug;\n      }\n    }\n\n    // Fallback for unknown error structures\n    return 'Unknown error';\n  }\n\n  private _statusHandler(res: ISbResponse): Promise<ISbResponse | ISbError> {\n    const statusOk = /20[0-6]/g;\n\n    return new Promise((resolve, reject) => {\n      if (statusOk.test(`${res.status}`)) {\n        return resolve(res);\n      }\n\n      const error: ISbError = {\n        message: this._normalizeErrorMessage(res.data),\n        status: res.status,\n        response: res,\n      };\n\n      reject(error);\n    });\n  }\n}\n\nexport default SbFetch;\n"
  },
  {
    "path": "src/throttlePromise.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport throttledQueue from './throttlePromise';\n\n// Mock function to simulate async work with a delay\nconst mockFn = vi.fn(async (input) => {\n  await new Promise(resolve => setTimeout(resolve, 200)); // Simulate async delay\n  return input;\n});\n\ndescribe('throttledQueue', () => {\n  it('should resolve or reject all promises after the queue finishes, even when aborting', async () => {\n    const throttled = throttledQueue(mockFn, 3, 10); // Throttle with 3 concurrent tasks\n    const promises: Promise<any>[] = [];\n\n    // Generate 10 tasks and push them to the promises array\n    for (let i = 0; i < 10; i++) {\n      promises.push(throttled(i));\n      if (i === 5) {\n        throttled.abort(); // but abort at call #6\n      }\n    }\n\n    const results = await Promise.allSettled(promises);\n    results.forEach((result) => {\n      expect(['fulfilled', 'rejected']).toContain(result.status);\n    });\n  });\n  it('should enforce sequential resolution when throttle limit is exceeded', async () => {\n    const throttled = throttledQueue(mockFn, 1, 100); // Limit of 1, 100ms interval\n\n    const start = Date.now();\n    const promises = [\n      throttled('test1'),\n      throttled('test2'),\n      throttled('test3'),\n    ];\n\n    const results = await Promise.all(promises);\n    const duration = Date.now() - start;\n\n    // Expected behavior:\n    // Since each call has a 200ms delay, and there's a 100ms throttle interval and limit is 1,\n    // and each successive call should only start after the previous one completes,\n    // then the total duration should be around 800ms (200*3 + 100*2).\n    expect(results).toEqual(['test1', 'test2', 'test3']);\n    expect(duration).toBeGreaterThanOrEqual(800);\n  });\n});\n"
  },
  {
    "path": "src/throttlePromise.ts",
    "content": "import type { ISbThrottle, Queue } from './interfaces';\n\nclass AbortError extends Error {\n  constructor(msg: string) {\n    super(msg);\n    this.name = 'AbortError';\n  }\n}\n\nfunction throttledQueue<T extends (...args: Parameters<T>) => ReturnType<T>>(\n  fn: T,\n  limit: number,\n  interval: number,\n): ISbThrottle<T> {\n  if (!Number.isFinite(limit)) {\n    throw new TypeError('Expected `limit` to be a finite number');\n  }\n\n  if (!Number.isFinite(interval)) {\n    throw new TypeError('Expected `interval` to be a finite number');\n  }\n\n  const queue: Queue<Parameters<T>>[] = [];\n  let timeouts: ReturnType<typeof setTimeout>[] = [];\n  let activeCount = 0;\n  let isAborted = false;\n\n  const next = async () => {\n    activeCount++;\n\n    const x = queue.shift();\n    if (x) {\n      try {\n        const res = await fn(...x.args);\n        x.resolve(res);\n      }\n      catch (error) {\n        x.reject(error);\n      }\n    }\n\n    const id = setTimeout(() => {\n      activeCount--;\n\n      if (queue.length > 0) {\n        next();\n      }\n\n      timeouts = timeouts.filter(currentId => currentId !== id);\n    }, interval);\n\n    if (!timeouts.includes(id)) {\n      timeouts.push(id);\n    }\n  };\n\n  const throttled: ISbThrottle<T> = (...args) => {\n    if (isAborted) {\n      return Promise.reject(\n        new Error(\n          'Throttled function is already aborted and not accepting new promises',\n        ),\n      );\n    }\n\n    return new Promise((resolve, reject) => {\n      queue.push({\n        resolve,\n        reject,\n        args,\n      });\n\n      if (activeCount < limit) {\n        next();\n      }\n    });\n  };\n\n  throttled.abort = () => {\n    isAborted = true;\n    timeouts.forEach(clearTimeout);\n    timeouts = [];\n\n    queue.forEach(x =>\n      x.reject(() => new AbortError('Throttle function aborted')),\n    );\n    queue.length = 0;\n  };\n\n  return throttled;\n}\n\nexport default throttledQueue;\n"
  },
  {
    "path": "src/utils.test.ts",
    "content": "import { describe, expect, it, vi } from 'vitest';\nimport {\n  arrayFrom,\n  asyncMap,\n  delay,\n  escapeHTML,\n  flatMap,\n  getOptionsPage,\n  getRegionURL,\n  isCDNUrl,\n  range,\n  stringify,\n} from './utils';\nimport type { ISbResult } from './interfaces';\n\ntype RangeFn = (...args: any) => [];\n\ndescribe('utils', () => {\n  describe('isCDNUrl', () => {\n    it('returns true if the URL contains /cdn/', () => {\n      expect(isCDNUrl('http://example.com/cdn/content')).toBe(true);\n    });\n\n    it('returns false if the URL does not contain /cdn/', () => {\n      expect(isCDNUrl('http://example.com/content')).toBe(false);\n    });\n  });\n\n  describe('getOptionsPage', () => {\n    it('constructs options with default pagination', () => {\n      const options = { uuid: 'awiwi' };\n      expect(getOptionsPage(options)).toEqual({\n        uuid: 'awiwi',\n        per_page: 25,\n        page: 1,\n      });\n    });\n\n    it('overrides defaults when parameters are provided', () => {\n      expect(getOptionsPage({ uuid: 'awiwi' }, 10, 2)).toEqual({\n        uuid: 'awiwi',\n        per_page: 10,\n        page: 2,\n      });\n    });\n  });\n\n  describe('delay', () => {\n    it('delays execution by specified ms', async () => {\n      vi.useFakeTimers();\n      const promise = delay(1000);\n      vi.advanceTimersByTime(1000);\n      await expect(promise).resolves.toBeUndefined();\n      vi.useRealTimers();\n    });\n  });\n\n  describe('range', () => {\n    it('creates an array from start to end', () => {\n      expect(range(1, 5)).toEqual([1, 2, 3, 4]);\n    });\n  });\n\n  describe('asyncMap', () => {\n    it('applies an async function to each element in the array', async () => {\n      const numbers = [1, 2, 3];\n      const doubleAsync = async (n: number) => (n * 2) as unknown as Promise<ISbResult>;\n      const results = await asyncMap(numbers as unknown as RangeFn[], doubleAsync);\n      expect(results).toEqual([2, 4, 6]);\n    });\n  });\n\n  describe('flatMap', () => {\n    it('maps and flattens the array based on the provided function', () => {\n      const data = [\n        { id: 1, values: [10, 20] },\n        { id: 2, values: [30, 40] },\n      ];\n      const flattenValues = (item: { values: number[] }) => item.values;\n      const result = flatMap(data as unknown as ISbResult[], flattenValues);\n      expect(result).toEqual([10, 20, 30, 40]);\n    });\n  });\n\n  describe('stringify', () => {\n    it('stringifies simple objects', () => {\n      const params = { name: 'John', age: 30 };\n      const result = stringify(params);\n      expect(result).toBe('name=John&age=30');\n    });\n\n    it('handles arrays correctly', () => {\n      const params = { names: ['John', 'Jane'] };\n      const result = stringify(params, '', true);\n      expect(result).toBe('=John&=Jane');\n    });\n\n    it('handles undefined values', () => {\n      const params = { name: 'John', age: undefined };\n      const result = stringify(params);\n      expect(result).toBe('name=John');\n    });\n\n    it('handles null values', () => {\n      const params = { name: 'John', age: null };\n      const result = stringify(params);\n      expect(result).toBe('name=John');\n    });\n\n    it('handles null and undefined values', () => {\n      const params = { name: 'John', age: null, city: undefined, country: 'Italy' };\n      const result = stringify(params);\n      expect(result).toBe('name=John&country=Italy');\n    });\n\n    it('handles empty string values', () => {\n      const params = { name: 'John', age: null, city: undefined, country: '' };\n      const result = stringify(params);\n      expect(result).toBe('name=John&country=');\n    });\n\n    it('does not break when given an empty object', () => {\n      const params = {};\n      const result = stringify(params);\n      expect(result).toBe('');\n    });\n\n    it('does not break when given a null params', () => {\n      const result = stringify(null as any);\n      expect(result).toBe('');\n    });\n  });\n\n  describe('arrayFrom function', () => {\n    it('arrayFrom(undefined, (v, i) => i)) should be an empty array', () => {\n      expect(arrayFrom(undefined, (_, i) => i)).toEqual([]);\n    });\n\n    it('arrayFrom(0, (v, i) => i)) should be an empty array', () => {\n      expect(arrayFrom(0, (_, i) => i)).toEqual([]);\n    });\n\n    it('arrayFrom(2, () => 1) should be an array with 1 and 1', () => {\n      expect(arrayFrom(2, () => 1)).toEqual([1, 1]);\n    });\n\n    it('arrayFrom(2, (v, i) => v)) should be an array with undefined values', () => {\n      expect(arrayFrom(2, v => v)).toEqual([undefined, undefined]);\n    });\n\n    it('arrayFrom(2, (v, i) => i) should be an array with 0 and 1', () => {\n      expect(arrayFrom(2, (v, i) => i)).toEqual([0, 1]);\n    });\n  });\n\n  describe('getRegionURL', () => {\n    it('returns the EU API URL by default', () => {\n      expect(getRegionURL()).toBe('api.storyblok.com');\n      expect(getRegionURL('unknown')).toBe('api.storyblok.com'); // test for unrecognized region code\n    });\n\n    it('returns the US API URL when region code is \"us\"', () => {\n      expect(getRegionURL('us')).toBe('api-us.storyblok.com');\n    });\n\n    it('returns the CN API URL when region code is \"cn\"', () => {\n      expect(getRegionURL('cn')).toBe('app.storyblokchina.cn');\n    });\n\n    it('returns the AP API URL when region code is \"ap\"', () => {\n      expect(getRegionURL('ap')).toBe('api-ap.storyblok.com');\n    });\n\n    it('returns the CA API URL when region code is \"ca\"', () => {\n      expect(getRegionURL('ca')).toBe('api-ca.storyblok.com');\n    });\n  });\n\n  describe('escapeHTML', () => {\n    it('escapes HTML characters', () => {\n      const str = '<div>Test & \"more\" test</div>';\n      const escaped = escapeHTML(str);\n      expect(escaped).toBe(\n        '&lt;div&gt;Test &amp; &quot;more&quot; test&lt;/div&gt;',\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import type {\n  AsyncFn,\n  HtmlEscapes,\n  ISbResult,\n  ISbStoriesParams,\n} from './interfaces';\n\n// TODO: Revise this type, is it needed?\ninterface ISbParams extends ISbStoriesParams {\n  [key: string]: any;\n}\n\ntype ArrayFn = (...args: any) => void;\ntype FlatMapFn = (...args: any) => [] | any;\ntype RangeFn = (...args: any) => [];\n\n/**\n * Checks if a URL is a CDN URL\n * @param url - The URL to check\n * @returns boolean indicating if the URL is a CDN URL\n */\nexport const isCDNUrl = (url = ''): boolean => url.includes('/cdn/');\n\n/**\n * Gets pagination options for the API request\n * @param options - The base options\n * @param perPage - Number of items per page\n * @param page - Current page number\n * @returns Object with pagination options\n */\nexport const getOptionsPage = (\n  options: ISbStoriesParams,\n  perPage = 25,\n  page = 1,\n) => ({\n  ...options,\n  per_page: perPage,\n  page,\n});\n\n/**\n * Creates a promise that resolves after the specified milliseconds\n * @param ms - Milliseconds to delay\n * @returns Promise that resolves after the delay\n */\nexport const delay = (ms: number): Promise<void> =>\n  new Promise(res => setTimeout(res, ms));\n\n/**\n * Creates an array of specified length using a mapping function\n * @param length - Length of the array\n * @param func - Mapping function\n * @returns Array of specified length\n */\nexport const arrayFrom = (length = 0, func: ArrayFn) =>\n  Array.from({ length }, func);\n\n/**\n * Creates an array of numbers in the specified range\n * @param start - Start of the range\n * @param end - End of the range\n * @returns Array of numbers in the range\n */\nexport const range = (start = 0, end = start): Array<any> => {\n  const length = Math.abs(end - start) || 0;\n  const step = start < end ? 1 : -1;\n  return arrayFrom(length, (_, i: number) => i * step + start);\n};\n\n/**\n * Maps an array asynchronously\n * @param arr - Array to map\n * @param func - Async mapping function\n * @returns Promise resolving to mapped array\n */\nexport const asyncMap = async (arr: RangeFn[], func: AsyncFn) =>\n  Promise.all(arr.map(func));\n\n/**\n * Flattens an array using a mapping function\n * @param arr - Array to flatten\n * @param func - Mapping function\n * @returns Flattened array\n */\nexport const flatMap = (arr: ISbResult[] = [], func: FlatMapFn) =>\n  arr.map(func).reduce((xs, ys) => [...xs, ...ys], []);\n\n/**\n * Stringifies an object into a URL query string\n * @param params - Parameters to stringify\n * @param prefix - Prefix for nested keys\n * @param isArray - Whether the current level is an array\n * @returns Stringified query parameters\n */\nexport const stringify = (\n  params: ISbParams,\n  prefix?: string,\n  isArray?: boolean,\n): string => {\n  const pairs = [];\n  for (const key in params) {\n    if (!Object.prototype.hasOwnProperty.call(params, key)) {\n      continue;\n    }\n    const value = params[key];\n    if (value === null || value === undefined) {\n      continue;\n    }\n    const enkey = isArray ? '' : encodeURIComponent(key);\n    let pair;\n    if (typeof value === 'object') {\n      pair = stringify(\n        value,\n        prefix ? prefix + encodeURIComponent(`[${enkey}]`) : enkey,\n        Array.isArray(value),\n      );\n    }\n    else {\n      pair = `${\n        prefix ? prefix + encodeURIComponent(`[${enkey}]`) : enkey\n      }=${encodeURIComponent(value)}`;\n    }\n    pairs.push(pair);\n  }\n  return pairs.join('&');\n};\n\n/**\n * Gets the base URL for a specific region\n * @param regionCode - Region code (eu, us, cn, ap, ca)\n * @returns Base URL for the region\n */\nexport const getRegionURL = (regionCode?: string): string => {\n  const REGION_URLS = {\n    eu: 'api.storyblok.com',\n    us: 'api-us.storyblok.com',\n    cn: 'app.storyblokchina.cn',\n    ap: 'api-ap.storyblok.com',\n    ca: 'api-ca.storyblok.com',\n  } as const;\n\n  return REGION_URLS[regionCode as keyof typeof REGION_URLS] ?? REGION_URLS.eu;\n};\n\n/**\n * Escapes HTML special characters in a string\n * @param string - String to escape\n * @returns Escaped string\n */\nexport const escapeHTML = (string: string): string => {\n  const htmlEscapes = {\n    '&': '&amp;',\n    '<': '&lt;',\n    '>': '&gt;',\n    '\"': '&quot;',\n    '\\'': '&#39;',\n  } as HtmlEscapes;\n\n  const reUnescapedHtml = /[&<>\"']/g;\n  const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);\n\n  return string && reHasUnescapedHtml.test(string)\n    ? string.replace(reUnescapedHtml, chr => htmlEscapes[chr])\n    : string;\n};\n"
  },
  {
    "path": "tests/api/index.e2e.ts",
    "content": "import StoryblokClient from 'storyblok-js-client';\nimport { beforeEach, describe, expect, it } from 'vitest';\n\ndescribe('StoryblokClient', () => {\n  let client: StoryblokClient;\n\n  beforeEach(() => {\n    // Setup default mocks\n    client = new StoryblokClient({\n      accessToken: process.env.VITE_ACCESS_TOKEN,\n      cache: { type: 'memory', clear: 'auto' },\n    });\n  });\n  // TODO: Uncomment when we have a valid token\n  /* if (process.env.VITE_OAUTH_TOKEN) {\n      describe('management API', () => {\n        const spaceId = process.env.VITE_SPACE_ID\n        describe('should return all spaces', async () => {\n          const StoryblokManagement = new StoryblokClient({\n            oauthToken: process.env.VITE_OAUTH_TOKEN,\n          })\n          const result = await StoryblokManagement.getAll(\n            `spaces/${spaceId}/stories`\n          )\n          expect(result.length).toBeGreaterThan(0)\n        })\n      })\n  } */\n\n  describe('get function', () => {\n    it('get(\\'cdn/spaces/me\\') should return the space information', async () => {\n      const { data } = await client.get('cdn/spaces/me');\n      expect(data.space.id).toBe(Number(process.env.VITE_SPACE_ID));\n    });\n\n    it('get(\\'cdn/stories\\') should return all stories', async () => {\n      const { data } = await client.get('cdn/stories');\n      expect(data.stories.length).toBeGreaterThan(0);\n    });\n\n    it('get(\\'cdn/stories/testcontent-0\\' should return the specific story', async () => {\n      const { data } = await client.get('cdn/stories/testcontent-0');\n      expect(data.story.slug).toBe('testcontent-0');\n    });\n\n    it('get(\\'cdn/stories\\' { starts_with: testcontent-0 } should return the specific story', async () => {\n      const { data } = await client.get('cdn/stories', {\n        starts_with: 'testcontent-0',\n      });\n      expect(data.stories.length).toBe(1);\n    });\n\n    it('get(\\'cdn/stories/testcontent-draft\\', { version: \\'draft\\' }) should return the specific story draft', async () => {\n      const { data } = await client.get('cdn/stories/testcontent-draft', {\n        version: 'draft',\n      });\n      expect(data.story.slug).toBe('testcontent-draft');\n    });\n\n    it('get(\\'cdn/stories/testcontent-0\\', { version: \\'published\\' }) should return the specific story published', async () => {\n      const { data } = await client.get('cdn/stories/testcontent-0', {\n        version: 'published',\n      });\n      expect(data.story.slug).toBe('testcontent-0');\n    });\n\n    it('cdn/stories/testcontent-0 should resolve author relations', async () => {\n      const { data } = await client.get('cdn/stories/testcontent-0', {\n        resolve_relations: 'root.author',\n      });\n\n      expect(data.story.content.author[0].slug).toBe('edgar-allan-poe');\n    });\n\n    it('get(\\'cdn/stories\\', { by_slugs: \\'folder/*\\' }) should return the specific story', async () => {\n      const { data } = await client.get('cdn/stories', {\n        by_slugs: 'folder/*',\n      });\n      expect(data.stories.length).toBeGreaterThan(0);\n    });\n  });\n\n  describe('getAll function', () => {\n    it('getAll(\\'cdn/stories\\') should return all stories', async () => {\n      const result = await client.getAll('cdn/stories', {});\n      expect(result.length).toBeGreaterThan(0);\n    });\n\n    it('getAll(\\'cdn/stories\\') should return all stories with filtered results', async () => {\n      const result = await client.getAll('cdn/stories', {\n        starts_with: 'testcontent-0',\n      });\n      expect(result.length).toBe(1);\n    });\n\n    it('getAll(\\'cdn/stories\\', filter_query: { __or: [{ category: { any_in_array: \\'Category 1\\' } }, { category: { any_in_array: \\'Category 2\\' } }]}) should return all stories with the specific filter applied', async () => {\n      const result = await client.getAll('cdn/stories', {\n        filter_query: {\n          __or: [\n            { category: { any_in_array: 'Category 1' } },\n            { category: { any_in_array: 'Category 2' } },\n          ],\n        },\n      });\n      expect(result.length).toBeGreaterThan(0);\n    });\n\n    it('getAll(\\'cdn/stories\\', {by_slugs: \\'folder/*\\'}) should return all stories with the specific filter applied', async () => {\n      const result = await client.getAll('cdn/stories', {\n        by_slugs: 'folder/*',\n      });\n      expect(result.length).toBeGreaterThan(0);\n    });\n\n    it('getAll(\\'cdn/links\\') should return all links', async () => {\n      const result = await client.getAll('cdn/links', {});\n      expect(result.length).toBeGreaterThan(0);\n    });\n  });\n\n  describe('caching', () => {\n    it('get(\\'cdn/spaces/me\\') should not be cached', async () => {\n      const provider = client.cacheProvider();\n      await provider.flush();\n      await client.get('cdn/spaces/me');\n      expect(Object.values(provider.getAll()).length).toBe(0);\n    });\n\n    it('get(\\'cdn/stories\\') should be cached when is a published version', async () => {\n      const cacheVersion = client.cacheVersion();\n\n      await client.get('cdn/stories');\n\n      expect(cacheVersion).not.toBe(undefined);\n\n      const newCacheVersion = client.cacheVersion();\n\n      await client.get('cdn/stories');\n\n      expect(newCacheVersion).toBe(client.cacheVersion());\n\n      await client.get('cdn/stories');\n\n      expect(newCacheVersion).toBe(client.cacheVersion());\n    });\n  });\n});\n"
  },
  {
    "path": "tests/utils.ts",
    "content": "export function headersToObject(headers: Headers) {\n  const obj: { [key: string]: string } = {};\n  for (const [key, value] of headers.entries()) {\n    obj[key] = value;\n  }\n  return obj;\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@tsconfig/recommended/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"commonjs\",\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationDir\": \"dist/types\",\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"skipLibCheck\": true\n  },\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Recommended\",\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\", \"./src/**/*.test.ts\", \"./src/**/*.spec.ts\"]\n}\n"
  },
  {
    "path": "vite.build.mjs",
    "content": "import { fileURLToPath } from 'node:url';\nimport { resolve } from 'node:path';\nimport { build } from 'vite';\n\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\n\nlet firstRunCounter = 0;\nconst bundles = [\n  {\n    entry: 'entry.esm.ts',\n    formats: ['es'],\n    fileName: 'index',\n  },\n  {\n    entry: 'entry.umd.ts',\n    formats: ['umd'],\n    name: 'StoryblokJSClient',\n    fileName: 'index',\n  },\n]\n\n;(async () => {\n  for (const bundle of bundles) {\n    await build({\n      configFile: 'vite.config.ts',\n      build: {\n        lib: {\n          entry: resolve(__dirname, 'src', bundle.entry),\n          formats: bundle.formats,\n          name: bundle.name,\n          fileName: bundle.fileName,\n        },\n        emptyOutDir: !firstRunCounter++,\n      },\n      define: {\n        'process.env': {\n          npm_package_version: process.env.npm_package_version,\n        },\n      },\n    });\n  }\n})();\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { lightGreen } from 'kolorist';\nimport pkg from './package.json';\nimport banner from 'vite-plugin-banner';\nimport { defineConfig, type Plugin } from 'vitest/config';\nimport dts from 'vite-plugin-dts';\n\n// eslint-disable-next-line no-console\nconsole.log(`${lightGreen('Storyblok JS Client')} v${pkg.version}`);\n\nexport default defineConfig(() => ({\n  plugins: [\n    dts({\n      insertTypesEntry: true,\n      outDir: 'dist/types',\n    }),\n    banner({\n      content: `/**\\n * name: ${pkg.name}\\n * (c) ${new Date().getFullYear()}\\n * description: ${pkg.description}\\n * author: ${pkg.author}\\n */`,\n    }),\n  ] as Plugin[],\n  test: {\n    include: ['./src/**/*.test.ts'],\n    coverage: {\n      include: ['src'],\n      reporter: ['text', 'json', 'html'],\n      reportsDirectory: './tests/unit/coverage',\n    },\n  },\n}));\n"
  },
  {
    "path": "vitest.config.e2e.ts",
    "content": "import { defineConfig } from 'vite';\nimport path from 'node:path';\n\nexport default defineConfig({\n  test: {\n    include: ['./tests/**/*.e2e.ts'],\n  },\n  resolve: {\n    alias: {\n      'storyblok-js-client': path.resolve(__dirname, 'dist'),\n    },\n  },\n});\n"
  }
]