From fd20741b496a37d54981be5485ca9218126fc25a Mon Sep 17 00:00:00 2001 From: Caven Date: Sun, 4 Feb 2024 15:29:51 +0800 Subject: [PATCH] refactor: rewrite js sdk (#48) * refactor: rewrite js sdk * feat: integrate comprehensive JS SDK rewrite Merge includes a complete overhaul of the JavaScript SDK, introducing full type-safety, tree-shakeable modules, and deprecated legacy SDK classes and methods. BREAKING CHANGE: The SDK rewrite introduces breaking changes to the public API. Users are recommended to migrate to the new API functions. Resolves: #35, #29, #28, #25, #22, #19 * chore: add pre-commit and commit-msg git hooks --------- Co-authored-by: Branko Conjic --- .changeset/config.json | 18 +- .changeset/modern-laws-hang.md | 52 + .env.example | 3 + .eslintrc.cjs | 58 + .github/changeset-version.js | 11 +- .github/workflows/release.yml | 35 +- .prettierignore | 5 +- CONTRIBUTING.md | 162 ++ README.md | 1632 +------------ bun.lockb | Bin 0 -> 234930 bytes commitlint.config.ts | 23 + examples/index.ts | 13 + package.json | 71 +- pnpm-lock.yaml | 2837 ----------------------- prettier.config.js | 1 + shims.d.ts | 7 + src/_deprecated/LemonSqueezy.ts | 1726 ++++++++++++++ src/_deprecated/index.ts | 1 + src/{ => _deprecated}/types/api.ts | 69 +- src/{ => _deprecated}/types/methods.ts | 29 +- src/checkouts/index.ts | 119 + src/checkouts/types.ts | 509 ++++ src/customers/index.ts | 142 ++ src/customers/types.ts | 122 + src/discountRedemptions/index.ts | 51 + src/discountRedemptions/types.ts | 69 + src/discounts/index.ts | 134 ++ src/discounts/types.ts | 181 ++ src/files/index.ts | 40 + src/files/types.ts | 84 + src/index.ts | 1557 ++----------- src/internal/fetch/index.ts | 76 + src/internal/fetch/types.ts | 28 + src/internal/index.ts | 3 + src/internal/setup/index.ts | 14 + src/internal/setup/types.ts | 13 + src/internal/utils/index.ts | 112 + src/internal/utils/kv.ts | 21 + src/license/index.ts | 75 + src/license/types.ts | 44 + src/licenseKeyInstances/index.ts | 50 + src/licenseKeyInstances/types.ts | 53 + src/licenseKeys/index.ts | 84 + src/licenseKeys/types.ts | 151 ++ src/orderItems/index.ts | 50 + src/orderItems/types.ts | 87 + src/orders/index.ts | 49 + src/orders/types.ts | 241 ++ src/prices/index.ts | 48 + src/prices/types.ts | 196 ++ src/products/index.ts | 48 + src/products/types.ts | 98 + src/stores/index.ts | 46 + src/stores/types.ts | 103 + src/subscriptionInvoices/index.ts | 53 + src/subscriptionInvoices/types.ts | 204 ++ src/subscriptionItems/index.ts | 99 + src/subscriptionItems/types.ts | 73 + src/subscriptions/index.ts | 113 + src/subscriptions/types.ts | 323 +++ src/types/common.ts | 6 + src/types/index.ts | 3 + src/types/iso.ts | 437 ++++ src/types/response/data.ts | 21 + src/types/response/index.ts | 20 + src/types/response/links.ts | 7 + src/types/response/meta.ts | 21 + src/types/response/params.ts | 9 + src/types/response/relationships.ts | 62 + src/usageRecords/index.ts | 84 + src/usageRecords/types.ts | 80 + src/users/index.ts | 13 + src/users/types.ts | 40 + src/variants/index.ts | 49 + src/variants/types.ts | 167 ++ src/webhooks/index.ts | 134 ++ src/webhooks/types.ts | 100 + test/checkouts/index.test.ts | 990 ++++++++ test/customers/index.test.ts | 689 ++++++ test/discountRedemptions/index.test.ts | 271 +++ test/discounts/index.test.ts | 471 ++++ test/files/index.test.ts | 252 ++ test/index.test.ts | 110 + test/internal/configure.test.ts | 17 + test/internal/fetch.test.ts | 97 + test/internal/utils.test.ts | 82 + test/license/index.test.ts | 613 +++++ test/licenseKeyInstances/index.test.ts | 223 ++ test/licenseKeys/index.test.ts | 627 +++++ test/orderItems/index.test.ts | 290 +++ test/orders/index.test.ts | 498 ++++ test/prices/index.test.ts | 272 +++ test/products/index.test.ts | 262 +++ test/stores/index.test.ts | 257 ++ test/subscriptionInvoices/index.test.ts | 390 ++++ test/subscriptionItems/index.test.ts | 363 +++ test/subscriptions/index.test.ts | 664 ++++++ test/usageRecords/index.test.ts | 310 +++ test/users/index.test.ts | 72 + test/variants/index.test.ts | 333 +++ test/webhooks/index.test.ts | 400 ++++ tsconfig.json | 19 +- tsup.config.ts | 13 +- 103 files changed, 16355 insertions(+), 5799 deletions(-) create mode 100644 .changeset/modern-laws-hang.md create mode 100644 .env.example create mode 100644 .eslintrc.cjs create mode 100644 CONTRIBUTING.md create mode 100755 bun.lockb create mode 100644 commitlint.config.ts create mode 100644 examples/index.ts delete mode 100644 pnpm-lock.yaml create mode 100644 shims.d.ts create mode 100644 src/_deprecated/LemonSqueezy.ts create mode 100644 src/_deprecated/index.ts rename src/{ => _deprecated}/types/api.ts (98%) rename src/{ => _deprecated}/types/methods.ts (96%) create mode 100644 src/checkouts/index.ts create mode 100644 src/checkouts/types.ts create mode 100644 src/customers/index.ts create mode 100644 src/customers/types.ts create mode 100644 src/discountRedemptions/index.ts create mode 100644 src/discountRedemptions/types.ts create mode 100644 src/discounts/index.ts create mode 100644 src/discounts/types.ts create mode 100644 src/files/index.ts create mode 100644 src/files/types.ts create mode 100644 src/internal/fetch/index.ts create mode 100644 src/internal/fetch/types.ts create mode 100644 src/internal/index.ts create mode 100644 src/internal/setup/index.ts create mode 100644 src/internal/setup/types.ts create mode 100644 src/internal/utils/index.ts create mode 100644 src/internal/utils/kv.ts create mode 100644 src/license/index.ts create mode 100644 src/license/types.ts create mode 100644 src/licenseKeyInstances/index.ts create mode 100644 src/licenseKeyInstances/types.ts create mode 100644 src/licenseKeys/index.ts create mode 100644 src/licenseKeys/types.ts create mode 100644 src/orderItems/index.ts create mode 100644 src/orderItems/types.ts create mode 100644 src/orders/index.ts create mode 100644 src/orders/types.ts create mode 100644 src/prices/index.ts create mode 100644 src/prices/types.ts create mode 100644 src/products/index.ts create mode 100644 src/products/types.ts create mode 100644 src/stores/index.ts create mode 100644 src/stores/types.ts create mode 100644 src/subscriptionInvoices/index.ts create mode 100644 src/subscriptionInvoices/types.ts create mode 100644 src/subscriptionItems/index.ts create mode 100644 src/subscriptionItems/types.ts create mode 100644 src/subscriptions/index.ts create mode 100644 src/subscriptions/types.ts create mode 100644 src/types/common.ts create mode 100644 src/types/index.ts create mode 100644 src/types/iso.ts create mode 100644 src/types/response/data.ts create mode 100644 src/types/response/index.ts create mode 100644 src/types/response/links.ts create mode 100644 src/types/response/meta.ts create mode 100644 src/types/response/params.ts create mode 100644 src/types/response/relationships.ts create mode 100644 src/usageRecords/index.ts create mode 100644 src/usageRecords/types.ts create mode 100644 src/users/index.ts create mode 100644 src/users/types.ts create mode 100644 src/variants/index.ts create mode 100644 src/variants/types.ts create mode 100644 src/webhooks/index.ts create mode 100644 src/webhooks/types.ts create mode 100644 test/checkouts/index.test.ts create mode 100644 test/customers/index.test.ts create mode 100644 test/discountRedemptions/index.test.ts create mode 100644 test/discounts/index.test.ts create mode 100644 test/files/index.test.ts create mode 100644 test/index.test.ts create mode 100644 test/internal/configure.test.ts create mode 100644 test/internal/fetch.test.ts create mode 100644 test/internal/utils.test.ts create mode 100644 test/license/index.test.ts create mode 100644 test/licenseKeyInstances/index.test.ts create mode 100644 test/licenseKeys/index.test.ts create mode 100644 test/orderItems/index.test.ts create mode 100644 test/orders/index.test.ts create mode 100644 test/prices/index.test.ts create mode 100644 test/products/index.test.ts create mode 100644 test/stores/index.test.ts create mode 100644 test/subscriptionInvoices/index.test.ts create mode 100644 test/subscriptionItems/index.test.ts create mode 100644 test/subscriptions/index.test.ts create mode 100644 test/usageRecords/index.test.ts create mode 100644 test/users/index.test.ts create mode 100644 test/variants/index.test.ts create mode 100644 test/webhooks/index.test.ts diff --git a/.changeset/config.json b/.changeset/config.json index 8be6933..53a79d4 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "lmsqueezy/lemonsqueezy.js" }], - "commit": false, - "fixed": [], - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "lmsqueezy/lemonsqueezy.js" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] } diff --git a/.changeset/modern-laws-hang.md b/.changeset/modern-laws-hang.md new file mode 100644 index 0000000..868f817 --- /dev/null +++ b/.changeset/modern-laws-hang.md @@ -0,0 +1,52 @@ +--- +"@lemonsqueezy/lemonsqueezy.js": major +--- + +BREAKING CHANGE: Completely rewritten the JS SDK for full type-safety and tree-shakeability. + +## Notes: + +- **Bun**: Transitioned to Bun for repo management. + +- **Type-safe**: Written in TypeScript and documented with TSDoc. + +- **Tree-shakeable**: Use only functions that you need. + +- **Improved docs**: Added detailed Wiki pages on how to use the new SDK functions. + +- **Deprecate old SDK classes and methods**: The deprecated methods and the LemonSqueezy class will be removed with the next major release. + +- **Unit tests**: Introduces comprehensive unit tests for all functions. + +- **Improved repo management**: Transitioned to Bun, adopted Conventional Commits convention, husky, Prettier, ESLint and other tools for better repo management. + +## Fixes: + +This release fixes the following issues. + +- #35 +- #29 +- #28 +- #25 +- #22 +- #19 + +## How to upgrade + +Use the new setup function to initialize the SDK with your API key. + +```tsx +lemonSqueezySetup({ apiKey }); +``` + +Import functions from the SDK and use them in your application. + +```tsx +const { data, error, statusCode } = await getAuthenticatedUser(); +``` + +For more information, see [API Reference](https://docs.lemonsqueezy.com/api) and [Functions Usage Wiki](https://github.com/lmsqueezy/lemonsqueezy.js/wiki). + +## Credits + +🎉 A massive thanks to @heybrostudio for their awesome work and contributions that led to this release. \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..876b15c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +LEMON_SQUEEZY_API_KEY=Your_Lemon_Squeezy_API_Key +LEMON_SQUEEZY_STORE_ID=Your_Lemon_Squeezy_Store_ID +LEMON_SQUEEZY_LICENSE_KEY=Your_Lemon_Squeezy_Product_License_Key \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..5a57722 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,58 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + env: { + browser: false, + es2021: true, + node: true, + }, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + overrides: [ + { + env: { + node: true, + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", + }, + ignorePatterns: [ + // Ignore dotfiles + ".*.js", + ".*.cjs", + ".*.md", + ".*.mdx", + "/dist", + ], + plugins: ["@typescript-eslint"], + rules: { + "no-empty": "off", + "no-unused-vars": "off", + + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + }, + ], + + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + }, +}; diff --git a/.github/changeset-version.js b/.github/changeset-version.js index c81f85f..15d64f2 100644 --- a/.github/changeset-version.js +++ b/.github/changeset-version.js @@ -1,12 +1,7 @@ -// ORIGINALLY FROM CLOUDFLARE WRANGLER: -// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js - -import { exec } from "child_process"; - // This script is used by the `release.yml` workflow to update the version of the packages being released. // The standard step is only to run `changeset version` but this does not update the package-lock.json file. -// So we also run `npm install`, which does this update. +// So we also run `bun install`, which does this update. // This is a workaround until this is handled automatically by `changeset version`. // See https://github.com/changesets/changesets/issues/421. -exec("npx changeset version"); -exec("pnpm install"); +Bun.spawnSync("bun", ["changeset", "version"]); +Bun.spawnSync("bun", ["install"]); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e9efa7..3ef92d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,37 +19,14 @@ jobs: with: fetch-depth: 0 - - uses: pnpm/action-setup@v2 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - cache: "pnpm" - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v3 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + - name: Install Bun + uses: oven-sh/setup-bun@v1 - name: Install dependencies - run: pnpm install + run: bun install - name: Build the package - run: pnpm build + run: bun build - name: Publish to NPM id: changesets @@ -57,8 +34,8 @@ jobs: with: commit: "chore(release): version packages" title: "chore(release): version packages" - version: node .github/changeset-version.js - publish: npx changeset publish + version: bun changeset version && bun install + publish: bun changeset publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.prettierignore b/.prettierignore index e2cba30..76e1467 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ **/*.yaml /coverage -dist/ -src/ -.changeset/ \ No newline at end of file +/dist +/.changeset \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9fb264b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,162 @@ +# Contributing to lemonsqueezy.js + +👋 Hey there! We're thrilled that you're interested in contributing to +**lemonsqueezy.js**. Before submitting your contribution, please take a moment to read through this guide. + +This repo is managed with [Bun](https://bun.sh/). We recommend reading the [Bun documentation](https://bun.sh/docs) to learn more about it. + +## Tooling and Technologies + +We utilize a variety of tools to ensure code quality, consistency, and smooth development processes. + +- **[Bun](https://bun.sh/)**: for managing the repo and testing. + +- **[Prettier](https://prettier.io/)**: for code formatting. Our codebase adheres to the configuration specified in `prettier.config.js`. + +- **[ESLint](https://eslint.org/)**: for code linting. Make sure to check and fix any linting issues before submitting your code. Our codebase linting rules are defined in `.eslintrc.cjs`. + +- **[husky](https://typicode.github.io/husky/#/)**: for Git hooks. It ensures that linters, and other checks are passed before commits and pushes. + +- **[tsup](https://tsup.egoist.dev/)**: for bundling the library files. We bundle both ESM and CJS versions of the library. + +- **[Changesets](https://github.com/atlassian/changesets)**: for changelog generation and release management. + +## Commit Convention + +Our project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. + +When preparing your commits for a Pull Request, ensure they adhere to our commit message format: `type(scope): description`. + +### Types of Commits + +Your commits should fall into one of the following categories: + +- `feat` (or `feature`): Introduces new code or functionality to the project. + +- `fix`: Addresses and resolves a bug. Linking to an issue if available is highly encouraged. + +- `refactor`: Code changes that neither fix a bug nor add a feature, but improve the existing codebase. + +- `docs`: Updates or additions to documentation, such as README files, usage guides, etc. + +- `build`: Changes affecting the build system, including dependency updates and additions. + +- `test`: Modifications involving tests, including adding new tests or refining existing ones. + +- `ci`: Adjustments to our continuous integration setup, like GitHub Actions or other CI tools. + +- `chore`: General maintenance and organizational tasks that don't fit other categories. + +For example, a commit message might look like: `feat: add activateLicense function`. + +## Setup + +Make sure you have the latest version of [Bun](https://bun.sh/) installed in your system. + +Clone this repo to your local computer and install the dependencies. + +```bash +bun install +``` + +To run the development version, you can start it locally by: + +```bash +bun run dev +``` + +### Configure .env + +- Copy `.env.example` to `.env` (`.env` has been added to `.gitignore`). + +- Configure the three environment variables in the `.env` file.: + - `LEMON_SQUEEZY_API_KEY` + - `LEMON_SQUEEZY_STORE_ID` + - `LEMON_SQUEEZY_LICENSE_KEY` + +### Run unit tests + +To run all the test, you can start it locally by: + +```bash +bun test +``` + +To run a specific test, you can start it locally by: + +```bash +bun test test/.ts +``` + +## Pull Request Guidelines + +### Branch Workflow + +- Develop in dedicated branches, not directly on the `main` branch. + +- Use descriptive names for your branches. Make sure the branch name adheres to the format `[type/scope]`, like `feat/button-enhancement` or `docs/update-readme`. + +### Adding New Features + +- Provide tests for new features. + +- Discuss new features in an issue or discussion topic before coding. + +### Fixing Bugs + +- Describe the bug in detail in your PR. + +- Include steps to reproduce or a live demo if possible. + +- Link the issue your PR fixes, using the format `fix #issue_number`. + +Remember, clear and detailed PRs help us efficiently review and integrate your contributions! + +## Creating a Pull Request (PR) + +1. **Fork and Clone**: Begin by forking the main repository and cloning your fork. + +2. **Branching**: Create a new branch off `main` using the format `[type/scope]`, like `feat/button-enhancement` or `docs/update-readme`. The `type` should align with conventional commit types. + +3. **Development**: Make your changes and commit them adhering to the [commit guidelines](#commit-convention). Use `bun run typecheck` to test your changes. + +4. **Document Changes**: Run `bun changeset` for a detailed change description. For minor updates (like CI changes), use `bun changeset add --empty`. + +5. **Submit PR**: Push your branch and open a PR to the `main` branch of the main repository. Ensure all tests pass and the package builds correctly. + +## Project Structure + +### Function Folder + +The functions of a category should be placed in a folder. + +``` +src + checkouts/ - the functions for `checkouts` + customers/ - the functions for `customers` + internal/ - the some `internal` methods + .../ - the other functions folders +``` + +A function folder typically contains these 2 files: + +``` +index.ts - function source code itself +types.ts - type definition +``` + +### Unit Test Folder + +Test cases for a category of functions should be placed in a folder. There is a one-to-one correspondence with the functions folder. + +``` +test + checkouts/ - Unit tests for `checkouts` functions + customers/ - Unit tests for `customers` functions + internal/ - Unit tests for `internal` functions + .../ - Unit Tests for other functions +``` + +## Thanks + +Thanks for all your contributions and efforts towards improving this project! You are awesome ✨! diff --git a/README.md b/README.md index b74d959..8e367c5 100644 --- a/README.md +++ b/README.md @@ -1,1557 +1,139 @@ # The official Lemon Squeezy JavaScript SDK -[![](https://img.shields.io/npm/v/@lemonsqueezy/lemonsqueezy.js?style=plastic)](https://www.npmjs.com/package/@lemonsqueezy/lemonsqueezy.js) [![](https://img.shields.io/npm/dw/@lemonsqueezy/lemonsqueezy.js?style=plastic)](https://www.npmjs.com/package/@lemonsqueezy/lemonsqueezy.js) +[![NPM Version](https://img.shields.io/npm/v/%40lemonsqueezy%2Flemonsqueezy.js?label=&color=%230d9488)](https://www.npmjs.com/package/@lemonsqueezy/lemonsqueezy.js) +![](https://img.shields.io/npm/dw/@lemonsqueezy/lemonsqueezy.js) +[![Functions usage](https://img.shields.io/badge/Docs-%237c3aed)](https://github.com/lemonsqueezy/lemonsqueezy.js/wiki) +![APIs Count](https://img.shields.io/badge/56_Functions-%232563eb) ## Introduction This is the official JavaScript SDK for [Lemon Squeezy](https://lemonsqueezy.com), helping make it easy to incorporate billing into your JavaScript application. -Now with full TypeScript support. +- Read [API Reference](https://docs.lemonsqueezy.com/api) to understand how the Lemon Squeezy API works. -Please read the [API introduction page](https://docs.lemonsqueezy.com/api) to understand how the API works. +- Visit [Wiki page](https://docs.lemonsqueezy.com/api-reference) for function usage. -## Installation - -### Install the package - -Install with `npm install @lemonsqueezy/lemonsqueezy.js` - -### Create an API key - -Create a new API key from [Settings > API](https://app.lemonsqueezy.com/settings/api) in your Lemon Squeezy dashboard. - -Add this API key into your project, for example as `LEMONSQUEEZY_API_KEY` in your `.env` file. - -You can test the API/SDK when in [test mode](https://docs.lemonsqueezy.com/help/getting-started/test-mode) so you can build a full integration without making live transactions. - -## Usage - -### Basic usage - -```javascript -import { LemonSqueezy } from "@lemonsqueezy/lemonsqueezy.js"; -const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY); - -const products = await ls.getProducts(); -``` +## Features -Parameters for requests should be passed in an object. For list methods, these parameters are used for filtering and for list pagination. For create and update methods, these parameters contain the values for the request. +- Type-safe: Written in [TypeScript](https://www.typescriptlang.org/) and documented with [TSDoc](https://github.com/microsoft/tsdoc). +- Tree-shakeable: Use only functions that you need. See [bundle size](#bundle-size). -```javascript -const subscriptions = await ls.getSubscriptions({ storeId: 123, perPage: 50 }); +## Installation -const subscription = await ls.getSubscription({ - id: 123, - include: ["subscription-invoices"], -}); +### Install the package -const subscription = await ls.cancelSubscription({ id: 123 }); +```bash +# bun +bun install @lemonsqueezy/lemonsqueezy.js ``` -### Including related resources - -You can use `include` in every "read" method to pull in [related resources](https://docs.lemonsqueezy.com/api#including-related-resources) (works for both individual and list methods). - -Note: In v1.0.3 and lower, `include` was a string of object names. Now it should be an array of strings. - -```javascript -const product = await ls.getProduct({ - id: 123, - include: ["store", "variants"], -}); +```bash +# pnpm +pnpm install @lemonsqueezy/lemonsqueezy.js ``` -### Pagination - -Endpoints that return a list of results can be paged using optional `page` and `perPage` values. -If `perPage` is omitted, the API returns the default of 10 results per page. -`perPage` should be a value between 1 and 100. - -```javascript -// Querying a list of orders for store #3, 50 records per page, page 2, including store and customer related resources -const order = await ls.getOrders({ - storeId: 3, - perPage: 50, - page: 2, - include: ["store", "customer"], -}); +```bash +# npm +npm install @lemonsqueezy/lemonsqueezy.js ``` -### Looping lists - -You can also use `page` and `perPage` to loop lists of results. - -"List" method responses contain a `meta.page` object, which describes the pagination of your request. - -```json -{ - "meta": { - "page": { - "currentPage": 1, - "from": 1, - "lastPage": 16, - "perPage": 10, - "to": 10, - "total": 154 - } - }, - ... -} -``` +### Create an API key -In this example, you can use the `lastPage` value to check if you are on the last page of results. +Create a new API key from [Settings > API](https://app.lemonsqueezy.com/settings/api) in your Lemon Squeezy dashboard. -```javascript -let hasNextPage = true; -let perPage = 100; -let page = 1; -let variants = []; -while (hasNextPage) { - const resp = await ls.getVariants({ perPage, page }); +Add this API key into your project, for example as `LEMONSQUEEZY_API_KEY` in your `.env` file. - variants = variants.concat(resp["data"]); +### Using the API in test mode - if (resp.meta.page.lastPage > page) { - page += 1; - } else { - hasNextPage = false; - } -} -``` +You can build and test a full API integration with Lemon Squeezy using test mode. -### Handling errors +Any API keys created in test mode will interact with your test mode store data. -Each method will throw an exception if there are issues with the request. JSON will be returned containing error details. +When you are ready to go live with your integration, make sure to create an API key in live mode and use that in your production application. -Use `try { ... } catch { ... }` to access this object. Error messages will be available in a list in `errors`. +## Usage -```javascript -// "something" is not a valid value for `include` so this request will return an error -try { - const subscriptions = await ls.getSubscriptions({ include: ["something"] }); -} catch (err) { - // `err` is an object like this: - // { - // "jsonapi": { - // "version": "1.0" - // } - // "errors": [ - // { - // "detail": "Include path something is not allowed.", - // "source": { - // "parameter": "include" - // }, - // "status": "400", - // "title": "Invalid Query Parameter" - // } - // ] - // } -} -``` +```tsx +lemonSqueezySetup({ apiKey }); + +const { data, error, statusCode } = await getAuthenticatedUser(); + +console.log({ data, error, statusCode }); +``` + +For more functions usage, see [Wiki](https://github.com/lemonsqueezy/lemonsqueezy.js/wiki). + +## Bundle size + +
+ Click to view + +| export | min+brotli | +| :------------------------------ | ---------: | +| LemonSqueezy (deprecated) | 1.87 kB | +| createDiscount | 928 B | +| createCheckout | 821 B | +| listWebhooks | 770 B | +| listSubscriptionInvoices | 767 B | +| listDiscountRedemptions | 766 B | +| updateSubscription | 766 B | +| listLicenseKeyInstances | 765 B | +| listSubscriptionItems | 765 B | +| listLicenseKeys | 764 B | +| listOrderItems | 764 B | +| listUsageRecords | 764 B | +| listCheckouts | 763 B | +| listFiles | 762 B | +| listOrders | 762 B | +| listPrices | 762 B | +| listProducts | 762 B | +| listStores | 762 B | +| listSubscriptions | 762 B | +| listCustomers | 761 B | +| listDiscounts | 761 B | +| listVariants | 759 B | +| createWebhook | 744 B | +| updateLicenseKey | 737 B | +| updateWebhook | 728 B | +| deactivateLicense | 699 B | +| validateLicense | 699 B | +| activateLicense | 698 B | +| createUsageRecord | 652 B | +| getLicenseKeyInstance | 640 B | +| getDiscountRedemption | 639 B | +| getSubscriptionInvoice | 636 B | +| getLicenseKey | 634 B | +| getOrderItem | 633 B | +| getUsageRecord | 632 B | +| getWebhook | 632 B | +| getCheckout | 629 B | +| getSubscription | 629 B | +| getStore | 628 B | +| getCustomer | 627 B | +| getDiscount | 627 B | +| getFile | 627 B | +| getOrder | 627 B | +| getPrice | 627 B | +| getProduct | 627 B | +| getVariant | 627 B | +| updateSubscriptionItem | 621 B | +| createCustomer | 616 B | +| archiveCustomer | 615 B | +| updateCustomer | 609 B | +| getSubscriptionItemCurrentUsage | 592 B | +| cancelSubscription | 587 B | +| deleteWebhook | 587 B | +| deleteDiscount | 585 B | +| getSubscriptionItem | 583 B | +| getAuthenticatedUser | 529 B | +| lemonSqueezySetup | 106 B | + +
## Notes -Do not use this package directly in the browser. as this will expose your API key. This would give anyone full API access to your Lemon Squeezy account and store(s). - ---- - -## Methods - -- [getUser()](#getuser) -- [getStores()](#getstoresparameters) -- [getStore()](#getstoreparameters) -- [getProducts()](#getproductsparameters) -- [getProduct()](#getproductparameters) -- [getVariants()](#getvariantsparameters) -- [getVariant()](#getvariantparameters) -- [getPrices()](#getpricesparameters) -- [getPrice()](#getpriceparameters) -- [getCheckouts()](#getcheckoutsparameters) -- [getCheckout()](#getcheckoutparameters) -- [createCheckout()](#createcheckoutparameters) -- [getCustomers()](#getcustomersparameters) -- [getCustomer()](#getcustomerparameters) -- [getOrders()](#getordersparameters) -- [getOrder()](#getorderparameters) -- [getFiles()](#getfilesparameters) -- [getFile()](#getfileparameters) -- [getOrderItems()](#getorderitemsparameters) -- [getOrderItem()](#getorderitemparameters) -- [getSubscriptions()](#getsubscriptionsparameters) -- [getSubscription()](#getsubscriptionparameters) -- [updateSubscription()](#updatesubscriptionparameters) -- [cancelSubscription()](#cancelsubscriptionparameters) -- [resumeSubscription()](#resumesubscriptionparameters) -- [pauseSubscription()](#pausesubscriptionparameters) -- [unpauseSubscription()](#unpausesubscriptionparameters) -- [getSubscriptionInvoices()](#getsubscriptioninvoicesparameters) -- [getSubscriptionInvoice()](#getsubscriptioninvoiceparameters) -- [getSubscriptionItems()](#getsubscriptionitemsparameters) -- [getSubscriptionItem()](#getsubscriptionitemparameters) -- [updateSubscriptionItem()](#updatesubscriptionitemparameters) -- [getSubscriptionItemUsage()](#getsubscriptionitemusageparameters) -- [getUsageRecords()](#getusagerecordsparameters) -- [getUsageRecord()](#getusagerecordparameters) -- [createUsageRecord()](#createusagerecordparameters) -- [getDiscounts()](#getdiscountsparameters) -- [getDiscount()](#getdiscountparameters) -- [createDiscount()](#creatediscountparameters) -- [deleteDiscount()](#deletediscountparameters) -- [getDiscountRedemptions()](#getdiscountredemptionsparameters) -- [getDiscountRedemption()](#getdiscountredemptionparameters) -- [getLicenseKeys()](#getlicensekeysparameters) -- [getLicenseKey()](#getlicensekeyparameters) -- [getLicenseKeyInstances()](#getlicensekeyinstancesparameters) -- [getLicenseKeyInstance()](#getlicensekeyinstanceparameters) -- [getWebhooks()](#getwebhooksparameters) -- [getWebhook()](#getwebhookparameters) -- [createWebhook()](#createwebhookparameters) -- [updateWebhook()](#updatewebhookparameters) -- [deleteWebhook()](#deletewebhookparameters) - ---- - -### getUser() - -Get the current user. - -Returns a [User object](https://docs.lemonsqueezy.com/api/users). - -[API reference](https://docs.lemonsqueezy.com/api/users#retrieve-the-authenticated-user). - -#### Parameters - -None. - -#### Example - -```javascript -const user = await ls.getUser(); -``` - ---- - -### getStores(parameters) - -Get the current user's stores. - -Returns a list of [Store objects](https://docs.lemonsqueezy.com/api/stores). - -[API reference](https://docs.lemonsqueezy.com/api/stores#list-all-stores). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `perPage` | number | | `10` | | -| `page` | number | | `1` | | -| `include` | string | | | Comma-separated list of object names:
  • products
  • discounts
  • license-keys
  • subscriptions
  • webhooks
| - -#### Example - -```javascript -const stores = await ls.getStores(); - -const stores = await ls.getStores({ include: "products" }); -``` - ---- - -### getStore(parameters) - -Get a store. - -Returns a [Store object](https://docs.lemonsqueezy.com/api/stores). - -[API reference](https://docs.lemonsqueezy.com/api/stores#retrieve-a-store). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • products
  • discounts
  • license-keys
  • subscriptions
  • webhooks
| - -#### Example - -```javascript -const store = await ls.getStore({ id: 123 }); -``` - ---- - -### getProducts(parameters) - -Get a list of products. - -Returns a list of [Product objects](https://docs.lemonsqueezy.com/api/products). - -[API reference](https://docs.lemonsqueezy.com/api/products#list-all-products). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------------ | -| `storeId` | number | - | - | Filter products by store. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variants
| - -#### Example - -```javascript -const products = await ls.getProducts(); - -const products = await ls.getProducts({ - storeId: 123, - perPage: 50, - include: "variants", -}); -``` - ---- - -### getProduct(parameters) - -Get a product. - -Returns a [Product object](https://docs.lemonsqueezy.com/api/products). - -[API reference](https://docs.lemonsqueezy.com/api/products#retrieve-a-product). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------------ | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variants
| - -#### Example - -```javascript -const product = await ls.getProduct({ id: 123 }); -``` - ---- - -### getVariants(parameters) - -Get a list of variants. - -Returns a list of [Variant objects](https://docs.lemonsqueezy.com/api/variants). - -[API reference](https://docs.lemonsqueezy.com/api/variants#list-all-variants). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | ----------------------------------------------------------------------------- | -| `productId` | number | - | - | Filter variants by product. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • product
  • files
| - -#### Example - -```javascript -const variants = await ls.getVariants(); - -const variants = await ls.getVariants({ - productId: 123, - perPage: 50, - include: "product", -}); -``` - ---- - -### getVariant(parameters) - -Get a variant. - -Returns a [Variant object](https://docs.lemonsqueezy.com/api/variants). - -[API reference](https://docs.lemonsqueezy.com/api/variants#retrieve-a-variant). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • product
  • files
| - -#### Example - -```javascript -const variant = await ls.getVariant({ id: 123 }); -``` - ---- - -### getPrices(parameters) - -Get a list of prices. - -Returns a list of [Price objects](https://docs.lemonsqueezy.com/api/prices). - -[API reference](https://docs.lemonsqueezy.com/api/prices#list-all-prices). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | --------------------------------------------------------------- | -| `variantId` | number | - | - | Filter prices by variant. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • variant
| - -#### Example - -```javascript -const prices = await ls.getPrices(); - -const prices = await ls.getPrices({ variantId: 123, include: "variant" }); -``` - ---- - -### getPrice(parameters) - -Get a price. - -Returns a [Price object](https://docs.lemonsqueezy.com/api/prices). - -[API reference](https://docs.lemonsqueezy.com/api/prices#retrieve-a-price). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | --------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • variant
| - -#### Example - -```javascript -const price = await ls.getPrice({ id: 123 }); -``` - ---- - -### getCheckouts(parameters) - -Get a list of checkouts. - -Returns a list of [Checkout objects](https://docs.lemonsqueezy.com/api/checkouts). - -[API reference](https://docs.lemonsqueezy.com/api/checkouts#list-all-checkouts). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | ----------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter checkouts by store. | -| `variantId` | number | - | - | Filter checkouts by variant. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variant
| - -#### Example - -```javascript -const checkouts = await ls.getCheckouts(); - -const checkouts = await ls.getCheckouts({ storeId: 123, perPage: 50 }); -``` - ---- - -### getCheckout(parameters) - -Get a checkout. - -Returns a [Checkout object](https://docs.lemonsqueezy.com/api/checkouts). - -[API reference](https://docs.lemonsqueezy.com/api/checkouts#retrieve-a-checkout). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------- | -| `id` | string | Yes | - | Checkout IDs are UUIDs. | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variant
| - -#### Example - -```javascript -const checkout = await ls.getCheckout({ - id: "edc0158c-794a-445d-bfad-24ab66baeb01", -}); -``` - ---- - -### createCheckout(parameters) - -Create a checkout. - -This method allows you to retrieve a product's checkout URL (using store and variant IDs) or create fully customised checkouts (using additional attributes). - -Returns a [Checkout object](https://docs.lemonsqueezy.com/api/checkouts). - -[API reference](https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ------------ | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | Yes | - | | -| `variantId` | number | Yes | - | | -| `attributes` | Object | - | - | An [object of values](https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout) used to configure the checkout. | - -#### Example - -``` -let attributes = { - checkout_data: { - email: 'user@gmail.com', - discount_code: '10PERCENT', - custom: { - user_id: 123 - } - }, - product_options: { - redirect_url: 'https://customredirect.com' - }, - checkout_options: { - dark: true, - logo: false - } -} - -const checkout = await ls.createCheckout({ storeId: 123, variantId: 123, attributes }) -``` - ---- - -### getCustomers(parameters) - -Get a list of customers. - -Returns a list of [Customer objects](https://docs.lemonsqueezy.com/api/customers). - -[API reference](https://docs.lemonsqueezy.com/api/customers#list-all-customers). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter customers by store. | -| `email` | string | - | - | Filter customers by email address. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • license-keys
  • orders
  • store
  • subscriptions
| - -#### Example - -```javascript -const customers = await ls.getCustomers(); - -const customers = await ls.getCustomers({ - email: "customer@gmail.com", - include: "orders,license-keys,subscriptions", -}); -``` - ---- - -### getCustomer(parameters) - -Get a customer. - -Returns a [Customer object](https://docs.lemonsqueezy.com/api/customers). - -[API reference](https://docs.lemonsqueezy.com/api/customers#retrieve-a-customer). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • license-keys
  • orders
  • store
  • subscriptions
| - -#### Example - -```javascript -const customer = await ls.getCustomer({ id: 123 }); -``` - ---- - -### getOrders(parameters) - -Get a list of orders. - -Returns a list of [Order objects](https://docs.lemonsqueezy.com/api/orders). - -[API reference](https://docs.lemonsqueezy.com/api/orders#list-all-orders). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter orders by store. | -| `userEmail` | string | - | - | Filter orders by email address. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • customer
  • discount-redemptions
  • license-keys
  • order-items
  • store
  • subscriptions
| - -#### Example - -```javascript -const orders = await ls.getOrders(); - -const orders = await ls.getOrders({ - email: "customer@gmail.com", - include: "orders,license-keys,subscriptions", -}); -``` - ---- - -### getOrder(parameters) - -Get an order. - -Returns an [Order object](https://docs.lemonsqueezy.com/api/orders). - -[API reference](https://docs.lemonsqueezy.com/api/orders#retrieve-a-order). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • customer
  • discount-redemptions
  • license-keys
  • order-items
  • store
  • subscriptions
| - -#### Example - -```javascript -const order = await ls.getOrder({ id: 123 }); -``` - ---- - -### getFiles(parameters) - -Get a list of files. - -Returns a list of [File objects](https://docs.lemonsqueezy.com/api/files). - -[API reference](https://docs.lemonsqueezy.com/api/files#list-all-files). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | --------------------------------------------------------------- | -| `variantId` | number | - | - | Filter files by variant. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • variant
| - -#### Example - -```javascript -const files = await ls.getFiles(); - -const files = await ls.getFiles({ variantId: 123 }); -``` - ---- - -### getFile(parameters) - -Get a file. - -Returns a [File object](https://docs.lemonsqueezy.com/api/files). - -[API reference](https://docs.lemonsqueezy.com/api/files#retrieve-a-file). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | --------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • variant
| - -#### Example - -```javascript -const file = await ls.getFile({ id: 123 }); -``` - ---- - -### getOrderItems(parameters) - -Get a list of order items. - -Returns a list of [Order item objects](https://docs.lemonsqueezy.com/api/order-items). - -[API reference](https://docs.lemonsqueezy.com/api/order-items#list-all-order-items). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------------- | -| `orderId` | number | - | - | Filter order items by order. | -| `productId` | number | - | - | Filter order items by product. | -| `variantId` | number | - | - | Filter order items by variant. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • order
  • product
  • variant
| - -#### Example - -```javascript -const orderItems = await ls.getOrderItems(); - -const orderItems = await ls.getOrderItems({ order: 123 }); -``` - ---- - -### getOrderItem(parameters) - -Get an order item. - -Returns an [Order item object](https://docs.lemonsqueezy.com/api/order-items). - -[API reference](https://docs.lemonsqueezy.com/api/order-items#retrieve-an-order-item). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • order
  • product
  • variant
| - -#### Example - -```javascript -const orderItem = await ls.getOrderItem({ id: 123 }); -``` - ---- - -### getSubscriptions(parameters) - -Get a list of subscriptions. - -Returns a list of [Subscription objects](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#list-all-subscriptions). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter subscriptions by store. | -| `orderId` | number | - | - | Filter subscriptions by order. | -| `orderItemId` | number | - | - | Filter subscriptions by order item. | -| `productId` | number | - | - | Filter subscriptions by product. | -| `variantId` | number | - | - | Filter subscriptions by variant. | -| `status` | string | - | - | Filter subscriptions by status. Options:
  • `on_trial`
  • `active`
  • `paused`
  • `past_due`
  • `unpaid`
  • `cancelled`
  • `expired
| -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • customer
  • order
  • order-item
  • product
  • variant
| +Do not use this package directly in the browser, as this will expose your API key. This would give anyone full API access to your Lemon Squeezy account and store(s). For more information, [see more](https://docs.lemonsqueezy.com/api#authentication). -#### Example +## Contributing -```javascript -const subscriptions = await ls.getSubscriptions(); - -const subscriptions = await ls.getSubscriptions({ - storeId: 123, - status: "past_due", -}); -``` - ---- - -### getSubscription(parameters) - -Get a subscription. - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#retrieve-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • customer
  • order
  • order-item
  • product
  • variant
| - -#### Example - -```javascript -const subscription = await ls.getSubscription({ id: 123 }); -``` - ---- - -### updateSubscription(parameters) - -Update a subscription: change plan or billing anchor. - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#update-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------------- | ------ | ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `productId` | number | If changing plans | - | ID of product when changing plans. | -| `variantId` | number | If changing plans | - | ID of variant when changing plans. | -| `proration` | string | - | - | Set the proration when changing plans.
  • Use `immediate` to charge a prorated amount immediately
  • Use `disable` to charge a full amount immediately
  • If `proration` is not included, proration will occur at the next renewal date
| -| `billingAnchor` | number | - | - | Change the billing day used for renewal charges. Must be a number between `1` and `31`. | - -#### Example - -```javascript -const subscription = await ls.updateSubscription({ - id: 123, - productId: 123, - variantId: 123, -}); -``` - ---- - -### cancelSubscription(parameters) - -Cancel a subscription. - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#cancel-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -const subscription = await ls.cancelSubscription({ id: 123 }); -``` - ---- - -### resumeSubscription(parameters) - -Resume a cancelled subscription. - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#update-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -const subscription = await ls.resumeSubscription({ id: 123 }); -``` - ---- - -### pauseSubscription(parameters) - -Pause a subscription (halt payment renewals). - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#update-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ----------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `mode` | string | - | `void` | Type of pause:
  • `void` - your product or service is unavailable to customers
  • `free` - the user should get free access
| -| `resumesAt` | string | - | - | Date to automatically resume the subscription ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format datetime). | - -#### Example - -```javascript -const subscription = await ls.pauseSubscription({ id: 123 }); -``` - ---- - -### unpauseSubscription(parameters) - -Un-pause a paused subscription. - -Returns a [Subscription object](https://docs.lemonsqueezy.com/api/subscriptions). - -[API reference](https://docs.lemonsqueezy.com/api/subscriptions#update-a-subscription). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -const subscription = await ls.unpauseSubscription({ id: 123 }); -``` - ---- - -### getSubscriptionInvoices(parameters) - -Get a list of subscription invoices. - -Returns a list of [Subscription invoice objects](https://docs.lemonsqueezy.com/api/subscription-invoices). - -[API reference](https://docs.lemonsqueezy.com/api/subscription-invoices#list-all-subscription-invoices). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ---------------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter subscription invoices by store. | -| `status` | string | - | - | Filter subscription invoices by status. Options:
  • `paid`
  • `pending`
  • `void`
  • `refunded`
| -| `refunded` | boolean | - | - | Filter subscription invoices by refunded. | -| `subscriptionId` | number | - | - | Filter subscription invoices by subscription. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • subscription
| - -#### Example - -```javascript -const subscriptionInvoices = await ls.getSubscriptionInvoices(); - -const subscriptionInvoices = await ls.getSubscriptionInvoices({ - storeId: 123, - refunded: true, -}); -``` - ---- - -### getSubscriptionInvoice(parameters) - -Get a subscription invoice. - -Returns a [Subscription invoice object](https://docs.lemonsqueezy.com/api/subscription-invoices). - -[API reference](https://docs.lemonsqueezy.com/api/subscription-invoices#retrieve-a-subscription-invoice). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • subscription
| - -#### Example - -```javascript -const subscriptionInvoice = await ls.getSubscriptionInvoice({ id: 123 }); -``` - ---- - -### getSubscriptionItems(parameters) - -Get a list of subscription items. - -Returns a list of [Subscription item objects](https://docs.lemonsqueezy.com/api/subscription-items). - -[API reference](https://docs.lemonsqueezy.com/api/subscription-items#list-all-subscription-items). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ---------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------- | -| `subscriptionId` | number | - | - | Filter subscription items by subscription. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • subscription
  • price
  • usage-records
| - -#### Example - -```javascript -const subscriptionItems = await ls.getSubscriptionItems(); - -const subscriptionItems = await ls.getSubscriptionItems({ storeId: 123 }); -``` - ---- - -### getSubscriptionItem(parameters) - -Get a subscription item. - -Returns a [Subscription item object](https://docs.lemonsqueezy.com/api/subscription-items). - -[API reference](https://docs.lemonsqueezy.com/api/subscription-items#retrieve-a-subscription-item). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • subscription
  • price
  • usage-records
| - -#### Example - -```javascript -const subscriptionItem = await ls.getSubscriptionItem({ - id: 123, - include: "price", -}); -``` - ---- - -### updateSubscriptionItem(parameters) - -Update the quantity of a subscription item. - -Returns a [Subscription item object](https://docs.lemonsqueezy.com/api/subscription-items). - -[API reference](https://docs.lemonsqueezy.com/api/subscription-items#update-a-subscription-item). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ---------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | -| `quantity` | number | Yes | - | | - -#### Example - -```javascript -const subscriptionItem = await ls.updateSubscriptionItem({ - id: 123, - quantity: 10, -}); -``` - ---- - -### getSubscriptionItemUsage(parameters) - -Retrieves a subscription item's current usage. - -Returns a meta object containing usage information. - -[API reference](https://docs.lemonsqueezy.com/api/subscription-items#retrieve-a-subscription-item-s-current-usage). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -const usageInformation = await ls.getSubscriptionItemUsage({ id: 123 }); -``` - ---- - -### getUsageRecords(parameters) - -Get a list of usage records. - -Returns a list of [Usage record objects](https://docs.lemonsqueezy.com/api/usage-records). - -[API reference](https://docs.lemonsqueezy.com/api/usage-records#list-all-usage-records). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| -------------------- | ------ | -------- | ------- | ------------------------------------------------------------------------- | -| `subscriptionItemId` | number | - | - | Filter usage records by subscription item. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • subscription-item
| - -#### Example - -```javascript -const usageRecords = await ls.getUsageRecords(); - -const usageRecords = await ls.getUsageRecords({ subscriptionItemId: 123 }); -``` - ---- - -### getUsageRecord(parameters) - -Get a usage record. - -Returns a [Usage record object](https://docs.lemonsqueezy.com/api/usage-records). - -[API reference](https://docs.lemonsqueezy.com/api/usage-records#retrieve-a-usage-record). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • subscription-item
| - -#### Example - -```javascript -const usageRecord = await ls.getUsageRecord({ id: 123 }); -``` - ---- - -### createUsageRecord(parameters) - -Create a usage record. - -Returns a [Usage record object](https://docs.lemonsqueezy.com/api/usage-records). - -[API reference](https://docs.lemonsqueezy.com/api/usage-records#create-a-usage-record). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| -------------------- | ------ | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `subscriptionItemId` | number | Yes | - | | -| `quantity` | number | Yes | - | | -| `action` | string | - | `increment` | The type of record:
  • `increment` - Add to existing records from this billing period.
  • `set` - Reset usage in this billing period to the given quantity.
| - -#### Example - -```javascript -const usageRecord = await ls.createUsageRecord({ - subscriptionItemId: 123, - quantity: 18, -}); -``` - ---- - -### getDiscounts(parameters) - -Get a list of discounts. - -Returns a list of [Discount objects](https://docs.lemonsqueezy.com/api/discounts). - -[API reference](https://docs.lemonsqueezy.com/api/discounts#list-all-discounts). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter discounts by store. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variants
  • discount-redemptions
| - -#### Example - -```javascript -const discounts = await ls.getDiscounts(); - -const discounts = await ls.getDiscounts({ - storeId: 123, - include: "discount-redemptions", -}); -``` - ---- - -### getDiscount(parameters) - -Get a discount. - -Returns a [Discount object](https://docs.lemonsqueezy.com/api/discounts). - -[API reference](https://docs.lemonsqueezy.com/api/discounts#retrieve-a-discount). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
  • store
  • variants
  • discount-redemptions
| - -#### Example - -```javascript -const discount = await ls.getDiscount({ id: 123 }); -``` - ---- - -### createDiscount(parameters) - -Create a discount. - -Returns a [Discount object](https://docs.lemonsqueezy.com/api/discounts). - -[API reference](https://docs.lemonsqueezy.com/api/discounts#create-a-discount). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ------------------ | -------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | Yes | - | | -| `name` | string | Yes | - | The reference name of the discount. | -| `code` | string | Yes | - | The discount code. Can contain uppercase letters and numbers, between 3 and 256 characters. | -| `amount` | string | Yes | - | Either a fixed amount in cents or a percentage:
  • `1000` means $10 when `amount_type` is `fixed`
  • `10` means 10% when `amount_type` is `percent`
  • | -| `amountType` | string | - | `percent` | The type of discount. Options:
    • `percent`
    • `fixed`
    | -| `duration` | string | - | `once` | How many times the discount should apply (for subscriptions only). Options:
    • `once` - only the first payment.
    • `repeating` - applies for months defined in `durationInMonths`.
    • `forever` - applies to every subscription .payment
    | -| `durationInMonths` | number | - | - | How many months the discount should apply when `duration` is `repeating`. | -| `variantIds` | number[] | - | - | Limit discount to certain variants.
    List of variant IDs like `[1,2,3]`. | -| `maxRedemptions` | number | - | - | Limit the total amount of redemptions allowed. | -| `startsAt` | string | - | - | Date the discount code starts on ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format datetime). | -| `expiresAt` | string | - | - | Date the discount code expires on ([ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format datetime). | - -#### Example - -```javascript -const options = { - storeId: 123, - name: "Summer sale", - code: "SUMMERSALE", - amount: 30, - amountType: "percent", - duration: "repeating", - durationInMonths: 3, - startsAt: "2023-07-31T08:00:00.000000Z", -}; -const discount = await ls.createDiscount(options); -``` - ---- - -### deleteDiscount(parameters) - -Delete a discount. - -[API reference](https://docs.lemonsqueezy.com/api/discounts#delete-a-discount). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -await ls.deleteDiscount({ id: 123 }); -``` - ---- - -### getDiscountRedemptions(parameters) - -Get a list of discount redemptions. - -Returns a list of [Discount redemption objects](https://docs.lemonsqueezy.com/api/discount-redemptions). - -[API reference](https://docs.lemonsqueezy.com/api/discount-redemptions#list-all-discount-redemptions). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ------------ | ------ | -------- | ------- | ------------------------------------------------------------------------------ | -| `discountId` | number | - | - | Filter discount redemptions by discount. | -| `orderId` | number | - | - | Filter discount redemptions by order. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
    • discount
    • order
    | - -#### Example - -```javascript -const discountRedemptions = await ls.getDiscountRedemptions(); - -const discountRedemptions = await ls.getDiscountRedemptions({ - orderId: 123, - include: "discount,order", -}); -``` - ---- - -### getDiscountRedemption(parameters) - -Get a discount redemption. - -Returns a [Discount redemption object](https://docs.lemonsqueezy.com/api/discount-redemptions). - -[API reference](https://docs.lemonsqueezy.com/api/discount-redemptions#retrieve-a-discount-redemption). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------------ | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
    • discount
    • order
    | - -#### Example - -```javascript -const discountRedemption = await ls.getDiscountRedemption({ id: 123 }); -``` - ---- - -### getLicenseKeys(parameters) - -Get a list of license keys. - -Returns a list of [License key objects](https://docs.lemonsqueezy.com/api/license-keys). - -[API reference](https://docs.lemonsqueezy.com/api/license-keys#list-all-license-keys). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| ------------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | - | - | Filter license keys by store. | -| `orderId` | number | - | - | Filter license keys by order. | -| `orderItemId` | number | - | - | Filter license keys by order item. | -| `productId` | number | - | - | Filter license keys by product. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    • customer
    • order
    • order-item
    • product
    • license-key-instances
    | - -#### Example - -```javascript -const licenseKeys = await ls.getLicenseKeys(); - -const licenseKeys = await ls.getLicenseKeys({ storeId: 123 }); -``` - ---- - -### getLicenseKey(parameters) - -Get a license key. - -Returns a [License key object](https://docs.lemonsqueezy.com/api/license-keys). - -[API reference](https://docs.lemonsqueezy.com/api/license-keys#retrieve-a-license-key). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    • customer
    • order
    • order-item
    • product
    • license-key-instances
    | - -#### Example - -```javascript -const licenseKey = await ls.getLicenseKey({ id: 123 }); -``` - ---- - -### getLicenseKeyInstances(parameters) - -Get a list of license key instances. - -Returns a list of [License key instance objects](https://docs.lemonsqueezy.com/api/license-key-instances). - -[API reference](https://docs.lemonsqueezy.com/api/license-key-instances#list-all-license-key-instances). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| -------------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `licenseKeyId` | number | - | - | Filter license key instances by license key. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    • customer
    • order
    • order-item
    • product
    • license-key-instances
    | - -#### Example - -```javascript -const licenseKeys = await ls.getLicenseKeys(); - -const licenseKeys = await ls.getLicenseKeys({ licenseKeyId: 123 }); -``` - ---- - -### getLicenseKeyInstance(parameters) - -Get a license key instance. - -Returns a [License key instance object](https://docs.lemonsqueezy.com/api/license-key-instances). - -[API reference](https://docs.lemonsqueezy.com/api/license-key-instances#retrieve-a-license-key-instance). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    • customer
    • order
    • order-item
    • product
    • license-key-instances
    | - -#### Example - -```javascript -const licenseKey = await ls.getLicenseKey({ id: 123 }); -``` - ---- - -### getWebhooks(parameters) - -Get a list of webhooks. - -Returns a list of [Webhook objects](https://docs.lemonsqueezy.com/api/webhooks). - -[API reference](https://docs.lemonsqueezy.com/api/webhooks#list-all-webhooks). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------- | -| `storeId` | number | - | - | Filter webhooks by store. | -| `perPage` | number | - | `10` | | -| `page` | number | - | `1` | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    | - -#### Example - -```javascript -const webhooks = await ls.getWebhooks(); - -const webhooks = await ls.getWebhooks({ storeId: 123 }); -``` - ---- - -### getWebhook(parameters) - -Get a webhook. - -Returns a [Webhook object](https://docs.lemonsqueezy.com/api/webhooks). - -[API reference](https://docs.lemonsqueezy.com/api/webhooks#retrieve-a-webhook). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ------------------------------------------------------------- | -| `id` | number | Yes | - | | -| `include` | string | - | - | Comma-separated list of object names:
    • store
    | - -#### Example - -```javascript -const webhook = await ls.getWebhook({ id: 123 }); -``` - ---- - -### createWebhook(parameters) - -Create a webhook. - -Returns a [Webhook object](https://docs.lemonsqueezy.com/api/webhooks). - -[API reference](https://docs.lemonsqueezy.com/api/webhooks#create-a-webhook). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| `storeId` | number | Yes | - | | -| `url` | string | Yes | - | The endpoint URL that the webhooks should be sent to. | -| `events` | string[] | Yes | - | A list of webhook events to receive. [See all options](https://docs.lemonsqueezy.com/help/webhooks#event-types) | -| `secret` | string | Yes | - | A signing secret used to [secure the webhook](https://docs.lemonsqueezy.com/help/webhooks#signing-requests). Must be between 6 and 40 characters. | - -#### Example - -```javascript -const options = { - storeId: 123, - url: "https://myapp.com/webhook/", - events: ["subscription_created", "subscription_updated"], - secret: "randomstring", -}; -const webhook = await ls.createWebhook(options); -``` - ---- - -### updateWebhook(parameters) - -Update a webhook. - -Returns a [Webhook object](https://docs.lemonsqueezy.com/api/webhooks). - -[API reference](https://docs.lemonsqueezy.com/api/webhooks#update-a-webhook). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | number | - | - | | -| `url` | string | - | - | The endpoint URL that the webhooks should be sent to. | -| `events` | string[] | - | - | A list of webhook events to receive. [See all options](https://docs.lemonsqueezy.com/help/webhooks#event-types) | -| `secret` | string | - | - | A signing secret used to [secure the webhook](https://docs.lemonsqueezy.com/help/webhooks#signing-requests). Must be between 6 and 40 characters. | - -#### Example - -```javascript -const options = { - id: 123, - url: "https://myapp.com/webhook/", -}; -const webhook = await ls.updateWebhook(options); -``` - ---- - -### deleteWebhook(parameters) - -Delete a webhook. - -[API reference](https://docs.lemonsqueezy.com/api/webhooks#delete-a-webhook). - -#### Parameters - -| Parameter | Type | Required | Default | Notes | -| --------- | ------ | -------- | ------- | ----- | -| `id` | number | Yes | - | | - -#### Example - -```javascript -await ls.deleteWebhook({ id: 123 }); -``` +See the [Contributing Guide](https://github.com/lemonsqueezy/lemonsqueezy.js/blob/main/CONTRIBUTING.md). diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a2103e76fa14c14a6d00e47b9d8b0c8257a03ca9 GIT binary patch literal 234930 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 literal 0 HcmV?d00001 diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 0000000..19d3f5f --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,23 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "docs", + "style", + "refactor", + "perf", + "test", + "chore", + "revert", + "build", + "release", + ], + ], + "subject-case": [0], + }, +}; diff --git a/examples/index.ts b/examples/index.ts new file mode 100644 index 0000000..b1553a9 --- /dev/null +++ b/examples/index.ts @@ -0,0 +1,13 @@ +import { getAuthenticatedUser, lemonSqueezySetup } from "../src/index.js"; + +const apiKey = import.meta.env.LEMON_SQUEEZY_API_KEY; + +// Setup +lemonSqueezySetup({ + apiKey, + onError: (error) => console.error("Error!", error), +}); + +// Get authenticated user +const { data, error, statusCode } = await getAuthenticatedUser(); +console.log({ data, error, statusCode }); diff --git a/package.json b/package.json index 2b15b29..c702715 100644 --- a/package.json +++ b/package.json @@ -2,45 +2,86 @@ "name": "@lemonsqueezy/lemonsqueezy.js", "description": "The official Lemon Squeezy JavaScript SDK.", "version": "1.2.5", - "author": "Lemon Squeezy", + "author": { + "name": "Lemon Squeezy", + "email": "hello@lemonsqueezy.com", + "url": "https://lemonsqueezy.com" + }, "license": "MIT", - "main": "dist/index.cjs", - "module": "dist/index.js", - "homepage": "https://github.com/lmsqueezy/lemonsqueezy.js", + "type": "module", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "homepage": "https://lemonsqueezy.com", "repository": { "type": "git", "url": "https://github.com/lmsqueezy/lemonsqueezy.js.git" }, + "publishConfig": { + "access": "public" + }, "bugs": { "url": "https://github.com/lmsqueezy/lemonsqueezy.js/issues" }, "keywords": [ "api", + "sdk", "lemonsqueezy", "javascript", "typescript" ], "files": [ - "./dist/*", + "dist", "README.md" ], "scripts": { + "dev": "bun run --watch examples/index.ts", "build": "tsup", - "dev": "tsup --watch", - "prepublishOnly": "npm run build", - "changeset": "changeset", - "version": "changeset version", - "version:dev": "changeset version --snapshop --no-git-tag --tag dev", - "release": "npm run build && changeset publish", - "release:dev": "npm run build && changeset publish --snapshot --no-git-tag --tag dev" + "coverage": "bun test --coverage", + "export-size": "export-size .", + "format": "prettier --write .", + "lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", + "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", + "test": "bun test", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.1", - "@types/node": "^20.10.6", - "prettier": "^3.1.1", + "@commitlint/cli": "^18.6.0", + "@commitlint/config-conventional": "^18.6.0", + "@types/bun": "latest", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "eslint": "^8.56.0", + "export-size": "^0.7.0", + "lint-staged": "^15.2.1", + "simple-git-hooks": "^2.9.0", + "prettier": "^3.2.4", "tsup": "^8.0.1", "typescript": "^5.3.3" }, - "type": "module" + "engines": { + "node": ">=18" + }, + "lint-staged": { + "*": "bun lint:fix && bun run format" + }, + "simple-git-hooks": { + "pre-commit": "bun run lint-staged", + "commit-msg": "bun run commitlint --edit $1" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index e93057d..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2837 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@changesets/changelog-github': - specifier: ^0.5.0 - version: 0.5.0 - '@changesets/cli': - specifier: ^2.27.1 - version: 2.27.1 - '@types/node': - specifier: ^20.10.6 - version: 20.10.7 - prettier: - specifier: ^3.1.1 - version: 3.1.1 - tsup: - specifier: ^8.0.1 - version: 8.0.1(typescript@5.3.3) - typescript: - specifier: ^5.3.3 - version: 5.3.3 - -packages: - - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 - dev: true - - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true - - /@babel/runtime@7.23.7: - resolution: {integrity: sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: true - - /@changesets/apply-release-plan@7.0.0: - resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/config': 3.0.0 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - detect-indent: 6.1.0 - fs-extra: 7.0.1 - lodash.startcase: 4.4.0 - outdent: 0.5.0 - prettier: 2.8.8 - resolve-from: 5.0.0 - semver: 7.5.4 - dev: true - - /@changesets/assemble-release-plan@6.0.0: - resolution: {integrity: sha512-4QG7NuisAjisbW4hkLCmGW2lRYdPrKzro+fCtZaILX+3zdUELSvYjpL4GTv0E4aM9Mef3PuIQp89VmHJ4y2bfw==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.0.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - semver: 7.5.4 - dev: true - - /@changesets/changelog-git@0.2.0: - resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} - dependencies: - '@changesets/types': 6.0.0 - dev: true - - /@changesets/changelog-github@0.5.0: - resolution: {integrity: sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==} - dependencies: - '@changesets/get-github-info': 0.6.0 - '@changesets/types': 6.0.0 - dotenv: 8.6.0 - transitivePeerDependencies: - - encoding - dev: true - - /@changesets/cli@2.27.1: - resolution: {integrity: sha512-iJ91xlvRnnrJnELTp4eJJEOPjgpF3NOh4qeQehM6Ugiz9gJPRZ2t+TsXun6E3AMN4hScZKjqVXl0TX+C7AB3ZQ==} - hasBin: true - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/apply-release-plan': 7.0.0 - '@changesets/assemble-release-plan': 6.0.0 - '@changesets/changelog-git': 0.2.0 - '@changesets/config': 3.0.0 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.0.0 - '@changesets/get-release-plan': 4.0.0 - '@changesets/git': 3.0.0 - '@changesets/logger': 0.1.0 - '@changesets/pre': 2.0.0 - '@changesets/read': 0.6.0 - '@changesets/types': 6.0.0 - '@changesets/write': 0.3.0 - '@manypkg/get-packages': 1.1.3 - '@types/semver': 7.5.6 - ansi-colors: 4.1.3 - chalk: 2.4.2 - ci-info: 3.9.0 - enquirer: 2.4.1 - external-editor: 3.1.0 - fs-extra: 7.0.1 - human-id: 1.0.2 - meow: 6.1.1 - outdent: 0.5.0 - p-limit: 2.3.0 - preferred-pm: 3.1.2 - resolve-from: 5.0.0 - semver: 7.5.4 - spawndamnit: 2.0.0 - term-size: 2.2.1 - tty-table: 4.2.3 - dev: true - - /@changesets/config@3.0.0: - resolution: {integrity: sha512-o/rwLNnAo/+j9Yvw9mkBQOZySDYyOr/q+wptRLcAVGlU6djOeP9v1nlalbL9MFsobuBVQbZCTp+dIzdq+CLQUA==} - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.0.0 - '@changesets/logger': 0.1.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - micromatch: 4.0.5 - dev: true - - /@changesets/errors@0.2.0: - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - dependencies: - extendable-error: 0.1.7 - dev: true - - /@changesets/get-dependents-graph@2.0.0: - resolution: {integrity: sha512-cafUXponivK4vBgZ3yLu944mTvam06XEn2IZGjjKc0antpenkYANXiiE6GExV/yKdsCnE8dXVZ25yGqLYZmScA==} - dependencies: - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - chalk: 2.4.2 - fs-extra: 7.0.1 - semver: 7.5.4 - dev: true - - /@changesets/get-github-info@0.6.0: - resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==} - dependencies: - dataloader: 1.4.0 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: true - - /@changesets/get-release-plan@4.0.0: - resolution: {integrity: sha512-9L9xCUeD/Tb6L/oKmpm8nyzsOzhdNBBbt/ZNcjynbHC07WW4E1eX8NMGC5g5SbM5z/V+MOrYsJ4lRW41GCbg3w==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/assemble-release-plan': 6.0.0 - '@changesets/config': 3.0.0 - '@changesets/pre': 2.0.0 - '@changesets/read': 0.6.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - dev: true - - /@changesets/get-version-range-type@0.4.0: - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - dev: true - - /@changesets/git@3.0.0: - resolution: {integrity: sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/errors': 0.2.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - is-subdir: 1.2.0 - micromatch: 4.0.5 - spawndamnit: 2.0.0 - dev: true - - /@changesets/logger@0.1.0: - resolution: {integrity: sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==} - dependencies: - chalk: 2.4.2 - dev: true - - /@changesets/parse@0.4.0: - resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} - dependencies: - '@changesets/types': 6.0.0 - js-yaml: 3.14.1 - dev: true - - /@changesets/pre@2.0.0: - resolution: {integrity: sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/errors': 0.2.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - dev: true - - /@changesets/read@0.6.0: - resolution: {integrity: sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/git': 3.0.0 - '@changesets/logger': 0.1.0 - '@changesets/parse': 0.4.0 - '@changesets/types': 6.0.0 - chalk: 2.4.2 - fs-extra: 7.0.1 - p-filter: 2.1.0 - dev: true - - /@changesets/types@4.1.0: - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} - dev: true - - /@changesets/types@6.0.0: - resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} - dev: true - - /@changesets/write@0.3.0: - resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/types': 6.0.0 - fs-extra: 7.0.1 - human-id: 1.0.2 - prettier: 2.8.8 - dev: true - - /@esbuild/aix-ppc64@0.19.11: - resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.19.11: - resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.19.11: - resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.19.11: - resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.19.11: - resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.19.11: - resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.19.11: - resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.19.11: - resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.19.11: - resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.19.11: - resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.19.11: - resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.19.11: - resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.19.11: - resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.19.11: - resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.19.11: - resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.19.11: - resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.19.11: - resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.19.11: - resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.19.11: - resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.19.11: - resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.19.11: - resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.19.11: - resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.19.11: - resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true - - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 - dev: true - - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true - - /@jridgewell/trace-mapping@0.3.20: - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - - /@manypkg/find-root@1.1.0: - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} - dependencies: - '@babel/runtime': 7.23.7 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 - dev: true - - /@manypkg/get-packages@1.1.3: - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - dependencies: - '@babel/runtime': 7.23.7 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.16.0 - dev: true - - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm-eabi@4.9.4: - resolution: {integrity: sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.9.4: - resolution: {integrity: sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.9.4: - resolution: {integrity: sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.9.4: - resolution: {integrity: sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.9.4: - resolution: {integrity: sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.9.4: - resolution: {integrity: sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.9.4: - resolution: {integrity: sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.9.4: - resolution: {integrity: sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.9.4: - resolution: {integrity: sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.9.4: - resolution: {integrity: sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.9.4: - resolution: {integrity: sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.9.4: - resolution: {integrity: sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.9.4: - resolution: {integrity: sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true - - /@types/minimist@1.2.5: - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - dev: true - - /@types/node@12.20.55: - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - dev: true - - /@types/node@20.10.7: - resolution: {integrity: sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==} - dependencies: - undici-types: 5.26.5 - dev: true - - /@types/normalize-package-data@2.4.4: - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - dev: true - - /@types/semver@7.5.6: - resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} - dev: true - - /ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true - - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true - - /any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - dev: true - - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: true - - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} - dependencies: - call-bind: 1.0.5 - is-array-buffer: 3.0.2 - dev: true - - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true - - /array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - es-shim-unscopables: 1.0.2 - dev: true - - /arraybuffer.prototype.slice@1.0.2: - resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 - is-array-buffer: 3.0.2 - is-shared-array-buffer: 1.0.2 - dev: true - - /arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - dev: true - - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - dependencies: - is-windows: 1.0.2 - dev: true - - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /breakword@1.0.6: - resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} - dependencies: - wcwidth: 1.0.1 - dev: true - - /bundle-require@4.0.2(esbuild@0.19.11): - resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.17' - dependencies: - esbuild: 0.19.11 - load-tsconfig: 0.2.5 - dev: true - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} - dependencies: - function-bind: 1.1.2 - get-intrinsic: 1.2.2 - set-function-length: 1.1.1 - dev: true - - /camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - map-obj: 4.3.0 - quick-lru: 4.0.1 - dev: true - - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - dev: true - - /cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: true - - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - dev: true - - /cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /csv-generate@3.4.3: - resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} - dev: true - - /csv-parse@4.16.3: - resolution: {integrity: sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==} - dev: true - - /csv-stringify@5.6.5: - resolution: {integrity: sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==} - dev: true - - /csv@5.5.3: - resolution: {integrity: sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==} - engines: {node: '>= 0.1.90'} - dependencies: - csv-generate: 3.4.3 - csv-parse: 4.16.3 - csv-stringify: 5.6.5 - stream-transform: 2.1.3 - dev: true - - /dataloader@1.4.0: - resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - dev: true - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - dev: true - - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: true - - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: true - - /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - dev: true - - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 - object-keys: 1.1.1 - dev: true - - /detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /dotenv@8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} - dev: true - - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true - - /enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - dev: true - - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: true - - /es-abstract@1.22.3: - resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - arraybuffer.prototype.slice: 1.0.2 - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 - es-set-tostringtag: 2.0.2 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.6 - get-intrinsic: 1.2.2 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - has-proto: 1.0.1 - has-symbols: 1.0.3 - hasown: 2.0.0 - internal-slot: 1.0.6 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.12 - is-weakref: 1.0.2 - object-inspect: 1.13.1 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.1 - safe-array-concat: 1.0.1 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.8 - string.prototype.trimend: 1.0.7 - string.prototype.trimstart: 1.0.7 - typed-array-buffer: 1.0.0 - typed-array-byte-length: 1.0.0 - typed-array-byte-offset: 1.0.0 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.13 - dev: true - - /es-set-tostringtag@2.0.2: - resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.2 - has-tostringtag: 1.0.0 - hasown: 2.0.0 - dev: true - - /es-shim-unscopables@1.0.2: - resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} - dependencies: - hasown: 2.0.0 - dev: true - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true - - /esbuild@0.19.11: - resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.11 - '@esbuild/android-arm': 0.19.11 - '@esbuild/android-arm64': 0.19.11 - '@esbuild/android-x64': 0.19.11 - '@esbuild/darwin-arm64': 0.19.11 - '@esbuild/darwin-x64': 0.19.11 - '@esbuild/freebsd-arm64': 0.19.11 - '@esbuild/freebsd-x64': 0.19.11 - '@esbuild/linux-arm': 0.19.11 - '@esbuild/linux-arm64': 0.19.11 - '@esbuild/linux-ia32': 0.19.11 - '@esbuild/linux-loong64': 0.19.11 - '@esbuild/linux-mips64el': 0.19.11 - '@esbuild/linux-ppc64': 0.19.11 - '@esbuild/linux-riscv64': 0.19.11 - '@esbuild/linux-s390x': 0.19.11 - '@esbuild/linux-x64': 0.19.11 - '@esbuild/netbsd-x64': 0.19.11 - '@esbuild/openbsd-x64': 0.19.11 - '@esbuild/sunos-x64': 0.19.11 - '@esbuild/win32-arm64': 0.19.11 - '@esbuild/win32-ia32': 0.19.11 - '@esbuild/win32-x64': 0.19.11 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - - /extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - dev: true - - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true - - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fastq@1.16.0: - resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} - dependencies: - reusify: 1.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /find-yarn-workspace-root2@1.2.16: - resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - dependencies: - micromatch: 4.0.5 - pkg-dir: 4.2.0 - dev: true - - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: true - - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - dev: true - - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: true - - /fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true - - /function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - functions-have-names: 1.2.3 - dev: true - - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true - - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} - dependencies: - function-bind: 1.1.2 - has-proto: 1.0.1 - has-symbols: 1.0.3 - hasown: 2.0.0 - dev: true - - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true - - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 - dev: true - - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.1 - dev: true - - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.0 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.2 - dev: true - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true - - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - - /hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true - - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true - - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} - dependencies: - get-intrinsic: 1.2.2 - dev: true - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: true - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: true - - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: true - - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true - - /human-id@1.0.2: - resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} - dev: true - - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true - - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - - /ignore@5.3.0: - resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} - engines: {node: '>= 4'} - dev: true - - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true - - /internal-slot@1.0.6: - resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.2 - hasown: 2.0.0 - side-channel: 1.0.4 - dev: true - - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-typed-array: 1.1.12 - dev: true - - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true - - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: true - - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - dependencies: - hasown: 2.0.0 - dev: true - - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true - - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 - dev: true - - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - dependencies: - call-bind: 1.0.5 - dev: true - - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: true - - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - dependencies: - better-path-resolve: 1.0.0 - dev: true - - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /is-typed-array@1.1.12: - resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} - engines: {node: '>= 0.4'} - dependencies: - which-typed-array: 1.1.13 - dev: true - - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - dependencies: - call-bind: 1.0.5 - dev: true - - /is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - dev: true - - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: true - - /joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - dev: true - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true - - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: true - - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true - - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - dev: true - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true - - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: true - - /lilconfig@3.0.0: - resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} - engines: {node: '>=14'} - dev: true - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true - - /load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /load-yaml-file@0.2.0: - resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} - engines: {node: '>=6'} - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - dev: true - - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - dependencies: - p-locate: 4.1.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - dev: true - - /lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - dev: true - - /lru-cache@10.1.0: - resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} - engines: {node: 14 || >=16.14} - dev: true - - /lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: true - - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true - - /meow@6.1.1: - resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} - engines: {node: '>=8'} - dependencies: - '@types/minimist': 1.2.5 - camelcase-keys: 6.2.2 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 2.5.0 - read-pkg-up: 7.0.1 - redent: 3.0.0 - trim-newlines: 3.0.1 - type-fest: 0.13.1 - yargs-parser: 18.1.3 - dev: true - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true - - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true - - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - dev: true - - /minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - dev: true - - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true - - /mixme@0.5.10: - resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==} - engines: {node: '>= 8.0.0'} - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - dev: true - - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: true - - /normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.8 - semver: 5.7.2 - validate-npm-package-license: 3.0.4 - dev: true - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: true - - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - dev: true - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: true - - /object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - has-symbols: 1.0.3 - object-keys: 1.1.1 - dev: true - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: true - - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true - - /outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - dev: true - - /p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} - dependencies: - p-map: 2.1.0 - dev: true - - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - dependencies: - p-try: 2.2.0 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - dependencies: - p-limit: 2.3.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - dev: true - - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - dev: true - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.23.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 10.1.0 - minipass: 7.0.4 - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - dev: true - - /pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - dev: true - - /pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - dependencies: - find-up: 4.1.0 - dev: true - - /postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 3.0.0 - yaml: 2.3.4 - dev: true - - /preferred-pm@3.1.2: - resolution: {integrity: sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q==} - engines: {node: '>=10'} - dependencies: - find-up: 5.0.0 - find-yarn-workspace-root2: 1.2.16 - path-exists: 4.0.0 - which-pm: 2.0.0 - dev: true - - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true - - /prettier@3.1.1: - resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: true - - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - dev: true - - /read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} - dependencies: - find-up: 4.1.0 - read-pkg: 5.2.0 - type-fest: 0.8.1 - dev: true - - /read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 2.5.0 - parse-json: 5.2.0 - type-fest: 0.6.0 - dev: true - - /read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - dev: true - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} - dependencies: - indent-string: 4.0.0 - strip-indent: 3.0.0 - dev: true - - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - dev: true - - /regexp.prototype.flags@1.5.1: - resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - set-function-name: 2.0.1 - dev: true - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true - - /require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: true - - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true - - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rollup@4.9.4: - resolution: {integrity: sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.9.4 - '@rollup/rollup-android-arm64': 4.9.4 - '@rollup/rollup-darwin-arm64': 4.9.4 - '@rollup/rollup-darwin-x64': 4.9.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.9.4 - '@rollup/rollup-linux-arm64-gnu': 4.9.4 - '@rollup/rollup-linux-arm64-musl': 4.9.4 - '@rollup/rollup-linux-riscv64-gnu': 4.9.4 - '@rollup/rollup-linux-x64-gnu': 4.9.4 - '@rollup/rollup-linux-x64-musl': 4.9.4 - '@rollup/rollup-win32-arm64-msvc': 4.9.4 - '@rollup/rollup-win32-ia32-msvc': 4.9.4 - '@rollup/rollup-win32-x64-msvc': 4.9.4 - fsevents: 2.3.3 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /safe-array-concat@1.0.1: - resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} - engines: {node: '>=0.4'} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - has-symbols: 1.0.3 - isarray: 2.0.5 - dev: true - - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-regex: 1.1.4 - dev: true - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - - /semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - dev: true - - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true - - /set-function-length@1.1.1: - resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.1 - get-intrinsic: 1.2.2 - gopd: 1.0.1 - has-property-descriptors: 1.0.1 - dev: true - - /set-function-name@2.0.1: - resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.1 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.1 - dev: true - - /shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - dependencies: - shebang-regex: 1.0.0 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - object-inspect: 1.13.1 - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true - - /smartwrap@2.0.2: - resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==} - engines: {node: '>=6'} - hasBin: true - dependencies: - array.prototype.flat: 1.3.2 - breakword: 1.0.6 - grapheme-splitter: 1.0.4 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - yargs: 15.4.1 - dev: true - - /source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - dependencies: - whatwg-url: 7.1.0 - dev: true - - /spawndamnit@2.0.0: - resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} - dependencies: - cross-spawn: 5.1.0 - signal-exit: 3.0.7 - dev: true - - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.16 - dev: true - - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true - - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.16 - dev: true - - /spdx-license-ids@3.0.16: - resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} - dev: true - - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true - - /stream-transform@2.1.3: - resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} - dependencies: - mixme: 0.5.10 - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: true - - /string.prototype.trim@1.2.8: - resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - dev: true - - /string.prototype.trimend@1.0.7: - resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - dev: true - - /string.prototype.trimstart@1.0.7: - resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} - dependencies: - call-bind: 1.0.5 - define-properties: 1.2.1 - es-abstract: 1.22.3 - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: true - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true - - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - dependencies: - min-indent: 1.0.1 - dev: true - - /sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - commander: 4.1.1 - glob: 10.3.10 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - dev: true - - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true - - /term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - dev: true - - /thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - dependencies: - thenify: 3.3.1 - dev: true - - /thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - dependencies: - any-promise: 1.3.0 - dev: true - - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - dependencies: - os-tmpdir: 1.0.2 - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true - - /tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - dependencies: - punycode: 2.3.1 - dev: true - - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - dev: true - - /trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - dev: true - - /ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: true - - /tsup@8.0.1(typescript@5.3.3): - resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': - optional: true - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - dependencies: - bundle-require: 4.0.2(esbuild@0.19.11) - cac: 6.7.14 - chokidar: 3.5.3 - debug: 4.3.4 - esbuild: 0.19.11 - execa: 5.1.1 - globby: 11.1.0 - joycon: 3.1.1 - postcss-load-config: 4.0.2 - resolve-from: 5.0.0 - rollup: 4.9.4 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tree-kill: 1.2.2 - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - - /tty-table@4.2.3: - resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - chalk: 4.1.2 - csv: 5.5.3 - kleur: 4.1.5 - smartwrap: 2.0.2 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - yargs: 17.7.2 - dev: true - - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - dev: true - - /type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true - - /type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true - - /typed-array-buffer@1.0.0: - resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-typed-array: 1.1.12 - dev: true - - /typed-array-byte-length@1.0.0: - resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.5 - for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 - dev: true - - /typed-array-byte-offset@1.0.0: - resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 - for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 - dev: true - - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - dependencies: - call-bind: 1.0.5 - for-each: 0.3.3 - is-typed-array: 1.1.12 - dev: true - - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - dependencies: - call-bind: 1.0.5 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: true - - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true - - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: true - - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - dev: true - - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: true - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true - - /webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - dev: true - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: true - - /whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - dev: true - - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: true - - /which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - dev: true - - /which-pm@2.0.0: - resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} - engines: {node: '>=8.15'} - dependencies: - load-yaml-file: 0.2.0 - path-exists: 4.0.0 - dev: true - - /which-typed-array@1.1.13: - resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - dev: true - - /which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - dev: true - - /y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - dev: true - - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - - /yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yaml@2.3.4: - resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} - engines: {node: '>= 14'} - dev: true - - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - dev: true - - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true - - /yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - dev: true - - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - dependencies: - cliui: 8.0.1 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true diff --git a/prettier.config.js b/prettier.config.js index d0adc0d..b732ef1 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -4,6 +4,7 @@ const config = { printWidth: 80, singleQuote: false, tabWidth: 2, + trailingComma: "es5", }; diff --git a/shims.d.ts b/shims.d.ts new file mode 100644 index 0000000..adbb3eb --- /dev/null +++ b/shims.d.ts @@ -0,0 +1,7 @@ +declare module "bun" { + interface Env { + LEMON_SQUEEZY_API_KEY: string; + LEMON_SQUEEZY_STORE_ID: string; + LEMON_SQUEEZY_LICENSE_KEY: string; + } +} diff --git a/src/_deprecated/LemonSqueezy.ts b/src/_deprecated/LemonSqueezy.ts new file mode 100644 index 0000000..f572c0d --- /dev/null +++ b/src/_deprecated/LemonSqueezy.ts @@ -0,0 +1,1726 @@ +import { requiredCheck } from "../internal"; +import type { + CheckoutResponse, + CheckoutsResponse, + CustomerResponse, + CustomersResponse, + DiscountRedemptionResponse, + DiscountRedemptionsResponse, + DiscountResponse, + DiscountsResponse, + FileResponse, + FilesResponse, + LicenseKeyInstanceResponse, + LicenseKeyInstancesResponse, + LicenseKeyResponse, + LicenseKeysResponse, + OrderResponse, + OrdersResponse, + ProductResponse, + ProductsResponse, + StoreResponse, + StoresResponse, + SubscriptionInvoiceResponse, + SubscriptionInvoicesResponse, + SubscriptionItemResponse, + SubscriptionItemUsageResponse, + SubscriptionResponse, + SubscriptionsResponse, + UsageRecordResponse, + UsageRecordsResponse, + UserResponse, + VariantResponse, + VariantsResponse, + WebhookResponse, + WebhooksResponse, +} from "./types/api"; +import { + type BaseUpdateSubscriptionOptions, + type CreateCheckoutOptions, + type CreateDiscountAttributes, + type CreateDiscountOptions, + type CreateUsageRecordOptions, + type CreateWebhookOptions, + type DeleteDiscountOptions, + type DeleteWebhookOptions, + type GetCheckoutOptions, + type GetCheckoutsOptions, + type GetCustomerOptions, + type GetCustomersOptions, + type GetDiscountOptions, + type GetDiscountRedemptionOptions, + type GetDiscountRedemptionsOptions, + type GetDiscountsOptions, + type GetFileOptions, + type GetFilesOptions, + type GetLicenseKeyInstanceOptions, + type GetLicenseKeyInstancesOptions, + type GetLicenseKeyOptions, + type GetLicenseKeysOptions, + type GetOrderItemOptions, + type GetOrderItemsOptions, + type GetOrderOptions, + type GetOrdersOptions, + type GetPriceOptions, + type GetPricesOptions, + type GetProductOptions, + type GetProductsOptions, + type GetStoreOptions, + type GetStoresOptions, + type GetSubscriptionInvoiceOptions, + type GetSubscriptionInvoicesOptions, + type GetSubscriptionItemOptions, + type GetSubscriptionItemUsageOptions, + type GetSubscriptionItemsOptions, + type GetSubscriptionOptions, + type GetSubscriptionsOptions, + type GetUsageRecordOptions, + type GetUsageRecordsOptions, + type GetVariantOptions, + type GetVariantsOptions, + type GetWebhookOptions, + type GetWebhooksOptions, + type PauseSubscriptionAttributes, + type PauseSubscriptionOptions, + type QueryApiOptions, + type UpdateSubscriptionAttributes, + type UpdateSubscriptionItemOptions, + type UpdateSubscriptionOptions, + type UpdateWebhookOptions, +} from "./types/methods"; + +/** + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method. + * + * @see https://github.com/lmsqueezy/lemonsqueezy.js?tab=readme-ov-file#usage. + */ +export class LemonSqueezy { + protected apiKey: string; + public apiUrl = "https://api.lemonsqueezy.com/"; + + constructor(apiKey: string) { + console.warn( + "Warning: LemonSqueezy class is deprecated. Please use the new setup method. See https://github.com/lmsqueezy/lemonsqueezy.js?tab=readme-ov-file#usage." + ); + + this.apiKey = apiKey; + } + + /* -------------------------------------------------------------------------- */ + /* Helpers */ + /* -------------------------------------------------------------------------- */ + + /** + * Builds a params object for the API query based on provided and allowed + * filters. + * + * Also converts pagination parameters `page` to `page[number]` and `perPage` + * to `page[size]` + * + * @param [args] Arguments to the API method + * @param [allowedFilters] List of filters the API query permits + * (camelCase) + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _buildParams>( + args: TArgs, + allowedFilters: Array = [] + ): Record { + const params: Record = {}; + + for (const filter in args) { + if (allowedFilters.includes(filter)) { + const queryFilter = filter.replace( + /[A-Z]/g, + (letter) => `_${letter.toLowerCase()}` + ); + + params["filter[" + queryFilter + "]"] = args[filter]; + } else { + // In v1.0.3 and lower we supported passing in a string of comma separated values + // for the `include` filter. This is now deprecated in favour of an array. + if (filter === "include") { + params["include"] = Array.isArray(args[filter]) + ? args[filter].join(",") + : args[filter]; + } + + if (filter === "page") params["page[number]"] = args[filter]; + if (filter === "perPage") params["page[size]"] = args[filter]; + } + } + + return params; + } + + /** + * Send an API query to the LemonSqueezy API + * + * @param [path] The path to the API endpoint. + * @param [method] POST, GET, PATCH, DELETE. + * @param [params] URL query parameters. + * @param [payload] Object/JSON payload. + * + * @returns JSON response from the API or throws an error. + */ + private async _query({ + path, + method = "GET", + params, + payload, + }: QueryApiOptions) { + const url = new URL(path, this.apiUrl); + if (params && method === "GET") + Object.entries(params).forEach(([key, value]) => + url.searchParams.append(key, value) + ); + + const headers = new Headers(); + headers.set("Accept", "application/vnd.api+json"); + headers.set("Authorization", `Bearer ${this.apiKey}`); + headers.set("Content-Type", "application/vnd.api+json"); + + const response = await fetch(url.href, { + headers, + method, + body: payload ? JSON.stringify(payload) : undefined, + }); + + if (!response.ok) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errorsJson = (await response.json()) as any; + + throw { + status: response.status, + message: response.statusText, + errors: errorsJson.errors, + }; + } + + if (method !== "DELETE") { + return await response.json(); + } + } + + /* -------------------------------------------------------------------------- */ + /* Users */ + /* -------------------------------------------------------------------------- */ + + /** + * Get current user + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getAuthenticatedUser` + * function. + * + * @returns JSON + */ + async getUser(): Promise { + return this._query({ path: "v1/users/me" }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Stores */ + /* -------------------------------------------------------------------------- */ + + /** + * Get stores + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getStore` function + * instead. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listStores` function + * instead. + * + * @param [params] + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A paginated list of `store` objects ordered by name. + */ + async getStores(params: GetStoresOptions = {}): Promise { + return this._query({ + path: "v1/stores", + params: this._buildParams(params), + }) as Promise; + } + + /** + * Retrieve a store. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getStore` function + * instead. + * + * @param storeId (Required) The given store id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A store object. + */ + async getStore(p: GetStoreOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/stores/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Customers */ + /* -------------------------------------------------------------------------- */ + + /** + * Get customers + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listCustomers` function + * instead. + * + * @param [params] + * @param [params.storeId] Filter customers by store + * @param [params.email] Filter customers by email address + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A paginated list of customer objects ordered by `created_at` (descending). + */ + async getCustomers( + params: GetCustomersOptions = {} + ): Promise { + return this._query({ + path: "v1/customers", + params: this._buildParams(params, ["storeId", "email"]), + }) as Promise; + } + + /** + * Get a customer + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getCustomer` function + * instead. + * + * @param customerId The given customer id. + * @param [params] + * @param [params.id] + * @param [params.include] List of record types to include + * + * @returns A customer object. + */ + async getCustomer(p: GetCustomerOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/customers/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Products */ + /* -------------------------------------------------------------------------- */ + + /** + * Get products + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listProducts` function + * instead. + * + * @param [params] Additional parameters. + * @param [params.storeId] Filter products by store + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A paginated list of product objects ordered by `name`. + */ + async getProducts( + params: GetProductsOptions = {} + ): Promise { + return this._query({ + path: "v1/products", + params: this._buildParams(params, ["storeId"]), + }) as Promise; + } + + /** + * Get a product + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getProduct` function + * instead. + * + * @param productId The given product id. + * @param [params] + * @param [params.id] + * @param [params.include] List of record types to include + * + * @returns A product object. + */ + async getProduct(p: GetProductOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/products/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Variants */ + /* -------------------------------------------------------------------------- */ + + /** + * Get variants + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listVariants` function + * instead. + * + * @param [params] + * @param [params.productId Filter variants by product + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns {Object} JSON + */ + async getVariants( + params: GetVariantsOptions = {} + ): Promise { + return this._query({ + path: "v1/variants", + params: this._buildParams(params, ["productId"]), + }) as Promise; + } + + /** + * Get a variant + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getVariant` function + * instead. + * + * @param {Object} params + * @param {number} params.id + * @param {Array<"product" | "files">} [params.include] List of record types to include + * + * @returns {Object} JSON + */ + async getVariant(p: GetVariantOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/variants/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Prices */ + /* -------------------------------------------------------------------------- */ + + /** + * Get prices + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listPrices` function + * instead. + * + * @param [params] + * @param [params.variantId] Filter prices by variant + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A paginated list of price objects ordered by `created_at` (descending). + */ + async getPrices(params: GetPricesOptions = {}) { + return this._query({ + path: "v1/prices", + params: this._buildParams(params, ["variantId"]), + }); + } + + /** + * Get a price + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listPrices` function + * instead. + * + * @param priceId The given price id. + * @param [params] + * @param [params.include] List of record types to include + * + * @returns A price object. + */ + async getPrice(p: GetPriceOptions) { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/prices/${id}`, + params: this._buildParams(params), + }); + } + + /* -------------------------------------------------------------------------- */ + /* Files */ + /* -------------------------------------------------------------------------- */ + /** + * Get files + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listFiles` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.variantId] (Optional) Only return files belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of file objects ordered by `sort`. + */ + async getFiles(params: GetFilesOptions = {}): Promise { + return this._query({ + path: "v1/files", + params: this._buildParams(params, ["variantId"]), + }) as Promise; + } + + /** + * Retrieve a file. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getFile` function + * instead. + * + * @param fileId The given file id + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A file object. + */ + async getFile(p: GetFileOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/files/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Orders */ + /* -------------------------------------------------------------------------- */ + + /** + * Get orders. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listOrders` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return orders belonging to the store with this ID. + * @param [params.filter.userEmail] (Optional) Only return orders where the `user_email` field is equal to this email address. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of order objects ordered by `created_at` (descending). + */ + async getOrders(params: GetOrdersOptions = {}): Promise { + return this._query({ + path: "v1/orders", + params: this._buildParams(params, ["storeId", "userEmail"]), + }) as Promise; + } + + /** + * Retrieve an order. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getOrder` function + * instead. + * + * @param orderId The given order id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns An order object. + */ + async getOrder(p: GetOrderOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/orders/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Order Items */ + /* -------------------------------------------------------------------------- */ + + /** + * List all order items. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listOrderItems` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.orderId] (Optional) Only return order items belonging to the order with this ID. + * @param [params.filter.productId] (Optional) Only return order items belonging to the product with this ID. + * @param [params.filter.variantId] (Optional) Only return order items belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of order item objects ordered by `id`. + */ + async getOrderItems(params: GetOrderItemsOptions = {}) { + return this._query({ + path: "v1/order-items", + params: this._buildParams(params, ["orderId", "productId", "variantId"]), + }); + } + + /** + * Retrieve an order item. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getOrderItem` function + * instead. + * + * @param orderItemId The given order item id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns An order item object. + */ + async getOrderItem(p: GetOrderItemOptions) { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/order-items/${id}`, + params: this._buildParams(params), + }); + } + + /* -------------------------------------------------------------------------- */ + /* Subscriptions */ + /* -------------------------------------------------------------------------- */ + + /** + * Get all subscriptions. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listSubscriptions` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter.storeId] (Optional) Only return subscriptions belonging to the store with this ID. + * @param [params.filter.orderId] (Optional) Only return subscriptions belonging to the order with this ID. + * @param [params.filter.orderItemId] (Optional) Only return subscriptions belonging to the order item with this ID. + * @param [params.filter.productId] (Optional) Only return subscriptions belonging to the product with this ID. + * @param [params.filter.variantId] (Optional) Only return subscriptions belonging to the variant with this ID. + * @param [params.filter.userEmail] (Optional) Only return subscriptions where the `user_email` field is equal to this email address. + * @param [params.filter.status] (Optional) Only return subscriptions with this status. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of subscription objects ordered by `created_at` (descending). + */ + async getSubscriptions( + params: GetSubscriptionsOptions = {} + ): Promise { + return this._query({ + path: "v1/subscriptions", + params: this._buildParams(params, [ + "storeId", + "orderId", + "orderItemId", + "productId", + "variantId", + "status", + ]), + }) as Promise; + } + + /** + * Retrieve a subscription. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getSubscription` function + * instead. + * + * @param subscriptionId The given subscription id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription object. + */ + async getSubscription( + p: GetSubscriptionOptions + ): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscriptions/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Update a subscription's plan + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateSubscription` function + * instead. + * + * @param params.id + * @param [params.variantId] ID of variant (required if changing plans) + * @param [params.productId] ID of product (required if changing plans) + * @param [params.billingAnchor] Set the billing day (1–31) used for renewal charges + * @param {"immediate" | "disable"} [params.proration] If not included, proration will occur at the next renewal date. + * Use 'immediate' to charge a prorated amount immediately. + * Use 'disable' to charge a full ammount immediately. + * + * @returns {Object} JSON + */ + async updateSubscription( + p: UpdateSubscriptionOptions + ): Promise { + const { id, variantId, productId, billingAnchor, proration } = p || {}; + requiredCheck({ id }); + + const attributes: UpdateSubscriptionAttributes = { + variant_id: variantId, + product_id: productId, + billing_anchor: billingAnchor, + }; + + if (proration == "disable") { + attributes.disable_prorations = true; + } + + if (proration == "immediate") { + attributes.invoice_immediately = true; + } + + return this._query({ + path: `v1/subscriptions/${id}`, + method: "PATCH", + payload: { + data: { + type: "subscriptions", + id: "" + id, + attributes, + }, + }, + }) as Promise; + } + + /** + * Cancel a subscription. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `cancelSubscription` function + * instead. + * + * @param subscriptionId The given subscription id + * @returns The Subscription object in a cancelled state. + */ + async cancelSubscription( + p: BaseUpdateSubscriptionOptions + ): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscriptions/${id}`, + method: "DELETE", + }) as Promise; + } + + /** + * Resume (un-cancel) a subscription + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateSubscription` function + * instead. + * + * @param subscriptionId The given subscription id + * @returns The Subscription object. + */ + async resumeSubscription( + p: BaseUpdateSubscriptionOptions + ): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscriptions/${id}`, + method: "PATCH", + payload: { + data: { + type: "subscriptions", + id: "" + id, + attributes: { + cancelled: false, + }, + }, + }, + }) as Promise; + } + + /** + * Pause a subscription + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateSubscription` function + * instead. + * + * @param subscriptionId The given subscription id + * @param [params] (Optional) Additional parameters. + * @param [params.mode] Pause mode: "void" (default) or "free" + * @param [params.resumesAt] Date to automatically resume the subscription (ISO 8601 format) + * + * @returns {Object} JSON + */ + async pauseSubscription( + p: PauseSubscriptionOptions + ): Promise { + const { id, mode, resumesAt } = p || {}; + requiredCheck({ id }); + + const pause: PauseSubscriptionAttributes = { mode: "void" }; + + if (mode) { + pause.mode = mode; + } + + if (resumesAt) { + pause.resumes_at = resumesAt; + } + + return this._query({ + path: `v1/subscriptions/${id}`, + method: "PATCH", + payload: { + data: { + type: "subscriptions", + id: id.toString(), + attributes: { pause }, + }, + }, + }) as Promise; + } + + /** + * Unpause a subscription. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateSubscription` function + * instead. + * + * @param subscriptionId The given subscription id + * @returns The Subscription object. + */ + async unpauseSubscription( + p: BaseUpdateSubscriptionOptions + ): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscriptions/${id}`, + method: "PATCH", + payload: { + data: { + type: "subscriptions", + id: "" + id, + attributes: { + pause: null, + }, + }, + }, + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Subscription Invoices */ + /* -------------------------------------------------------------------------- */ + + /** + * Get subscription invoices + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listSubscriptionInvoices` function + * instead. + * + * @param [params] + * @param [params.storeId] Filter subscription invoices by store + * @param [params.status] Filter subscription invoices by status + * @param [params.refunded] Filter subscription invoices by refunded + * @param [params.subscriptionId] Filter subscription invoices by subscription + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A paginated list of subscription invoice objects ordered by `created_at` (descending). + */ + async getSubscriptionInvoices( + params: GetSubscriptionInvoicesOptions = {} + ): Promise { + return this._query({ + path: "v1/subscription-invoices", + params: this._buildParams(params, [ + "storeId", + "status", + "refunded", + "subscriptionId", + ]), + }) as Promise; + } + + /** + * Retrieve a subscription invoice. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getSubscriptionInvoice` function + * instead. + * + * @param subscriptionInvoiceId The given subscription invoice id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription invoice object. + */ + async getSubscriptionInvoice( + p: GetSubscriptionInvoiceOptions + ): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscription-invoices/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Subscription Items */ + /* -------------------------------------------------------------------------- */ + + /** + * List all subscription items. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listSubscriptionItems` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.subscriptionId] (Optional) Only return subscription items belonging to a subscription with this ID. + * @param [params.filter.priceId] (Optional) Only return subscription items belonging to a price with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of subscription item objects ordered by `created_at` (descending). + */ + async getSubscriptionItems( + params: GetSubscriptionItemsOptions = {} + ): Promise { + return this._query({ + path: "v1/subscription-items", + params: this._buildParams(params, ["subscriptionId", "priceId"]), + }) as Promise; + } + + /** + * Retrieve a subscription item. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getSubscriptionItem` function + * instead. + * + * @param subscriptionItemId The given subscription item id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription item object. + */ + async getSubscriptionItem( + p: GetSubscriptionItemOptions + ): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscription-items/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Update a subscription item. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateSubscriptionItem` function + * instead. + * + * Note: this endpoint is only used with quantity-based billing. + * If the related subscription's product/variant has usage-based billing + * enabled, this endpoint will return a `422 Unprocessable Entity` response. + * + * @param subscriptionItemId The given subscription item id. + * @param quantity The unit quantity of the subscription. + * @returns A subscription item object. + */ + async updateSubscriptionItem( + p: UpdateSubscriptionItemOptions + ): Promise { + const { id, quantity } = p || {}; + requiredCheck({ id, quantity }); + + return this._query({ + path: `v1/subscription-items/${id}`, + method: "PATCH", + payload: { + data: { + type: "subscription-items", + id: "" + id, + attributes: { + quantity, + }, + }, + }, + }) as Promise; + } + + /** + * Retrieve a subscription item's current usage. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getSubscriptionItemCurrentUsage` function + * instead. + * + * Note: this endpoint is only for subscriptions with usage-based billing + * enabled. It will return a `404 Not Found` response if the related + * subscription product/variant does not have usage-based billing enabled. + * + * @param subscriptionItemId The given subscription item id. + * @returns A meta object containing usage information. + */ + async getSubscriptionItemUsage( + p: GetSubscriptionItemUsageOptions + ): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/subscription-items/${id}/current-usage`, + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Usage Records */ + /* -------------------------------------------------------------------------- */ + + /** + * Get all usage records. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listUsageRecords` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.subscriptionItemId] (Optional) Only return usage records belonging to the subscription item with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of usage record objects ordered by `created_at` (descending). + */ + async getUsageRecords( + params: GetUsageRecordsOptions = {} + ): Promise { + return this._query({ + path: "v1/usage-records", + params: this._buildParams(params, ["subscriptionItemId"]), + }) as Promise; + } + + /** + * Retrieve a usage record. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getUsageRecord` function + * instead. + * + * @param params.id The usage record id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A usage record object. + */ + async getUsageRecord(p: GetUsageRecordOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/usage-records/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Create a usage record + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `createUsageRecord` function + * instead. + * + * @param params + * @param params.subscriptionItemId The ID of the subscription item to report usage for + * @param params.quantity The number of units to report + * @param [params.action] Type of record + * + * @returns A usage record object. + */ + async createUsageRecord( + p: CreateUsageRecordOptions + ): Promise { + const { subscriptionItemId, quantity, action } = p || {}; + requiredCheck({ subscriptionItemId, quantity }); + + return this._query({ + path: "v1/usage-records", + method: "POST", + payload: { + data: { + type: "usage-records", + attributes: { + quantity, + action, + }, + relationships: { + "subscription-item": { + data: { + type: "subscription-items", + id: subscriptionItemId.toString(), + }, + }, + }, + }, + }, + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Discounts */ + /* -------------------------------------------------------------------------- */ + + /** + * List all discounts. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listDiscounts` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return discounts belonging to the store with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of discount objects ordered by `created_at`. + */ + async getDiscounts( + params: GetDiscountsOptions = {} + ): Promise { + return this._query({ + path: "v1/discounts", + params: this._buildParams(params, ["storeId"]), + }) as Promise; + } + + /** + * Retrieve a discount. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getDiscount` function + * instead. + * + * @param params.id The usage record id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A discount object. + */ + async getDiscount(p: GetDiscountOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/discounts/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Create a discount + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `createDiscount` function + * instead. + * + * @param params + * @param params.storeId Store to create a discount in + * @param params.name Name of discount + * @param params.code Discount code (uppercase letters and numbers, between 3 and 256 characters) + * @param params.amount Amount the discount is for + * @param params.amountType Type of discount + * @param [params.duration] Duration of discount + * @param [params.durationInMonths] Number of months to repeat the discount for + * @param [params.variantIds] Limit the discount to certain variants + * @param [params.maxRedemptions] The total number of redemptions allowed + * @param [params.startsAt] Date the discount code starts on (ISO 8601 format) + * @param [params.expiresAt] Date the discount code expires on (ISO 8601 format) + * + * @returns A discount object. + */ + async createDiscount(p: CreateDiscountOptions): Promise { + const { + storeId, + name, + code, + amount, + amountType = "fixed", + duration = "once", + durationInMonths, + variantIds, + maxRedemptions, + startsAt, + expiresAt, + } = p || {}; + requiredCheck({ storeId, name, code, amount }); + + const attributes: CreateDiscountAttributes = { + name, + code, + amount, + amount_type: amountType, + duration, + starts_at: startsAt, + expires_at: expiresAt, + }; + + if (durationInMonths && duration != "once") { + attributes.duration_in_months = durationInMonths; + } + + if (maxRedemptions) { + attributes.is_limited_redemptions = true; + attributes.max_redemptions = maxRedemptions; + } + + const relationships: { store: object; variants?: object } = { + store: { + data: { + type: "stores", + id: storeId.toString(), + }, + }, + }; + + if (variantIds) { + const variantData: Array<{ type: string; id: string }> = []; + for (let i = 0; i < variantIds.length; i++) { + variantData.push({ type: "variants", id: "" + variantIds[i] }); + } + attributes.is_limited_to_products = true; + relationships.variants = { + data: variantData, + }; + } + + return this._query({ + path: "v1/discounts", + method: "POST", + payload: { + data: { + type: "discounts", + attributes, + relationships, + }, + }, + }) as Promise; + } + + /** + * Delete a discount. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `deleteDiscount` function + * instead. + * + * @param params.id The given discount id. + * @returns A `204 No Content` response on success. + */ + async deleteDiscount(p: DeleteDiscountOptions): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/discounts/${id}`, + method: "DELETE", + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Discount Redemptions */ + /* -------------------------------------------------------------------------- */ + + /** + * List all discount redemptions. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listDiscountRedemption` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.discountId] (Optional) Only return discount redemptions belonging to the discount with this ID. + * @param [params.filter.orderId] (Optional) Only return discount redemptions belonging to the order with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of discount redemption objects ordered by `created_at` (descending). + */ + async getDiscountRedemptions( + params: GetDiscountRedemptionsOptions = {} + ): Promise { + return this._query({ + path: "v1/discount-redemptions", + params: this._buildParams(params, ["discountId", "orderId"]), + }) as Promise; + } + + /** + * Retrieve a discount redemption. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getDiscountRedemption` function + * instead. + * + * @param discountRedemptionId The given discount redemption id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A discount redemption object. + */ + async getDiscountRedemption( + p: GetDiscountRedemptionOptions + ): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/discount-redemptions/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* License Keys */ + /* -------------------------------------------------------------------------- */ + + /** + * List all license keys. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listLicenseKeys` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return license keys belonging to the store with this ID. + * @param [params.filter.orderId] (Optional) (Optional) Only return license keys belonging to the order with this ID. + * @param [params.filter.orderItemId] (Optional) Only return license keys belonging to the order item with this ID. + * @param [params.filter.productId] (Optional) Only return license keys belonging to the product with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of license key objects ordered by `id`. + */ + async getLicenseKeys( + params: GetLicenseKeysOptions = {} + ): Promise { + return this._query({ + path: "v1/license-keys", + params: this._buildParams(params, [ + "storeId", + "orderId", + "orderItemId", + "productId", + ]), + }) as Promise; + } + + /** + * Retrieve a license key. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getLicenseKey` function + * instead. + * + * @param params parameters. + * @param params.id The license key id. + * @param [params.include] (Optional) Related resources. + * @returns A license key object. + */ + async getLicenseKey(p: GetLicenseKeyOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/license-keys/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* License Keys Instances */ + /* -------------------------------------------------------------------------- */ + + /** + * Get license key instances + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listLicenseKeyInstances` function + * instead. + * + * @param {Object} [params] + * @param {number} [params.licenseKeyId] Filter license keys instances by license key + * @param {number} [params.perPage] Number of records to return (between 1 and 100) + * @param {number} [params.page] Page of records to return + * @param {Array<"license-key">} [params.include] List of record types to include + * + * @returns {Object} JSON + */ + async getLicenseKeyInstances( + params: GetLicenseKeyInstancesOptions = {} + ): Promise { + return this._query({ + path: "v1/license-key-instances", + params: this._buildParams(params, ["licenseKeyId"]), + }) as Promise; + } + + /** + * Retrieve a license key instance. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getLicenseKeyInstance` function + * instead. + * + * @param params.id The given license key instance id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A license key instance object. + */ + async getLicenseKeyInstance({ + id, + ...params + }: GetLicenseKeyInstanceOptions): Promise { + if (!id) throw "You must provide an `id` in getLicenseKeyInstance()."; + + return this._query({ + path: `v1/license-key-instances/${id}}`, + params: this._buildParams(params), + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Checkouts */ + /* -------------------------------------------------------------------------- */ + + /** + * List all checkouts. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listCheckouts` function + * instead. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return products belonging to the store with this ID. + * @param [params.filter.variantId] (Optional) Only return products belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of checkout objects ordered by `created_at` (descending). + */ + async getCheckouts( + params: GetCheckoutsOptions = {} + ): Promise { + return this._query({ + path: "v1/checkouts", + params: this._buildParams(params, ["storeId", "variantId"]), + }) as Promise; + } + + /** + * Retrieve a checkout. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getCheckout` function + * instead. + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getCheckout` function + * instead. + * + * @param params.id (Required) The checkout id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A checkout object. + */ + async getCheckout(p: GetCheckoutOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/checkouts/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Create a checkout + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `createCheckout` function + * instead. + * + * @param params + * @param params.storeId + * @param params.variantId + * @param [params.attributes] An object of values used to configure the checkout + * + * @see https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout + * + * @returns {Object} JSON + */ + async createCheckout(p: CreateCheckoutOptions): Promise { + const { storeId, variantId, attributes } = p || {}; + requiredCheck({ storeId, variantId }); + + return this._query({ + path: "v1/checkouts", + method: "POST", + payload: { + data: { + type: "checkouts", + attributes: attributes, + relationships: { + store: { + data: { + type: "stores", + id: "" + storeId, // convert to string + }, + }, + variant: { + data: { + type: "variants", + id: "" + variantId, // convert to string + }, + }, + }, + }, + }, + }) as Promise; + } + + /* -------------------------------------------------------------------------- */ + /* Webhooks */ + /* -------------------------------------------------------------------------- */ + + /** + * Get webhooks + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `listWebhooks` function + * instead. + * + * @param [params] + * @param [params.storeId] Filter webhooks by store + * @param [params.perPage] Number of records to return (between 1 and 100) + * @param [params.page] Page of records to return + * @param [params.include] List of record types to include + * + * @returns A webhook object. + */ + async getWebhooks( + params: GetWebhooksOptions = {} + ): Promise { + return this._query({ + path: "v1/webhooks", + params: this._buildParams(params, ["storeId"]), + }) as Promise; + } + + /** + * Get a webhook + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `getWebhook` function + * instead. + * + * @param params + * @param params.id + * @param [params.include] List of record types to include + * + * @returns A webhook object. + */ + async getWebhook(p: GetWebhookOptions): Promise { + const { id, ...params } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/webhooks/${id}`, + params: this._buildParams(params), + }) as Promise; + } + + /** + * Create a webhook + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `createWebhook` function + * instead. + * + * @param params + * @param params.storeId ID of the store the webhook is for + * @param params.url Endpoint URL that the webhooks should be sent to + * @param params.events List of webhook events to receive + * @param params.secret Signing secret (between 6 and 40 characters) + * + * @returns A webhook object. + */ + async createWebhook(p: CreateWebhookOptions): Promise { + const { storeId, url, events, secret } = p || {}; + requiredCheck({ storeId, url, events, secret }); + + return this._query({ + path: "v1/webhooks", + method: "POST", + payload: { + data: { + type: "webhooks", + attributes: { + url, + events, + secret, + }, + relationships: { + store: { + data: { + type: "stores", + id: "" + storeId, + }, + }, + }, + }, + }, + }) as Promise; + } + + /** + * Update a webhook + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `updateWebhook` function + * instead. + * + * @param params + * @param params.id + * @param [params.url] Endpoint URL that the webhooks should be sent to + * @param [params.events] List of webhook events to receive + * @param [params.secret] Signing secret (between 6 and 40 characters) + * + * @returns A webhook object. + */ + async updateWebhook(p: UpdateWebhookOptions): Promise { + const { id, url, events, secret } = p || {}; + requiredCheck({ id }); + + const attributes: { url?: string; events?: string[]; secret?: string } = {}; + + if (url) { + attributes.url = url; + } + + if (events) { + attributes.events = events; + } + + if (secret) { + attributes.secret = secret; + } + + return this._query({ + path: `v1/webhooks/${id}`, + method: "PATCH", + payload: { + data: { + type: "webhooks", + id: "" + id, + attributes, + }, + }, + }) as Promise; + } + + /** + * Delete a webhook + * + * @deprecated Deprecated since version 2.0.0. It will be removed with the + * next major version. Use the new setup method and `deleteWebhook` function + * instead. + * + * @param params + * @param params.id + */ + async deleteWebhook(p: DeleteWebhookOptions): Promise { + const { id } = p || {}; + requiredCheck({ id }); + + return this._query({ + path: `v1/webhooks/${id}`, + method: "DELETE", + }) as Promise; + } +} diff --git a/src/_deprecated/index.ts b/src/_deprecated/index.ts new file mode 100644 index 0000000..43e910c --- /dev/null +++ b/src/_deprecated/index.ts @@ -0,0 +1 @@ +export { LemonSqueezy } from "./LemonSqueezy"; diff --git a/src/types/api.ts b/src/_deprecated/types/api.ts similarity index 98% rename from src/types/api.ts rename to src/_deprecated/types/api.ts index 9220426..8c97b4f 100644 --- a/src/types/api.ts +++ b/src/_deprecated/types/api.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ interface BaseListResponse { meta: object; jsonapi: { @@ -22,7 +23,6 @@ interface BaseApiObject { links: object; } - interface SubscriptionAttributes { /** * The ID of the store this subscription belongs to. @@ -71,11 +71,25 @@ interface SubscriptionAttributes { /** * The title-case formatted status of the subscription. */ - status_formatted: "On Trial" | "Active" | "Paused" | "Unpaid" | "Cancelled" | "Expired"; + status_formatted: + | "On Trial" + | "Active" + | "Paused" + | "Unpaid" + | "Cancelled" + | "Expired"; /** * Lowercase brand of the card used to pay for the latest subscription payment. */ - card_brand: "visa" | "mastercard" | "amex" | "discover" | "jcb" | "diners" | "unionpay" | null; + card_brand: + | "visa" + | "mastercard" + | "amex" + | "discover" + | "jcb" + | "diners" + | "unionpay" + | null; /** * The last 4 digits of the card used to pay for the latest subscription payment. */ @@ -90,7 +104,7 @@ interface SubscriptionAttributes { * - `void` - If you can't offer your services for a period of time (for maintenance as an example), you can void invoices so your customers aren't charged * - `free` - Offer your subscription services for free, whilst halting payment collection */ - mode: 'void' | 'free'; + mode: "void" | "free"; /** * An ISO-8601 formatted date-time string indicating when the subscription will continue collecting payments */ @@ -172,7 +186,6 @@ interface SubscriptionAttributes { test_mode: boolean; } - interface SubscriptionObject extends BaseApiObject { attributes: SubscriptionAttributes; } @@ -185,7 +198,6 @@ export interface SubscriptionResponse extends BaseIndividualResponse { data: SubscriptionObject; } - interface StoreAttributes { /** * The name of the store. @@ -261,7 +273,6 @@ export interface StoreResponse extends BaseIndividualResponse { data: StoreObject; } - interface CustomerAttributes { /** * The ID of the store this customer belongs to. @@ -278,7 +289,13 @@ interface CustomerAttributes { /** * The email marketing status of the customer. */ - status: "subscribed" | "unsubscribed" | "archived" | "requires_verification" | "invalid_email" | "bounced"; + status: + | "subscribed" + | "unsubscribed" + | "archived" + | "requires_verification" + | "invalid_email" + | "bounced"; /** * The city of the customer. */ @@ -302,7 +319,13 @@ interface CustomerAttributes { /** * The formatted status of the customer. */ - status_formatted: "Subscribed" | "Unsubscribed" | "Archived" | "Requires Verification" | "Invalid Email" | "Bounced"; + status_formatted: + | "Subscribed" + | "Unsubscribed" + | "Archived" + | "Requires Verification" + | "Invalid Email" + | "Bounced"; /** * The formatted country of the customer. */ @@ -350,7 +373,6 @@ export interface CustomerResponse extends BaseIndividualResponse { data: CustomerObject; } - interface UserAttributes { /** * The name of the user. @@ -390,7 +412,6 @@ export interface UserResponse extends BaseIndividualResponse { data: UserObject; } - interface ProductAttributes { /** * The ID of the store this product belongs to. @@ -474,7 +495,6 @@ export interface ProductResponse extends BaseIndividualResponse { data: ProductObject; } - interface VariantAttributes { /** * The ID of the product this variant belongs to. @@ -594,7 +614,6 @@ export interface VariantResponse extends BaseIndividualResponse { data: VariantObject; } - interface CheckoutProductOptions { /** * A custom name for the product. @@ -764,7 +783,7 @@ interface CheckoutPreview { /** * A human-readable string representing the total price of the checkout in the store currency. */ - total_formatted: string; + total_formatted: string; } interface CheckoutAttributes { @@ -830,7 +849,6 @@ export interface CheckoutResponse extends BaseIndividualResponse { data: CheckoutObject; } - interface OrderAttributes { /** * The ID of the store this order belongs to. @@ -1013,7 +1031,6 @@ export interface OrderResponse extends BaseIndividualResponse { data: OrderObject; } - interface FileAttributes { /** * The ID of the variant this file belongs to. @@ -1081,7 +1098,6 @@ export interface FileResponse extends BaseIndividualResponse { data: FileObject; } - interface SubscriptionInvoiceAttributes { /** * The ID of the Store this subscription invoice belongs to. @@ -1110,7 +1126,15 @@ interface SubscriptionInvoiceAttributes { /** * Lowercase brand of the card used to pay for the invoice. */ - card_brand: "visa" | "mastercard" | "amex" | "discover" | "jcb" | "diners" | "unionpay" | null; + card_brand: + | "visa" + | "mastercard" + | "amex" + | "discover" + | "jcb" + | "diners" + | "unionpay" + | null; /** * The last 4 digits of the card used to pay for the invoice. */ @@ -1222,7 +1246,6 @@ export interface SubscriptionInvoiceResponse extends BaseIndividualResponse { data: SubscriptionInvoiceObject; } - interface SubscriptionItemAttributes { /** * The ID of the Subscription this subscription item belongs to. @@ -1262,7 +1285,6 @@ export interface SubscriptionItemResponse extends BaseIndividualResponse { data: SubscriptionItemObject; } - export interface SubscriptionItemUsageResponse { jsonapi: { version: "1.0"; @@ -1288,10 +1310,9 @@ export interface SubscriptionItemUsageResponse { * The interval count of the subscription's variant. */ interval_quantity: number; - } + }; } - interface UsageRecordAttributes { /** * The ID of the Subscription item this usage record belongs to. @@ -1327,7 +1348,6 @@ export interface UsageRecordResponse extends BaseIndividualResponse { data: UsageRecordObject; } - interface DiscountAttributes { /** * The ID of the store this discount belongs to. @@ -1411,7 +1431,6 @@ export interface DiscountResponse extends BaseIndividualResponse { data: DiscountObject; } - interface DiscountRedemptionAttributes { /** * The ID of the discount this redemption belongs to. @@ -1463,7 +1482,6 @@ export interface DiscountRedemptionResponse extends BaseIndividualResponse { data: DiscountRedemptionObject; } - interface LicenseKeyAttributes { /** * The ID of the store this license key belongs to. @@ -1547,7 +1565,6 @@ export interface LicenseKeyResponse extends BaseIndividualResponse { data: LicenseKeyObject; } - interface LicenseKeyInstanceAttributes { /** * The ID of the license key this instance belongs to. diff --git a/src/types/methods.ts b/src/_deprecated/types/methods.ts similarity index 96% rename from src/types/methods.ts rename to src/_deprecated/types/methods.ts index 49ab55a..e3526ee 100644 --- a/src/types/methods.ts +++ b/src/_deprecated/types/methods.ts @@ -1,4 +1,4 @@ -import { WebhookEvent } from "./api"; +import type { WebhookEvent } from "./api"; /** * A union of all possible API versions available. @@ -50,7 +50,7 @@ export interface GetStoreOptions { /** * The ID of the store to retrieve */ - id: string; + id: string | number; /** * List of record types to include */ @@ -289,7 +289,9 @@ export interface GetSubscriptionsOptions extends PaginatedOptions { /** * List of record types to include */ - include?: Array<"store" | "customer" | "order" | "order-item" | "product" | "variant">; + include?: Array< + "store" | "customer" | "order" | "order-item" | "product" | "variant" + >; /** * Filter subscriptions by store */ @@ -313,7 +315,14 @@ export interface GetSubscriptionsOptions extends PaginatedOptions { /** * Filter subscriptions by status */ - status?: "on_trial" | "active" | "paused" | "past_due" | "unpaid" | "cancelled" | "expired"; + status?: + | "on_trial" + | "active" + | "paused" + | "past_due" + | "unpaid" + | "cancelled" + | "expired"; } export interface GetSubscriptionOptions { @@ -324,10 +333,11 @@ export interface GetSubscriptionOptions { /** * List of record types to include */ - include?: Array<"store" | "customer" | "order" | "order-item" | "product" | "variant">; + include?: Array< + "store" | "customer" | "order" | "order-item" | "product" | "variant" + >; } - export interface BaseUpdateSubscriptionOptions { /** * The ID of the subscription to update @@ -335,7 +345,8 @@ export interface BaseUpdateSubscriptionOptions { id: number; } -export interface UpdateSubscriptionOptions extends BaseUpdateSubscriptionOptions { +export interface UpdateSubscriptionOptions + extends BaseUpdateSubscriptionOptions { /** * The ID of the product (required when changing plans) */ @@ -377,7 +388,8 @@ export interface UpdateSubscriptionAttributes { invoice_immediately?: boolean; } -export interface PauseSubscriptionOptions extends BaseUpdateSubscriptionOptions { +export interface PauseSubscriptionOptions + extends BaseUpdateSubscriptionOptions { /** * Type of pause * @@ -758,7 +770,6 @@ export interface GetWebhookOptions { include?: Array<"store">; } - export interface CreateWebhookOptions { /** * ID of the store the webhook is for diff --git a/src/checkouts/index.ts b/src/checkouts/index.ts new file mode 100644 index 0000000..90233d7 --- /dev/null +++ b/src/checkouts/index.ts @@ -0,0 +1,119 @@ +import { + $fetch, + convertIncludeToQueryString, + convertKeys, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + Checkout, + GetCheckoutParams, + ListCheckouts, + ListCheckoutsParams, + NewCheckout, +} from "./types"; + +/** + * Create a checkout. + * + * @param storeId (Required) The given store id. + * @param variantId (Required) The given variant id. + * @param [checkout] (Optional) A new checkout info. + * @returns A checkout object. + * + * @see https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout + */ +export function createCheckout( + storeId: number | string, + variantId: number | string, + checkout: NewCheckout = {} +) { + requiredCheck({ storeId, variantId }); + + const { + customPrice, + productOptions, + checkoutOptions, + checkoutData, + expiresAt, + preview, + testMode, + } = checkout; + const relationships = { + store: { + data: { + type: "stores", + id: storeId.toString(), + }, + }, + variant: { + data: { + type: "variants", + id: variantId.toString(), + }, + }, + }; + const attributes = { + customPrice, + expiresAt, + preview, + testMode, + productOptions, + checkoutOptions, + checkoutData: { + ...checkoutData, + variantQuantities: checkoutData?.variantQuantities?.map((item) => + convertKeys(item) + ), + }, + }; + + return $fetch({ + path: "/v1/checkouts", + method: "POST", + body: { + data: { + type: "checkouts", + attributes: convertKeys(attributes), + relationships, + }, + }, + }); +} + +/** + * Retrieve a checkout. + * + * @param checkoutId (Required) The checkout id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A checkout object. + */ +export function getCheckout( + checkoutId: number | string, + params: GetCheckoutParams = {} +) { + requiredCheck({ checkoutId }); + return $fetch({ + path: `/v1/checkouts/${checkoutId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all checkouts. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return products belonging to the store with this ID. + * @param [params.filter.variantId] (Optional) Only return products belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of checkout objects ordered by `created_at` (descending). + */ +export function listCheckouts(params: ListCheckoutsParams = {}) { + return $fetch({ + path: `/v1/checkouts${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/checkouts/types.ts b/src/checkouts/types.ts new file mode 100644 index 0000000..33a37b8 --- /dev/null +++ b/src/checkouts/types.ts @@ -0,0 +1,509 @@ +import type { + Data, + ISO3166Alpha2CountryCode, + ISO4217CurrencyCode, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type ProductOptions = { + /** + * A custom name for the product + */ + name: string; + /** + * A custom description for the product + */ + description: string; + /** + * An array of image URLs to use as the product's media + */ + media: string[]; + /** + * A custom URL to redirect to after a successful purchase + */ + redirect_url: string; + /** + * A custom text to use for the order receipt email button + */ + receipt_button_text: string; + /** + * A custom URL to use for the order receipt email button + */ + receipt_link_url: string; + /** + * A custom thank you note to use for the order receipt email + */ + receipt_thank_you_note: string; + /** + * An array of variant IDs to enable for this checkout. If this is empty, all variants will be enabled. + */ + enabled_variants: number[]; + /** + * A custom text to use for order payment success message alert title + * + * Note: Not in the documentation, but in the response + */ + confirmation_title: string; + /** + * A custom text to use for order payment success message alert content + * + * Note: Not in the documentation, but in the response + */ + confirmation_message: string; + /** + * A custom text to use for order payment success message alert button + * + * Note: Not in the documentation, but in the response + */ + confirmation_button_text: string; +}; +type CheckoutOptions = { + /** + * If `true`, show the [checkout overlay](https://docs.lemonsqueezy.com/help/checkout/checkout-overlay) + */ + embed: boolean; + /** + * If `false`, hide the product media + */ + media: boolean; + /** + * If `false`, hide the store logo + */ + logo: boolean; + /** + * If `false`, hide the product description + */ + desc: boolean; + /** + * If `false`, hide the discount code field + */ + discount: boolean; + quantity: number; + /** + * If `true`, use the dark theme + */ + dark: boolean; + /** + * If `false`, hide the "You will be charged..." subscription preview text + */ + subscription_preview: boolean; + /** + * A custom hex color to use for the checkout button. Text within the button will be either white or dark depending on the brightness of your button color. + */ + button_color: string; +}; +type CheckoutData = { + /** + * A pre-filled email address + */ + email: string; + /** + * A pre-filled name + */ + name: string; + billing_address: { + /** + * A pre-filled billing address country in a [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format + */ + country: ISO3166Alpha2CountryCode; + /** + * A pre-filled billing address zip/postal code + */ + zip: string; + }; + /** + * A pre-filled tax number + */ + tax_number: string; + /** + * A pre-filled discount code + */ + discount_code: string; + /** + * An object containing any custom data to be passed to the checkout + */ + custom: unknown[] | Record; + /** + * An array of variant IDs to enable for this checkout. If this is empty, all variants will be enabled. + */ + variant_quantities: number[]; +}; +type Preview = { + /** + * The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code of the store (e.g. `USD`, `GBP`, etc). + */ + currency: ISO4217CurrencyCode; + /** + * If the store currency is USD, this will always be `1.0`. Otherwise, this is the currency conversion rate used to determine the price of the checkout in USD at the time of purchase. + */ + currency_rate: number; + /** + * A positive integer in cents representing the subtotal of the checkout in the store currency. + */ + subtotal: number; + /** + * A positive integer in cents representing the total discount value applied to the checkout in the store currency. + */ + discount_total: number; + /** + * A positive integer in cents representing the tax applied to the checkout in the store currency. + */ + tax: number; + /** + * A positive integer in cents representing the setup fee of the checkout in USD. + */ + setup_fee_usd: number; + /** + * A positive integer in cents representing the setup fee. + */ + setup_fee: number; + /** + * A positive integer in cents representing the total price of the checkout in the store currency. + */ + total: number; + /** + * A positive integer in cents representing the subtotal of the checkout in USD. + */ + subtotal_usd: number; + /** + * A positive integer in cents representing the total discount value applied to the checkout in USD. + */ + discount_total_usd: number; + /** + * A positive integer in cents representing the tax applied to the checkout in USD. + */ + tax_usd: number; + /** + * A positive integer in cents representing the total price of the checkout in USD. + */ + total_usd: number; + /** + * A human-readable string representing the subtotal of the checkout in the store currency (e.g. `$9.99`). + */ + subtotal_formatted: string; + /** + * A human-readable string representing the total discount value applied to the checkout in the store currency (e.g. `$9.99`). + */ + discount_total_formatted: string; + /** + * A human-readable string representing the setup fee of the checkout in the store currency (e.g. `$9.99`). + */ + setup_fee_formatted: string; + /** + * A human-readable string representing the tax applied to the checkout in the store currency (e.g. `$9.99`). + */ + tax_formatted: string; + /** + * A human-readable string representing the total price of the checkout in the store currency (e.g. `$9.99`). + */ + total_formatted: string; +}; +type Attributes = { + /** + * The ID of the store this checkout belongs to. + */ + store_id: number; + /** + * The ID of the variant associated with this checkout. + * + * Note: by default, all variants of the related product will be shown in the checkout, with your selected variant highlighted. If you want hide to other variants, you can utilise the `product_options.enabled_variants` option to determine which variant(s) are displayed in the checkout. + */ + variant_id: number; + /** + * If the value is not `null`, this represents a positive integer in cents representing the custom price of the variant. + */ + custom_price: null | number; + /** + * An object containing any overridden product options for this checkout. Possible options include: + * + * - `name` - A custom name for the product + * - `description` - A custom description for the product + * - `media` - An array of image URLs to use as the product's media + * - `redirect_url` - A custom URL to redirect to after a successful purchase + * - `receipt_button_text` - A custom text to use for the order receipt email button + * - `receipt_link_url` - A custom URL to use for the order receipt email button + * - `receipt_thank_you_note` - A custom thank you note to use for the order receipt email + * - `enabled_variants` - An array of variant IDs to enable for this checkout. If this is empty, all variants will be enabled. + */ + product_options: ProductOptions; + /** + * An object containing checkout options for this checkout. Possible options include: + * + * - `embed` - If `true`, show the [checkout overlay](https://docs.lemonsqueezy.com/help/checkout/checkout-overlay) + * - `media` - If `false`, hide the product media + * - `logo` - If `false`, hide the store logo + * - `desc` - If `false`, hide the product description + * - `discount` - If `false`, hide the discount code field + * - `dark` - If `true`, use the dark theme + * - `subscription_preview` - If `false`, hide the "You will be charged..." subscription preview text + * - `button_color` - A custom hex color to use for the checkout button. Text within the button will be either white or dark depending on the brightness of your button color. + */ + checkout_options: CheckoutOptions; + /** + * An object containing any [prefill](https://docs.lemonsqueezy.com/help/checkout/prefilling-checkout-fields) or [custom data](https://docs.lemonsqueezy.com/help/checkout/passing-custom-data) to be used in the checkout. Possible options include: + * + * - `email` - A pre-filled email address + * - `name` - A pre-filled name + * - `billing_address.country` - A pre-filled billing address country in a [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format + * - `billing_address.zip` - A pre-filled billing address zip/postal code + * - `tax_number` - A pre-filled tax number + * - `discount_code` - A pre-filled discount code + * - `custom` - An object containing any custom data to be passed to the checkout + * - `variant_quantities` - A list containing quantity data objects + */ + checkout_data: CheckoutData; + /** + * If `preview` is passed as `true` in the request, the Checkout object will contain a `preview` object. This contains pricing information for the checkout, including tax, any discount applied, and the total price. + * + * The `preview` object is only available when the checkout is created. + * + * Values returned: + * + * - `currency` - The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code of the store (e.g. `USD`, `GBP`, etc). + * - `currency_rate` - If the store currency is USD, this will always be `1.0`. Otherwise, this is the currency conversion rate used to determine the price of the checkout in USD at the time of purchase. + * - `subtotal` - A positive integer in cents representing the subtotal of the checkout in the store currency. + * - `discount_total` - A positive integer in cents representing the total discount value applied to the checkout in the store currency. + * - `tax` - A positive integer in cents representing the tax applied to the checkout in the store currency. + * - `total` - A positive integer in cents representing the total price of the checkout in the store currency. + * - `subtotal_usd` - A positive integer in cents representing the subtotal of the checkout in USD. + * - `discount_total_usd` - A positive integer in cents representing the total discount value applied to the checkout in USD. + * - `tax_usd` - A positive integer in cents representing the tax applied to the checkout in USD. + * - `total_usd` - A positive integer in cents representing the total price of the checkout in USD. + * - `subtotal_formatted` - A human-readable string representing the subtotal of the checkout in the store currency (e.g. `$9.99`). + * - `discount_total_formatted` - A human-readable string representing the total discount value applied to the checkout in the store currency (e.g. `$9.99`). + * - `tax_formatted` - A human-readable string representing the tax applied to the checkout in the store currency (e.g. `$9.99`). + * - `total_formatted` - A human-readable string representing the total price of the checkout in the store currency (e.g. `$9.99`). + */ + preview: Preview; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the checkout expires. Can be `null` if the checkout is perpetual. + */ + expires_at: null | string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; + /** + * The unique URL to access the checkout. Note: for security reasons, download URLs are signed. If the checkout `expires_at` is set, the URL will expire after the specified time. + */ + url: string; +}; +type CheckoutResponseData = Data< + Attributes, + Pick +>; + +export type NewCheckout = { + /** + * The store this checkout belongs to. + */ + // storeId: string | number + /** + * The variant associated with this checkout. + * + * Note: by default, all variants of the related product will be shown in the checkout, with your selected variant highlighted. If you want hide to other variants, you can utilize the `productOptions.enabledVariants` option to determine which variant(s) are displayed in the checkout. + */ + // variantId: string | number + /** + * A positive integer in cents representing the custom price of the variant. + * + * Note: If the product purchased is a subscription, this custom price is used for all renewal payments. If the subscription's variant changes in the future (i.e. the customer is moved to a different subscription "tier") the new variant's price will be used from that moment forward. + */ + customPrice?: number; + /** + * An object containing any overridden product options for this checkout. Possible options include: + * + * - `name` - A custom name for the product + * - `description` - A custom description for the product + * - `media` - An array of image URLs to use as the product's media + * - `redirectUrl` - A custom URL to redirect to after a successful purchase + * - `receiptButtonText` - A custom text to use for the order receipt email button + * - `receiptLinkUrl` - A custom URL to use for the order receipt email button + * - `receiptThankYouNote` - A custom thank you note to use for the order receipt email + * - `enabledVariants` - An array of variant IDs to enable for this checkout. If this is empty, all variants will be enabled. + */ + productOptions?: { + /** + * A custom name for the product + */ + name?: string; + /** + * A custom description for the product + */ + description?: string; + /** + * An array of image URLs to use as the product's media + */ + media?: string[]; + /** + * A custom URL to redirect to after a successful purchase + */ + redirectUrl?: string; + /** + * A custom text to use for the order receipt email button + */ + receiptButtonText?: string; + /** + * A custom URL to use for the order receipt email button + */ + receiptLinkUrl?: string; + /** + * A custom thank you note to use for the order receipt email + */ + receiptThankYouNote?: string; + /** + * An array of variant IDs to enable for this checkout. If this is empty, all variants will be enabled. + */ + enabledVariants?: number[]; + /** + * A custom text to use for order payment success message alert title + */ + confirmationTitle?: string; + /** + * A custom text to use for order payment success message alert content + */ + confirmationMessage?: string; + /** + * A custom text to use for order payment success message alert button + */ + confirmationButtonText?: string; + }; + /** + * An object containing checkout options for this checkout. Possible options include: + * + * - `embed` - If `true`, show the [checkout overlay](https://docs.lemonsqueezy.com/help/checkout/checkout-overlay) + * - `media` - If `false`, hide the product media + * - `logo` - If `false`, hide the store logo + * - `desc` - If `false`, hide the product description + * - `discount` - If `false`, hide the discount code field + * - `dark` - If `true`, use the dark theme + * - `subscription_preview` - If `false`, hide the "You will be charged..." subscription preview text + * - `buttonColor` - A custom hex color to use for the checkout button. Text within the button will be either white or dark depending on the brightness of your button color. + */ + checkoutOptions?: { + /** + * If `true`, show the checkout overlay + */ + embed?: boolean; + /** + * If `false`, hide the product media + */ + media?: boolean; + /** + * If `false`, hide the store logo + */ + logo?: boolean; + /** + * If `false`, hide the product description + */ + desc?: boolean; + /** + * If `false`, hide the discount code field + */ + discount?: boolean; + /** + * If `true`, use the dark theme + */ + dark?: boolean; + /** + * If `false`, hide the "You will be charged..." subscription preview text + */ + subscriptionPreview?: boolean; + /** + * A custom hex color to use for the checkout button + */ + buttonColor?: string; + }; + /** + * An object containing any [prefill](https://docs.lemonsqueezy.com/help/checkout/prefilling-checkout-fields) or [custom data](https://docs.lemonsqueezy.com/help/checkout/passing-custom-data) to be used in the checkout. Possible options include: + * + * - `email` - A pre-filled email address + * - `name` - A pre-filled name + * - `billingAddress.country` - A pre-filled billing address country in a [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format + * - `billingAddress.zip` - A pre-filled billing address zip/postal code + * - `taxNumber` - A pre-filled tax number + * - `discountCode` - A pre-filled discount code + * - `custom` - An object containing any custom data to be passed to the checkout + * - `variantQuantities` - A list containing quantity data objects + */ + checkoutData?: { + /** + * A pre-filled email address + */ + email?: string; + /** + * A pre-filled name + */ + name?: string; + billingAddress?: { + /** + * A pre-filled billing address country in a [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format + */ + country?: ISO3166Alpha2CountryCode; + /** + * A pre-filled billing address zip/postal code + */ + zip?: string; + }; + /** + * A pre-filled tax number + */ + taxNumber?: string; + /** + * A pre-filled discount discountCode + */ + discountCode?: string; + /** + * An object containing any custom data to be passed to the checkout + */ + custom?: Record; + /** + * A list containing quantity data objects + */ + variantQuantities?: { + variantId: number; + quantity: number; + }[]; + }; + /** + * A boolean indicating whether to return a preview of the checkout. If `true`, the checkout will include a `preview` object with the checkout preview data. + */ + preview?: boolean; + /** + * A boolean indicating whether the checkout should be created in test mode. + */ + testMode?: boolean; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the checkout expires. Can be null if the checkout is perpetual. + */ + expiresAt?: null | string; +}; +export type GetCheckoutParams = Pick< + Params<(keyof CheckoutResponseData["relationships"])[]>, + "include" +>; +export type ListCheckoutsParams = Params< + GetCheckoutParams["include"], + { storeId?: number | string; variantId?: string | number } +>; +export type Checkout = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListCheckouts = LemonSqueezyResponse< + CheckoutResponseData[], + Pick, + Omit +>; diff --git a/src/customers/index.ts b/src/customers/index.ts new file mode 100644 index 0000000..d596f75 --- /dev/null +++ b/src/customers/index.ts @@ -0,0 +1,142 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + Customer, + GetCustomerParams, + ListCustomers, + ListCustomersParams, + NewCustomer, + UpdateCustomer, +} from "./types"; + +/** + * Create a customer. + * + * @param storeId (Required)The Store ID. + * @param customer (Required) The new customer information. + * @param customer.name (Required) The name of the customer. + * @param customer.email (Required) The email of the customer. + * @param customer.city (Optional) The city of the customer. + * @param customer.region (Optional) The region of the customer. + * @param customer.country (Optional) The [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) two-letter country code for the customer (e.g. `US`, `GB`, etc). + * @returns A customer object. + */ +export function createCustomer( + storeId: number | string, + customer: NewCustomer +) { + requiredCheck({ storeId }); + return $fetch({ + path: "/v1/customers", + method: "POST", + body: { + data: { + type: "customers", + attributes: customer, + relationships: { + store: { + data: { + type: "stores", + id: storeId.toString(), + }, + }, + }, + }, + }, + }); +} + +/** + * Update a customer. + * + * @param customerId The customer id. + * @param customer The customer information that needs to be updated. + * @param customer.name (Optional) The name of the customer. + * @param customer.email (Optional) The email of the customer. + * @param customer.city (Optional) The city of the customer. + * @param customer.region (Optional) The region of the customer. + * @param customer.country (Optional) The [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) two-letter country code for the customer (e.g. `US`, `GB`, etc). + * @param customer.status (Optional) The email marketing status of the customer. Only one value: `archived`. + * @returns A customer object. + */ +export function updateCustomer( + customerId: string | number, + customer: UpdateCustomer +) { + requiredCheck({ customerId }); + return $fetch({ + path: `/v1/customers/${customerId}`, + method: "PATCH", + body: { + data: { + type: "customers", + id: customerId.toString(), + attributes: customer, + }, + }, + }); +} + +/** + * Archive a customer. + * + * @param customerId The customer id. + * @returns A customer object. + */ +export function archiveCustomer(customerId: string | number) { + requiredCheck({ customerId }); + return $fetch({ + path: `/v1/customers/${customerId}`, + method: "PATCH", + body: { + data: { + type: "customers", + id: customerId.toString(), + attributes: { + status: "archived", + }, + }, + }, + }); +} + +/** + * Retrieve a customer. + * + * @param customerId The given customer id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A customer object. + */ +export function getCustomer( + customerId: string | number, + params: GetCustomerParams = {} +) { + requiredCheck({ customerId }); + return $fetch({ + path: `/v1/customers/${customerId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all customers. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return customers belonging to the store with this ID. + * @param [params.filter.email] (Optional) Only return customers where the email field is equal to this email address. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of customer objects ordered by `created_at` (descending). + */ +export function listCustomers(params: ListCustomersParams = {}) { + return $fetch({ + path: `/v1/customers${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/customers/types.ts b/src/customers/types.ts new file mode 100644 index 0000000..29cac50 --- /dev/null +++ b/src/customers/types.ts @@ -0,0 +1,122 @@ +import type { + Data, + ISO3166Alpha2CountryCode, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; +type MarketingStatus = + | "subscribed" + | "unsubscribed" + | "archived" + | "requires_verification" + | "invalid_email" + | "bounced"; +type Attributes = { + /** + * The ID of the store this customer belongs to. + */ + store_id: number; + /** + * The full name of the customer. + */ + name: string; + /** + * The email address of the customer. + */ + email: string; + /** + * The email marketing status of the customer. One of + * + * - `subscribed` - This customer is subscribed to marketing emails. + * - `unsubscribed` - This customer has unsubscribed from marketing emails. + * - `archived` - This customer has been archived and will no longer receive marketing emails. + * - `requires_verification` - The customers email address need to be verified (happens automatically). + * - `invalid_email` - The customers email address has failed validation. + * - `bounced` - The customers email has hard bounced. + */ + status: MarketingStatus; + /** + * The city of the customer. + */ + city: string | null; + /** + * The region of the customer. + */ + region: string | null; + /** + * The country of the customer. + */ + country: ISO3166Alpha2CountryCode | null; + /** + * A positive integer in cents representing the total revenue from the customer (USD). + */ + total_revenue_currency: number; + /** + * A positive integer in cents representing the monthly recurring revenue from the customer (USD). + */ + mrr: number; + /** + * The formatted status of the customer. + */ + status_formatted: string; + /** + * The formatted country of the customer. + */ + country_formatted: string | null; + /** + * A human-readable string representing the total revenue from the customer (e.g. `$9.99`). + */ + total_revenue_currency_formatted: string; + /** + * A human-readable string representing the monthly recurring revenue from the customer (e.g. `$9.99`). + */ + mrr_formatted: string; + /** + * An object of customer-facing URLs. It contains: + * + * - `customer_portal` - A pre-signed URL to the [Customer Portal](https://docs.lemonsqueezy.com/help/online-store/customer-portal), which allows customers to fully manage their subscriptions and billing information from within your application. The URL is valid for 24 hours from time of request. Will be `null` if the customer has not bought a subscription in your store. + */ + urls: { + customer_portal: string | null; + }; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type CustomerData = Data< + Attributes, + Pick +>; + +export type GetCustomerParams = Pick< + Params<(keyof CustomerData["relationships"])[]>, + "include" +>; +export type ListCustomersParams = Params< + GetCustomerParams["include"], + { storeId?: string | number; email?: string } +>; +export type NewCustomer = Pick & + Partial>; +export type UpdateCustomer = Partial; +export type Customer = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListCustomers = LemonSqueezyResponse< + CustomerData[], + Pick, + Pick +>; diff --git a/src/discountRedemptions/index.ts b/src/discountRedemptions/index.ts new file mode 100644 index 0000000..bf9bfe9 --- /dev/null +++ b/src/discountRedemptions/index.ts @@ -0,0 +1,51 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + DiscountRedemption, + GetDiscountRedemptionParams, + ListDiscountRedemptions, + ListDiscountRedemptionsParams, +} from "./types"; + +/** + * Retrieve a discount redemption. + * + * @param discountRedemptionId The given discount redemption id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A discount redemption object. + */ +export function getDiscountRedemption( + discountRedemptionId: number | string, + params: GetDiscountRedemptionParams = {} +) { + requiredCheck({ discountRedemptionId }); + return $fetch({ + path: `/v1/discount-redemptions/${discountRedemptionId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all discount redemptions. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.discountId] (Optional) Only return discount redemptions belonging to the discount with this ID. + * @param [params.filter.orderId] (Optional) Only return discount redemptions belonging to the order with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of discount redemption objects ordered by `created_at` (descending). + */ +export function listDiscountRedemptions( + params: ListDiscountRedemptionsParams = {} +) { + return $fetch({ + path: `/v1/discount-redemptions${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/discountRedemptions/types.ts b/src/discountRedemptions/types.ts new file mode 100644 index 0000000..552b3e0 --- /dev/null +++ b/src/discountRedemptions/types.ts @@ -0,0 +1,69 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the discount this redemption belongs to. + */ + discount_id: number; + /** + * The ID of the order this redemption belongs to. + */ + order_id: number; + /** + * The name of the discount. + */ + discount_name: string; + /** + * The discount code that was used at checkout. + */ + discount_code: string; + /** + * The amount of the discount. Either a fixed amount in cents or a percentage depending on the value of `discount_amount_type`. + */ + discount_amount: number; + /** + * The type of the discount_amount. Either `percent` or `fixed`. + */ + discount_amount_type: "percent" | "fixed"; + /** + * A positive integer in cents representing the amount of the discount that was applied to the order (in the order currency). + */ + amount: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; +}; +type DiscountRedemptionData = Data< + Attributes, + Pick +>; + +export type GetDiscountRedemptionParams = Pick< + Params<(keyof DiscountRedemptionData["relationships"])[]>, + "include" +>; +export type ListDiscountRedemptionsParams = Params< + GetDiscountRedemptionParams["include"], + { discountId?: number | string; orderId?: number | string } +>; +export type DiscountRedemption = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListDiscountRedemptions = LemonSqueezyResponse< + DiscountRedemptionData[], + Pick, + Pick +>; diff --git a/src/discounts/index.ts b/src/discounts/index.ts new file mode 100644 index 0000000..80b1d50 --- /dev/null +++ b/src/discounts/index.ts @@ -0,0 +1,134 @@ +import { + $fetch, + convertIncludeToQueryString, + convertKeys, + convertListParamsToQueryString, + generateDiscount, + requiredCheck, +} from "../internal"; +import type { + Discount, + GetDiscountParams, + ListDiscounts, + ListDiscountsParams, + NewDiscount, +} from "./types"; + +/** + * Create a discount. + * + * @param discount New discount info. + * @returns A discount object. + */ +export function createDiscount(discount: NewDiscount) { + const { + storeId, + variantIds, + name, + amount, + amountType = "fixed", + code = generateDiscount(), + isLimitedToProducts = false, + isLimitedRedemptions = false, + maxRedemptions = 0, + startsAt = null, + expiresAt = null, + duration = "once", + durationInMonths = 1, + testMode, + } = discount; + + requiredCheck({ storeId, name, code, amount }); + + const attributes = convertKeys({ + name, + amount, + amountType, + code, + isLimitedRedemptions, + isLimitedToProducts, + maxRedemptions, + startsAt, + expiresAt, + duration, + durationInMonths, + testMode, + }); + + const relationships = { + store: { + data: { + type: "stores", + id: storeId.toString(), + }, + }, + variants: { + data: variantIds.map((id) => ({ + type: "variants", + id: id.toString(), + })), + }, + }; + + return $fetch({ + path: "/v1/discounts", + method: "POST", + body: { + data: { + type: "discounts", + attributes, + relationships, + }, + }, + }); +} + +/** + * List all discounts. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return discounts belonging to the store with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of discount objects ordered by `created_at`. + */ +export function listDiscounts(params: ListDiscountsParams = {}) { + return $fetch({ + path: `/v1/discounts${convertListParamsToQueryString(params)}`, + }); +} + +/** + * Retrieve a discount. + * + * @param discountId The given discount id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A discount object. + */ +export function getDiscount( + discountId: number | string, + params: GetDiscountParams = {} +) { + requiredCheck({ discountId }); + return $fetch({ + path: `/v1/discounts/${discountId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * Delete a discount. + * + * @param discountId The given discount id. + * @returns A `204 No Content` response on success. + */ +export function deleteDiscount(discountId: string | number) { + requiredCheck({ discountId }); + return $fetch({ + path: `/v1/discounts/${discountId}`, + method: "DELETE", + }); +} diff --git a/src/discounts/types.ts b/src/discounts/types.ts new file mode 100644 index 0000000..3d27208 --- /dev/null +++ b/src/discounts/types.ts @@ -0,0 +1,181 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type AmountType = "percent" | "fixed"; +type Duration = "once" | "repeating" | "forever"; +type Attributes = { + /** + * The ID of the store this discount belongs to. + */ + store_id: number; + /** + * The name of the discount. + */ + name: string; + /** + * The discount code that can be used at checkout. Made up of uppercase letters and numbers and between 3 and 256 characters long. + */ + code: string; + /** + * The amount of discount to apply to the Discount. Either a fixed amount in cents or a percentage depending on the value of `amount_type`. + */ + amount: number; + /** + * The type of the amount. Either `percent` or `fixed`. + */ + amount_type: AmountType; + /** + * Has the value `true` if the discount can only be applied to certain products/variants. + */ + is_limited_to_products: boolean; + /** + * Has the value `true` if the discount can only be redeemed a limited number of times. + */ + is_limited_redemptions: boolean; + /** + * If `is_limited_redemptions` is `true`, this is the maximum number of redemptions. + */ + max_redemptions: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the discount is valid from. Can be `null` if no start date is specified. + */ + starts_at: string | null; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the discount expires. Can be `null` if no expiration date is specified. + */ + expires_at: string | null; + /** + * If the discount is applied to a subscription, this specifies how often the discount should be applied. One of + * + * - `once` - The discount will be applied to the initial payment only. + * - `repeating` - The discount will be applied to a certain number of payments (use in combination with `duration_in_months`. + * - `forever` - The discount will apply to all payments. + */ + duration: Duration; + /** + * If `duration` is `repeating`, this specifies how many months the discount should apply. + */ + duration_in_months: number; + /** + * The status of the discount. Either `draft` or `published`. + */ + status: "published" | "draft"; + /** + * The formatted status of the discount. + */ + status_formatted: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type DiscountData = Data< + Attributes, + Pick +>; + +export type GetDiscountParams = Pick< + Params<(keyof DiscountData["relationships"])[]>, + "include" +>; +export type ListDiscountsParams = Params< + GetDiscountParams["include"], + { storeId?: string | number } +>; +export type NewDiscount = { + /** + * The store id this discount belongs to. + */ + storeId: number | string; + /** + * If `isLimitedToProducts` is `true`, the variant(s) the discount belongs to (this is not required otherwise). + */ + variantIds: Array; + /** + * The name of the discount. + */ + name: string; + /** + * The discount code that can be used at checkout. Uppercase letters and numbers are allowed. Must be between 3 and 256 characters. + * + * @default An 8-character string of uppercase letters and numbers. e.g. `I0NTQZNG` + */ + code?: string; + /** + * The amount of discount to apply to the order. Either a fixed amount in cents or a percentage depending on the value of `amountType`. + * + * - `1000` means `$10` when `amountType` is `fixed`. + * - `10` means `10%` when `amountType` is `percent`. + */ + amount: number; + /** + * The type of the amount. Either `percent` or `fixed`. + */ + amountType: AmountType; + /** + * Set this to true if the discount should only be applied to certain products/variants. See details in the Relationships section below. + */ + isLimitedToProducts?: boolean; + /** + * Set this to `true` if the discount should only be redeemed a limited number of times. See `maxRedemptions` below. + */ + isLimitedRedemptions?: boolean; + /** + * If `isLimitedToProducts` is `true`, this is the maximum number of redemptions. + */ + maxRedemptions?: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the discount is valid from. Can omitted or `null` if no start date is specified. + */ + startsAt?: string | null; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the discount expires. Can omitted or `null` if the discount is perpetual. + */ + expiresAt?: string | null; + /** + * If the discount is applied to a subscription, this specifies how often the discount should be applied. One of + * + * - `once` - The discount will be applied to the initial payment only. + * - `repeating` - The discount will be applied to a certain number of payments (use in combination with `duration_in_months`. + * - `forever` - The discount will apply to all payments. + * + * Defaults to `once` if omitted. + * @default `once` + */ + duration?: Duration; + /** + * If `duration` is `repeating`, this specifies how many months the discount should apply. Defaults to `1` if omitted. + * + * Note: for yearly subscription, the value needs to be `years x 12`, so `24` if you want the discount to repeat for the first two yearly payments. We do not recommend repeated discounts for daily or weekly subscriptions. + * + * @default `1` + */ + durationInMonths?: number; + /** + * Set this to `true` if the discount should only be applied to test mode orders. + */ + testMode?: boolean; +}; +export type Discount = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListDiscounts = LemonSqueezyResponse< + DiscountData[], + Pick, + Pick +>; diff --git a/src/files/index.ts b/src/files/index.ts new file mode 100644 index 0000000..6ec04c1 --- /dev/null +++ b/src/files/index.ts @@ -0,0 +1,40 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { File, GetFileParams, ListFiles, ListFilesParams } from "./types"; + +/** + * Retrieve a file. + * + * @param fileId The given file id + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A file object. + */ +export function getFile(fileId: number | string, params: GetFileParams = {}) { + requiredCheck({ fileId }); + return $fetch({ + path: `/v1/files/${fileId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all files. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.variantId] (Optional) Only return files belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of file objects ordered by `sort`. + */ +export function listFiles(params: ListFilesParams = {}) { + return $fetch({ + path: `/v1/files${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/files/types.ts b/src/files/types.ts new file mode 100644 index 0000000..17cad38 --- /dev/null +++ b/src/files/types.ts @@ -0,0 +1,84 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the variant this file belongs to. + */ + variant_id: number; + /** + * The unique identifier (UUID) for this file. + */ + identifier: string; + /** + * The name of the file (e.g. `example.pdf`). + */ + name: string; + /** + * The file extension of the file (e.g. `pdf`). + */ + extension: string; + /** + * The unique URL to download the file. + * + * Note: for security reasons, download URLs are signed, expire after 1 hour and are rate-limited to 10 downloads per day per IP address. + */ + download_url: string; + /** + * A positive integer in bytes representing the size of the file. + */ + size: number; + /** + * The human-readable size of the file (e.g. `5.5 MB`). + */ + size_formatted: string; + /** + * The software version of this file (if one exists, e.g. `1.0.0`). + */ + version: string; + /** + * An integer representing the order of this file when displayed. + */ + sort: number; + /** + * The status of the file. Either `draft` or `published`. + */ + status: "draft" | "published"; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + createdAt: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updatedAt: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type FileData = Data>; + +export type GetFileParams = Pick< + Params<(keyof FileData["relationships"])[]>, + "include" +>; +export type ListFilesParams = Params< + GetFileParams["include"], + { variantId?: string | number } +>; +export type File = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListFiles = LemonSqueezyResponse< + FileData[], + Pick, + Pick +>; diff --git a/src/index.ts b/src/index.ts index d67138b..a27745d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,1324 +1,233 @@ -import { - BaseUpdateSubscriptionOptions, - CreateCheckoutOptions, - CreateDiscountAttributes, - CreateDiscountOptions, - CreateWebhookOptions, - DeleteDiscountOptions, - DeleteWebhookOptions, - GetCheckoutOptions, - GetCheckoutsOptions, - GetCustomerOptions, - GetCustomersOptions, - GetDiscountOptions, - GetDiscountsOptions, - GetDiscountRedemptionOptions, - GetDiscountRedemptionsOptions, - GetFileOptions, - GetFilesOptions, - GetLicenseKeyOptions, - GetLicenseKeysOptions, - GetLicenseKeyInstanceOptions, - GetLicenseKeyInstancesOptions, - GetOrderItemOptions, - GetOrderItemsOptions, - GetOrderOptions, - GetOrdersOptions, - GetProductOptions, - GetProductsOptions, - GetStoreOptions, - GetStoresOptions, - GetSubscriptionOptions, - GetSubscriptionsOptions, - GetSubscriptionInvoiceOptions, - GetSubscriptionInvoicesOptions, - GetVariantOptions, - GetVariantsOptions, - GetWebhookOptions, - GetWebhooksOptions, - PauseSubscriptionAttributes, - PauseSubscriptionOptions, - QueryApiOptions, - UpdateSubscriptionAttributes, - UpdateSubscriptionOptions, - UpdateWebhookOptions, - GetSubscriptionItemsOptions, - GetUsageRecordsOptions, - GetSubscriptionItemOptions, - GetUsageRecordOptions, - CreateUsageRecordOptions, - GetPricesOptions, - GetPriceOptions, - UpdateSubscriptionItemOptions, - GetSubscriptionItemUsageOptions, -} from "./types/methods"; -import { - CheckoutsResponse, - CheckoutResponse, - CustomersResponse, - CustomerResponse, - DiscountsResponse, - DiscountResponse, - DiscountRedemptionsResponse, - DiscountRedemptionResponse, - FilesResponse, - FileResponse, - LicenseKeysResponse, - LicenseKeyResponse, - LicenseKeyInstancesResponse, - LicenseKeyInstanceResponse, - OrdersResponse, - OrderResponse, - ProductsResponse, - ProductResponse, - StoresResponse, - StoreResponse, - SubscriptionInvoicesResponse, - SubscriptionInvoiceResponse, - SubscriptionsResponse, - SubscriptionResponse, - UserResponse, - VariantsResponse, - VariantResponse, - WebhooksResponse, - WebhookResponse, - SubscriptionItemResponse, - SubscriptionItemUsageResponse, - UsageRecordsResponse, - UsageRecordResponse -} from "./types/api"; - -export class LemonSqueezy { - public apiKey: string; - - public apiUrl = "https://api.lemonsqueezy.com/"; - - /** - * LemonSqueezy API client - * - * @param {String} apiKey - Your LemonSqueezy API key - */ - constructor(apiKey: string) { - this.apiKey = apiKey; - } - - /** - * Builds a params object for the API query based on provided and allowed filters. - * - * Also converts pagination parameters `page` to `page[number]` and `perPage` to `page[size]` - * - * @params {Object} [args] Arguments to the API method - * @params {string[]} [allowedFilters] List of filters the API query permits (camelCase) - */ - private _buildParams>( - args: TArgs, - allowedFilters: Array = [] - ): Record { - let params: Record = {}; - - for (let filter in args) { - if (allowedFilters.includes(filter)) { - const queryFilter = filter.replace( - /[A-Z]/g, - (letter) => `_${letter.toLowerCase()}` - ); - - params["filter[" + queryFilter + "]"] = args[filter]; - } else { - // In v1.0.3 and lower we supported passing in a string of comma separated values - // for the `include` filter. This is now deprecated in favour of an array. - if (filter === "include") { - params["include"] = Array.isArray(args[filter]) - ? args[filter].join(",") - : args[filter]; - } - - if (filter === "page") params["page[number]"] = args[filter]; - if (filter === "perPage") params["page[size]"] = args[filter]; - } - } - - return params; - } - - /** - * Send an API query to the LemonSqueezy API - * - * @param {string} path - * @param {string} [method] POST, GET, PATCH, DELETE - * @param {Object} [params] URL query parameters - * @param {Object} [payload] Object/JSON payload - * - * @returns {Object} JSON - */ - private async _query({ - path, - method = "GET", - params, - payload, - }: QueryApiOptions) { - try { - const url = new URL(path, this.apiUrl); - if (params && method === "GET") - Object.entries(params).forEach(([key, value]) => - url.searchParams.append(key, value) - ); - - const headers = new Headers(); - headers.set("Accept", "application/vnd.api+json"); - headers.set("Authorization", `Bearer ${this.apiKey}`); - headers.set("Content-Type", "application/vnd.api+json"); - - const response = await fetch(url.href, { - headers, - method, - body: payload ? JSON.stringify(payload) : undefined, - }); - - if (!response.ok) { - let errorsJson = await response.json(); - throw { - status: response.status, - message: response.statusText, - errors: errorsJson.errors, - }; - } - - if (method !== "DELETE") return await response.json(); - } catch (error) { - throw error; - } - } - - /** - * Get current user - * - * @returns {Object} JSON - */ - async getUser(): Promise { - return this._query({ path: "v1/users/me" }); - } - - /** - * Get stores - * - * @param {Object} [params] - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"products" | "discounts" | "license-keys" | "subscriptions" | "webhooks">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getStores(params: GetStoresOptions = {}): Promise { - return this._query({ - path: "v1/stores", - params: this._buildParams(params), - }); - } - - /** - * Get a store - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"products" | "discounts" | "license-keys" | "subscriptions" | "webhooks">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getStore({ id, ...params }: GetStoreOptions): Promise { - if (!id) throw "You must provide an `id` in getStore()."; - - return this._query({ - path: `v1/stores/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get products - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter products by store - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "variants">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getProducts(params: GetProductsOptions = {}): Promise { - return this._query({ - path: "v1/products", - params: this._buildParams(params, ["storeId"]), - }); - } - - /** - * Get a product - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store" | "variants">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getProduct({ id, ...params }: GetProductOptions): Promise { - if (!id) throw "You must provide an `id` in getProduct()."; - return this._query({ - path: `v1/products/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get variants - * - * @param {Object} [params] - * @param {number} [params.productId] Filter variants by product - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"product" | "files">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getVariants(params: GetVariantsOptions = {}): Promise { - return this._query({ - path: "v1/variants", - params: this._buildParams(params, ["productId"]), - }); - } - - /** - * Get a variant - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"product" | "files">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getVariant({ id, ...params }: GetVariantOptions): Promise { - if (!id) throw "You must provide an `id` in getVariant()."; - return this._query({ - path: `v1/variants/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get prices - * - * @param {Object} [params] - * @param {number} [params.variantId] Filter prices by variant - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getPrices(params: GetPricesOptions = {}) { - return this._query({ - path: "v1/prices", - params: this._buildParams(params, ["variantId"]) - }); - } - - /** - * Get a price - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getPrice({ id, ...params }: GetPriceOptions) { - if (!id) throw 'You must provide an `id` in getPrice().' - return this._query({ - path: `v1/prices/${id}`, - params: this._buildParams(params) - }); - } - - - /** - * Get checkouts - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter variants by store - * @param {number} [params.variantId] Filter checkouts by variant - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getCheckouts(params: GetCheckoutsOptions = {}): Promise { - return this._query({ - path: "v1/checkouts", - params: this._buildParams(params, ["storeId", "variantId"]) - }); - } - - /** - * Get a checkout - * - * @param {Object} params - * @param {string} params.id - * @param {Array<"store" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getCheckout({ id, ...params }: GetCheckoutOptions): Promise { - if (!id) throw "You must provide an `id` in getCheckout()."; - return this._query({ - path: `v1/checkouts/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Create a checkout - * - * @param {Object} params - * @param {number} params.storeId - * @param {number} params.variantId - * @param {Object} [params.attributes] An object of values used to configure the checkout - * - * @see https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout - * - * @returns {Object} JSON - */ - async createCheckout({ - storeId, - variantId, - attributes = {}, - }: CreateCheckoutOptions): Promise { - if (!storeId) throw "You must provide a `storeId` in createCheckout()."; - if (!variantId) throw "You must provide a `variantId` in createCheckout()."; - return this._query({ - path: "v1/checkouts", - method: "POST", - payload: { - data: { - type: "checkouts", - attributes: attributes, - relationships: { - store: { - data: { - type: "stores", - id: "" + storeId, // convert to string - }, - }, - variant: { - data: { - type: "variants", - id: "" + variantId, // convert to string - }, - }, - }, - }, - }, - }); - } - - /** - * Get customers - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter customers by store - * @param {number} [params.email] Filter customers by email address - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"license-keys" | "orders" | "store" | "subscriptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getCustomers(params: GetCustomersOptions = {}): Promise { - return this._query({ - path: "v1/customers", - params: this._buildParams(params, ["storeId", "email"]), - }); - } - - /** - * Get a customer - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"license-keys" | "orders" | "store" | "subscriptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getCustomer({ id, ...params }: GetCustomerOptions): Promise { - if (!id) throw "You must provide an `id` in getCustomer()."; - return this._query({ - path: `v1/customers/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get orders - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter orders by store - * @param {number} [params.userEmail] Filter orders by email address - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"customer" | "discount-redemptions" | "license-keys" | "order-items" | "store" | "subscriptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getOrders(params: GetOrdersOptions = {}): Promise { - return this._query({ - path: "v1/orders", - params: this._buildParams(params, ["storeId", "userEmail"]), - }); - } - - /** - * Get an order - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"customer" | "discount-redemptions" | "license-keys" | "order-items" | "store" | "subscriptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getOrder({ id, ...params }: GetOrderOptions): Promise { - if (!id) throw "You must provide an `id` in getOrder()."; - return this._query({ - path: `v1/orders/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get files - * - * @param {Object} [params] - * @param {number} [params.variantId] Filter orders by variant - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getFiles(params: GetFilesOptions = {}): Promise { - return this._query({ - path: "v1/files", - params: this._buildParams(params, ["variantId"]), - }); - } - - /** - * Get a file - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getFile({ id, ...params }: GetFileOptions): Promise { - if (!id) throw "You must provide an `id` in getFile()."; - return this._query({ - path: `v1/files/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get order items - * - * @param {Object} [params] - * @param {number} [params.orderId] Filter order items by order - * @param {number} [params.productId] Filter order items by product - * @param {number} [params.variantId] Filter order items by variant - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"order" | "product" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getOrderItems(params: GetOrderItemsOptions = {}) { - return this._query({ - path: "v1/order-items", - params: this._buildParams(params, ["orderId", "productId", "variantId"]), - }); - } - - /** - * Get an order item - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"order" | "product" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getOrderItem({ id, ...params }: GetOrderItemOptions) { - if (!id) throw "You must provide an `id` in getOrderItem()."; - return this._query({ - path: `v1/order-items/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Get subscriptions - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter subscriptions by store - * @param {number} [params.orderId] Filter subscriptions by order - * @param {number} [params.orderItemId] Filter subscriptions by order item - * @param {number} [params.productId] Filter subscriptions by product - * @param {number} [params.variantId] Filter subscriptions by variant - * @param {"on_trial" | "active" | "paused" | "past_due" | "unpaid" | "cancelled" | "expired"} [params.status] Filter subscriptions by status - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "customer" | "order" | "order-item" | "product" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscriptions(params: GetSubscriptionsOptions = {}): Promise { - return this._query({ - path: "v1/subscriptions", - params: this._buildParams(params, [ - "storeId", - "orderId", - "orderItemId", - "productId", - "variantId", - "status", - ]) - }) - } - - /** - * Get a subscription - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store" | "customer" | "order" | "order-item" | "product" | "variant">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscription({ id, ...params }: GetSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in getSubscription()."; - return this._query({ - path: `v1/subscriptions/${id}`, - params: this._buildParams(params), - }); - } - - /** - * Update a subscription's plan - * - * @param {Object} params - * @param {number} params.id - * @param {number} [params.variantId] ID of variant (required if changing plans) - * @param {number} [params.productId] ID of product (required if changing plans) - * @param {number} [params.billingAnchor] Set the billing day (1–31) used for renewal charges - * @param {"immediate" | "disable"} [params.proration] If not included, proration will occur at the next renewal date. - * Use 'immediate' to charge a prorated amount immediately. - * Use 'disable' to charge a full ammount immediately. - * - * @returns {Object} JSON - */ - async updateSubscription({ - id, - variantId, - productId, - billingAnchor, - proration, - }: UpdateSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in updateSubscription()."; - let attributes: UpdateSubscriptionAttributes = { - variant_id: variantId, - product_id: productId, - billing_anchor: billingAnchor, - }; - if (proration == "disable") attributes.disable_prorations = true; - if (proration == "immediate") attributes.invoice_immediately = true; - return this._query({ - path: `v1/subscriptions/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscriptions", - id: "" + id, - attributes - } - } - }); - } - - /** - * Cancel a subscription - * - * @param {Object} params - * @param {number} params.id - * - * @returns {Object} JSON - */ - async cancelSubscription({ id }: BaseUpdateSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in cancelSubscription()."; - return this._query({ - path: `v1/subscriptions/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscriptions", - id: "" + id, - attributes: { - cancelled: true, - } - } - } - }); - } - - /** - * Resume (un-cancel) a subscription - * - * @param {Object} params - * @param {number} params.id - * - * @returns {Object} JSON - */ - async resumeSubscription({ id }: BaseUpdateSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in resumeSubscription()."; - return this._query({ - path: `v1/subscriptions/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscriptions", - id: "" + id, - attributes: { - cancelled: false, - } - } - } - }); - } - - /** - * Pause a subscription - * - * @param {Object} params - * @param {number} params.id - * @param {"void" | "free"} [params.mode] Pause mode: "void" (default) or "free" - * @param {string} [params.resumesAt] Date to automatically resume the subscription (ISO 8601 format) - * - * @returns {Object} JSON - */ - async pauseSubscription({ - id, - mode, - resumesAt - }: PauseSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in pauseSubscription()."; - let pause: PauseSubscriptionAttributes = { mode: "void" }; - if (mode) pause.mode = mode; - if (resumesAt) pause.resumes_at = resumesAt; - return this._query({ - path: `v1/subscriptions/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscriptions", - id: "" + id, - attributes: { pause }, - }, - } - }); - } - - /** - * Unpause a subscription - * - * @param {Object} params - * @param {number} params.id - * - * @returns {Object} JSON - */ - async unpauseSubscription({ id }: BaseUpdateSubscriptionOptions): Promise { - if (!id) throw "You must provide an `id` in unpauseSubscription()."; - return this._query({ - path: `v1/subscriptions/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscriptions", - id: "" + id, - attributes: { - pause: null - }, - }, - } - }); - } - - /** - * Get subscription invoices - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter subscription invoices by store - * @param {"paid" | "pending" | "void" | "refunded"} [params.status] Filter subscription invoices by status - * @param {boolean} [params.refunded] Filter subscription invoices by refunded - * @param {number} [params.subscriptionId] Filter subscription invoices by subscription - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "subscription">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscriptionInvoices(params: GetSubscriptionInvoicesOptions = {}): Promise { - return this._query({ - path: "v1/subscription-invoices", - params: this._buildParams(params, [ - "storeId", - "status", - "refunded", - "subscriptionId", - ]) - }); - } - - /** - * Get a subscription invoice - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store" | "subscription">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscriptionInvoice({ id, ...params }: GetSubscriptionInvoiceOptions): Promise { - if (!id) throw "You must provide an `id` in getSubscriptionInvoice()."; - return this._query({ - path: `v1/subscription-invoices/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Get subscription items - * - * @param {Object} [params] - * @param {number} [params.subscriptionId] Filter subscription items by subscription - * @param {number} [params.priceId] Filter subscription items by price - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"subscription" | "price" | "usage-records">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscriptionItems(params: GetSubscriptionItemsOptions = {}): Promise { - return this._query({ - path: "v1/subscription-items", - params: this._buildParams(params, ['subscriptionId', 'priceId']) - }); - } - - /** - * Get a subscription item - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"subscription" | "price" | "usage-records">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getSubscriptionItem({ id, ...params }: GetSubscriptionItemOptions): Promise { - if (!id) throw 'You must provide an `id` in getSubscriptionItem().' - return this._query({ - path: `v1/subscription-items/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Update the quantity of a subscription item - * - * @param {Object} params - * @param {number} params.id - * @param {number} params.quantity The new quantity for the subscription item - * - * @returns {Object} JSON - */ - async updateSubscriptionItem({ id, quantity }: UpdateSubscriptionItemOptions): Promise { - if (!id) throw 'You must provide an `id` in updateSubscriptionItem().' - if (!id) throw 'You must provide a `quantity` in updateSubscriptionItem().' - return this._query({ - path: `v1/subscription-items/${id}`, - method: "PATCH", - payload: { - data: { - type: "subscription-items", - id: "" + id, - attributes: { - quantity - } - } - } - }); - } - - /** - * Retrieves a subscription item's current usage - * - * @param {Object} params - * @param {number} params.id - * - * @returns {Object} JSON - */ - async getSubscriptionItemUsage({ id }: GetSubscriptionItemUsageOptions): Promise { - if (!id) throw 'You must provide an `id` in getSubscriptionItemUsage().' - return this._query({ - path: `v1/subscription-items/${id}/current-usage` - }); - } - - /** - * Get usage records - * - * @param {Object} [params] - * @param {number} [params.subscriptionItemId] Filter usage records by subscription item - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"subscription-item">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getUsageRecords(params: GetUsageRecordsOptions = {}): Promise { - return this._query({ - path: "v1/usage-records", - params: this._buildParams(params, ['subscriptionItemId']) - }); - } - - /** - * Get a usage record - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"subscription-item">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getUsageRecord({ id, ...params }: GetUsageRecordOptions): Promise { - if (!id) throw 'You must provide an `id` in getUsageRecord().' - return this._query({ - path: `v1/usage-records/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Create a usage record - * - * @param {Object} params - * @param {number} params.subscriptionItemId The ID of the subscription item to report usage for - * @param {number} params.quantity The number of units to report - * @param {"increment" | "set"} [params.action] Type of record - * - * @returns {Object} JSON - */ - async createUsageRecord({ - subscriptionItemId, - quantity, - action = "increment" - }: CreateUsageRecordOptions): Promise { - if (!subscriptionItemId) throw 'You must provide a `subscriptionItemId` in createUsageRecord().' - if (!quantity) throw 'You must provide a `quantity` in createUsageRecord().' - return this._query({ - path: "v1/usage-records", - method: "POST", - payload: { - data: { - type: "usage-records", - attributes: { - quantity, - action - }, - relationships: { - "subscription-item": { - data: { - type: "subscription-items", - id: "" + subscriptionItemId - } - } - } - } - } - }); - } - - /** - * Get discounts - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter discounts by store - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "variants" | "discount-redemptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getDiscounts(params: GetDiscountsOptions = {}): Promise { - return this._query({ - path: "v1/discounts", - params: this._buildParams(params, ["storeId"]) - }); - } - - /** - * Get a discount - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store" | "variants" | "discount-redemptions">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getDiscount({ id, ...params }: GetDiscountOptions): Promise { - if (!id) throw "You must provide an `id` in getDiscount()."; - return this._query({ - path: `v1/discounts/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Create a discount - * - * @param {Object} params - * @param {number} params.storeId Store to create a discount in - * @param {string} params.name Name of discount - * @param {string} params.code Discount code (uppercase letters and numbers, between 3 and 256 characters) - * @param {number} params.amount Amount the discount is for - * @param {"percent" | "fixed"} [params.amountType] Type of discount - * @param {"once" | "repeating" | "forever"} [params.duration] Duration of discount - * @param {number} [params.durationInMonths] Number of months to repeat the discount for - * @param {number[]} [params.variantIds] Limit the discount to certain variants - * @param {number} [params.maxRedemptions] The total number of redemptions allowed - * @param {number} [params.startsAt] Date the discount code starts on (ISO 8601 format) - * @param {number} [params.expiresAt] Date the discount code expires on (ISO 8601 format) - * - * @returns {Object} JSON - */ - async createDiscount({ - storeId, - name, - code, - amount, - amountType = "percent", - duration = "once", - durationInMonths, - variantIds, - maxRedemptions, - startsAt, - expiresAt - }: CreateDiscountOptions): Promise { - if (!storeId) throw "You must provide a `storeId` in createDiscount()."; - if (!name) throw "You must provide a `name` in createDiscount()."; - if (!code) throw "You must provide a `code` in createDiscount()."; - if (!amount) throw "You must provide an `amount` in createDiscount()."; - let attributes: CreateDiscountAttributes = { - name, - code, - amount, - amount_type: amountType, - duration, - starts_at: startsAt, - expires_at: expiresAt, - }; - if (durationInMonths && duration != "once") { - attributes.duration_in_months = durationInMonths; - } - if (maxRedemptions) { - attributes.is_limited_redemptions = true; - attributes.max_redemptions = maxRedemptions; - } - - let relationships: {store: Object; variants?: Object} = { - store: { - data: { - type: "stores", - id: "" + storeId, - } - } - } - - if (variantIds) { - let variantData: Array<{ type: string; id: string }> = []; - for (var i = 0; i < variantIds.length; i++) { - variantData.push({ type: "variants", id: "" + variantIds[i] }); - } - attributes.is_limited_to_products = true; - relationships.variants = { - data: variantData, - }; - } - - return this._query({ - path: "v1/discounts", - method: "POST", - payload: { - data: { - type: "discounts", - attributes, - relationships - } - } - }); - } - - /** - * Delete a discount - * - * @param {Object} params - * @param {number} params.id - */ - async deleteDiscount({ id }: DeleteDiscountOptions): Promise { - if (!id) throw "You must provide a `id` in deleteDiscount()."; - this._query({ path: `v1/discounts/${id}`, method: "DELETE" }); - } - - /** - * Get discount redemptions - * - * @param {Object} [params] - * @param {number} [params.discountId] Filter discount redemptions by discount - * @param {number} [params.orderId] Filter discount redemptions by order - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"discount" | "order">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getDiscountRedemptions(params: GetDiscountRedemptionsOptions = {}): Promise { - return this._query({ - path: "v1/discount-redemptions", - params: this._buildParams(params, ["discountId", "orderId"]) - }); - } - - - /** - * Get a discount redemption - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"discount" | "order">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getDiscountRedemption({ id, ...params }: GetDiscountRedemptionOptions): Promise { - if (!id) throw "You must provide a `id` in getDiscountRedemption()."; - return this._query({ - path: `v1/discount-redemptions/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Get license keys - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter license keys by store - * @param {number} [params.orderId] Filter license keys by order - * @param {number} [params.orderItemId] Filter license keys by order item - * @param {number} [params.productId] Filter license keys by product - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store" | "customer" | "order" | "order-item" | "product" | "license-key-instances">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getLicenseKeys(params: GetLicenseKeysOptions = {}): Promise { - return this._query({ - path: "v1/license-keys", - params: this._buildParams(params, [ - "storeId", - "orderId", - "orderItemId", - "productId", - ]) - }); - } - - /** - * Get a license key - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store" | "customer" | "order" | "order-item" | "product" | "license-key-instances">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getLicenseKey({ id, ...params }: GetLicenseKeyOptions): Promise { - if (!id) throw "You must provide an `id` in getLicenseKey()."; - return this._query({ - path: `v1/license-keys/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Get license key instances - * - * @param {Object} [params] - * @param {number} [params.licenseKeyId] Filter license keys instances by license key - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"license-key">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getLicenseKeyInstances(params: GetLicenseKeyInstancesOptions = {}): Promise { - return this._query({ - path: "v1/license-key-instances", - params: this._buildParams(params, ["licenseKeyId"]) - }); - } - - /** - * Get a license key instance - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"license-key">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getLicenseKeyInstance({ id, ...params }: GetLicenseKeyInstanceOptions): Promise { - if (!id) throw "You must provide an `id` in getLicenseKeyInstance()."; - return this._query({ - path: `v1/license-key-instances/${id}}`, - params: this._buildParams(params) - }); - } - - /** - * Get webhooks - * - * @param {Object} [params] - * @param {number} [params.storeId] Filter webhooks by store - * @param {number} [params.perPage] Number of records to return (between 1 and 100) - * @param {number} [params.page] Page of records to return - * @param {Array<"store">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getWebhooks(params: GetWebhooksOptions = {}): Promise { - return this._query({ - path: "v1/webhooks", - params: this._buildParams(params, ["storeId"]) - }); - } - - /** - * Get a webhook - * - * @param {Object} params - * @param {number} params.id - * @param {Array<"store">} [params.include] List of record types to include - * - * @returns {Object} JSON - */ - async getWebhook({ id, ...params }: GetWebhookOptions): Promise { - if (!id) throw "You must provide an `id` in getWebhook()."; - return this._query({ - path: `v1/webhooks/${id}`, - params: this._buildParams(params) - }); - } - - /** - * Create a webhook - * - * @param {Object} params - * @param {string} params.storeId ID of the store the webhook is for - * @param {string} params.url Endpoint URL that the webhooks should be sent to - * @param {string[]} params.events List of webhook events to receive - * @param {string} params.secret Signing secret (between 6 and 40 characters) - * - * @returns {Object} JSON - */ - async createWebhook({ storeId, url, events, secret }: CreateWebhookOptions): Promise { - if (!storeId) throw "You must provide a `storeId` in createWebhook()."; - if (!url) throw "You must provide a `url` in createWebhook()."; - if (!events || events?.length < 1) - throw "You must provide a list of events in createWebhook()."; - if (!secret) throw "You must provide a `secret` in createWebhook()."; - return this._query({ - path: "v1/webhooks", - method: "POST", - payload: { - data: { - type: "webhooks", - attributes: { - url, - events, - secret, - }, - relationships: { - store: { - data: { - type: "stores", - id: "" + storeId, - }, - }, - }, - }, - } - }); - } - - /** - * Update a webhook - * - * @param {Object} params - * @param {number} params.id - * @param {string} [params.url] Endpoint URL that the webhooks should be sent to - * @param {string[]} [params.events] List of webhook events to receive - * @param {string} [params.secret] Signing secret (between 6 and 40 characters) - * - * @returns {Object} JSON - */ - async updateWebhook({ id, url, events, secret }: UpdateWebhookOptions): Promise { - if (!id) throw "You must provide an `id` in updateWebhook()."; - let attributes: { url?: string; events?: string[]; secret?: string } = {}; - if (url) attributes.url = url; - if (events) attributes.events = events; - if (secret) attributes.secret = secret; - return this._query({ - path: `v1/webhooks/${id}`, - method: "PATCH", - payload: { - data: { - type: "webhooks", - id: "" + id, - attributes, - }, - } - }); - } - - /** - * Delete a webhook - * - * @param {Object} params - * @param {number} params.id - */ - async deleteWebhook({ id }: DeleteWebhookOptions): Promise { - if (!id) throw "You must provide an `id` in deleteWebhook()."; - this._query({ path: `v1/webhooks/${id}`, method: "DELETE" }); - } -} - -export default LemonSqueezy; +export { LemonSqueezy } from "./_deprecated"; + +// Setup +export { lemonSqueezySetup } from "./internal"; +export type { Flatten } from "./types"; + +// User +export type { User } from "./users/types"; +export { getAuthenticatedUser } from "./users"; + +// Stores +export type { + Store, + ListStores, + GetStoreParams, + ListStoresParams, +} from "./stores/types"; +export { getStore, listStores } from "./stores"; + +// Customers +export type { + Customer, + ListCustomers, + NewCustomer, + UpdateCustomer, + GetCustomerParams, + ListCustomersParams, +} from "./customers/types"; +export { + listCustomers, + getCustomer, + createCustomer, + archiveCustomer, + updateCustomer, +} from "./customers"; + +// Products +export type { + Product, + ListProducts, + GetProductParams, + ListProductsParams, +} from "./products/types"; +export { getProduct, listProducts } from "./products"; + +// Variants +export type { + Variant, + ListVariants, + GetVariantParams, + ListVariantsParams, +} from "./variants/types"; +export { getVariant, listVariants } from "./variants"; + +// Prices +export type { + Price, + ListPrices, + GetPriceParams, + ListPricesParams, +} from "./prices/types"; +export { getPrice, listPrices } from "./prices"; + +// Files +export type { + File, + ListFiles, + GetFileParams, + ListFilesParams, +} from "./files/types"; +export { getFile, listFiles } from "./files"; + +// Orders +export type { + Order, + ListOrders, + GetOrderParams, + ListOrdersParams, +} from "./orders/types"; +export { getOrder, listOrders } from "./orders"; + +// Order Items +export type { + OrderItem, + ListOrderItems, + GetOrderItemParams, + ListOrderItemsParams, +} from "./orderItems/types"; +export { getOrderItem, listOrderItems } from "./orderItems"; + +// Subscriptions +export type { + Subscription, + ListSubscriptions, + GetSubscriptionParams, + ListSubscriptionsParams, + UpdateSubscription, +} from "./subscriptions/types"; +export { + getSubscription, + listSubscriptions, + updateSubscription, + cancelSubscription, +} from "./subscriptions"; + +// Subscription Invoices +export type { + SubscriptionInvoice, + ListSubscriptionInvoices, + GetSubscriptionInvoiceParams, + ListSubscriptionInvoicesParams, +} from "./subscriptionInvoices/types"; +export { + getSubscriptionInvoice, + listSubscriptionInvoices, +} from "./subscriptionInvoices"; + +// Subscription Items +export type { + SubscriptionItem, + SubscriptionItemCurrentUsage, + ListSubscriptionItems, + GetSubscriptionItemParams, + ListSubscriptionItemsParams, +} from "./subscriptionItems/types"; +export { + getSubscriptionItem, + listSubscriptionItems, + getSubscriptionItemCurrentUsage, + updateSubscriptionItem, +} from "./subscriptionItems"; + +// Usage Records +export type { + NewUsageRecord, + UsageRecord, + ListUsageRecords, + GetUsageRecordParams, + ListUsageRecordsParams, +} from "./usageRecords/types"; +export { + listUsageRecords, + getUsageRecord, + createUsageRecord, +} from "./usageRecords"; + +// Discounts +export type { + Discount, + ListDiscounts, + GetDiscountParams, + ListDiscountsParams, + NewDiscount, +} from "./discounts/types"; +export { + listDiscounts, + getDiscount, + createDiscount, + deleteDiscount, +} from "./discounts"; + +// Discount Redemptions +export type { + DiscountRedemption, + ListDiscountRedemptions, + GetDiscountRedemptionParams, + ListDiscountRedemptionsParams, +} from "./discountRedemptions/types"; +export { + listDiscountRedemptions, + getDiscountRedemption, +} from "./discountRedemptions"; + +// License Keys +export type { + LicenseKey, + ListLicenseKeys, + GetLicenseKeyParams, + ListLicenseKeysParams, + UpdateLicenseKey, +} from "./licenseKeys/types"; +export { + listLicenseKeys, + getLicenseKey, + updateLicenseKey, +} from "./licenseKeys"; + +// License Key Instances +export type { + GetLicenseKeyInstanceParams, + ListLicenseKeyInstancesParams, + LicenseKeyInstance, + ListLicenseKeyInstances, +} from "./licenseKeyInstances/types"; +export { + listLicenseKeyInstances, + getLicenseKeyInstance, +} from "./licenseKeyInstances"; + +// Checkouts +export type { + Checkout, + ListCheckouts, + GetCheckoutParams, + ListCheckoutsParams, + NewCheckout, +} from "./checkouts/types"; +export { listCheckouts, getCheckout, createCheckout } from "./checkouts"; + +// Webhooks +export type { + Webhook, + ListWebhooks, + GetWebhookParams, + ListWebhooksParams, + NewWebhook, + UpdateWebhook, +} from "./webhooks/types"; +export { + listWebhooks, + getWebhook, + createWebhook, + updateWebhook, + deleteWebhook, +} from "./webhooks"; + +// License API +export type { + ActivateLicense, + ValidateLicense, + DeactivateLicense, +} from "./license/types"; +export { activateLicense, validateLicense, deactivateLicense } from "./license"; diff --git a/src/internal/fetch/index.ts b/src/internal/fetch/index.ts new file mode 100644 index 0000000..61a2b5b --- /dev/null +++ b/src/internal/fetch/index.ts @@ -0,0 +1,76 @@ +import type { Config } from "../setup/types"; +import { API_BASE_URL, CONFIG_KEY, getKV } from "../utils"; +import type { FetchOptions, FetchResponse } from "./types"; + +/** + * Internal customization of fetch. + * + * @param {FetchOptions} options Fetch options. + * @param {boolean} [needApiKey] (Optional) Whether `apiKey` is required. Default `true`. + * + * @returns Fetch response. Includes `statusCode`, `error` and `data`. + */ +export async function $fetch(options: FetchOptions, needApiKey = true) { + const response: FetchResponse = { + statusCode: null, + data: null, + error: null, + }; + const { apiKey, onError } = getKV(CONFIG_KEY) || {}; + + try { + if (needApiKey && !apiKey) { + response.error = Error( + "Please provide your Lemon Squeezy API key. Create a new API key: https://app.lemonsqueezy.com/settings/api", + { cause: "Missing API key" } + ); + onError?.(response.error); + return response; + } + + const { path, method = "GET", query, body } = options; + const _options: FetchRequestInit = { + method, + }; + + // url + const url = new URL(`${API_BASE_URL}${path}`); + for (const key in query) { + url.searchParams.append(key, query[key]); + } + + // headers + _options.headers = new Headers(); + _options.headers.set("Accept", "application/vnd.api+json"); + _options.headers.set("Content-Type", "application/vnd.api+json"); + + // authorization + if (needApiKey) { + _options.headers.set("Authorization", `Bearer ${apiKey}`); + } + + // If payload method, serialize body + if (["PATCH", "POST"].includes(method)) { + _options.body = body ? JSON.stringify(body) : null; + } + + const fetchResponse = await fetch(url.href, _options); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = (await fetchResponse.json()) as { error?: any; errors?: any }; + const fetchOk = fetchResponse.ok; + + Object.assign(response, { + statusCode: fetchResponse.status, + // The license api returns data in the event of an error + data: fetchOk ? data : data.error ? data : null, + error: fetchOk + ? null + : Error(fetchResponse.statusText, { cause: data.errors || data.error }), + }); + } catch (error) { + response.error = error as Error; + } + + response.error && onError?.(response.error); + return response; +} diff --git a/src/internal/fetch/types.ts b/src/internal/fetch/types.ts new file mode 100644 index 0000000..eba2a85 --- /dev/null +++ b/src/internal/fetch/types.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +type ApiVersion = "v1"; + +export type FetchResponse = { + statusCode: number | null; + data: T | null; + error: Error | null; +}; + +export type FetchOptions = { + /** + * The path to the API endpoint. + */ + path: `/${ApiVersion}/${string}`; + /** + * The HTTP method to use. + * @default "GET" + */ + method?: "GET" | "POST" | "PATCH" | "DELETE"; + /** + * Query parameters. + */ + query?: Record; + /** + * Request body. + */ + body?: Record; +}; diff --git a/src/internal/index.ts b/src/internal/index.ts new file mode 100644 index 0000000..ca228fa --- /dev/null +++ b/src/internal/index.ts @@ -0,0 +1,3 @@ +export * from "./setup"; +export * from "./fetch"; +export * from "./utils"; diff --git a/src/internal/setup/index.ts b/src/internal/setup/index.ts new file mode 100644 index 0000000..39a9c64 --- /dev/null +++ b/src/internal/setup/index.ts @@ -0,0 +1,14 @@ +import { CONFIG_KEY, setKV } from "../utils"; +import type { Config } from "./types"; + +/** + * Lemon squeezy setup. + * + * @param config The config. + * @returns User configuration. + */ +export function lemonSqueezySetup(config: Config) { + const { apiKey, onError } = config; + setKV(CONFIG_KEY, { apiKey, onError }); + return config; +} diff --git a/src/internal/setup/types.ts b/src/internal/setup/types.ts new file mode 100644 index 0000000..54b43a9 --- /dev/null +++ b/src/internal/setup/types.ts @@ -0,0 +1,13 @@ +export type Config = { + /** + * `Lemon Squeezy` API Key + */ + apiKey?: string; + /** + * Fires after a fetch response error + * + * @param error Error + * @returns void + */ + onError?: (error: Error) => void; +}; diff --git a/src/internal/utils/index.ts b/src/internal/utils/index.ts new file mode 100644 index 0000000..e44266c --- /dev/null +++ b/src/internal/utils/index.ts @@ -0,0 +1,112 @@ +import type { Params } from "../../types"; +export * from "./kv"; +export const CONFIG_KEY = "__config__"; +export const API_BASE_URL = "https://api.lemonsqueezy.com"; + +/** + * Checking if a `value` is an object type. + * + * @param value Value to be checked. + * @returns Whether it is an object type. + */ +export function isObject(value: unknown): value is Record { + return Object.prototype.toString.call(value) === "[object Object]"; +} + +/** + * Convert camel naming to underscore naming. + * + * @param key A string key. + * @returns Underscore naming. + */ +export function camelToUnderscore(key: string) { + return key.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`); +} + +/** + * Recursively converts an object's key to an underscore naming and exclude the specified values. + * + * @param obj An object. + * @param excludedValue Value to be excluded. The default is `undefined`. + * @returns A converted object. + */ +export function convertKeys( + obj: Record, + excludedValue: unknown = undefined +) { + const newObj: Record = {}; + + for (const key in obj) { + if (obj[key] === excludedValue) continue; + newObj[camelToUnderscore(key)] = isObject(obj[key]) + ? convertKeys(obj[key] as Record, excludedValue) + : obj[key]; + } + + return newObj; +} + +/** + * Convert `include` array to query string. + * + * @param include Related resources `include` array. + * @returns A query string containing `include`. + */ +export function convertIncludeToQueryString(include: string[] | undefined) { + if (!include || !Array.isArray(include) || !include.length) return ""; + return `?include=${include.join(",")}`; +} + +/** + * Convert list parameters to query string. + * + * @param params List parameters. + * @returns A query string containing `filter`, `include` and `page`. + */ +export function convertListParamsToQueryString(params: Params) { + const { filter = {}, page = {}, include = [] } = params; + const convertedQuery = convertKeys({ + filter, + page, + include: include.join(","), + }); + const searchParams = {} as Record; + + for (const key in convertedQuery) { + const params = convertedQuery[key]; + + if (isObject(params)) { + for (const iKey in params) { + searchParams[`${key}[${iKey}]`] = `${params[iKey]}`; + } + } else { + searchParams[`${key}`] = `${convertedQuery[key]}`; + } + } + + const queryString = new URLSearchParams(searchParams).toString(); + + return queryString ? `?${queryString}` : ""; +} + +/** + * Generate a discount code. + * + * @returns An 8-character string of uppercase letters and numbers. + */ +export function generateDiscount() { + return btoa(Date.now().toString()).slice(-10, -2).toUpperCase(); +} + +/** + * Checking required parameters. + * + * @param checkedObject The checked object value + */ +export function requiredCheck(checkedObject: Record) { + for (const key in checkedObject) { + if (!checkedObject[key]) { + throw Error(`Please provide the required parameter: ${key}.`); + } + } +} diff --git a/src/internal/utils/kv.ts b/src/internal/utils/kv.ts new file mode 100644 index 0000000..4b3b5c3 --- /dev/null +++ b/src/internal/utils/kv.ts @@ -0,0 +1,21 @@ +const KV: Record = {}; + +/** + * Get the `value` corresponding to the `key`. + * + * @param key String type key. + * @returns Returns the `value` corresponding to `key`. + */ +export function getKV(key: string) { + return KV[key] as T; +} + +/** + * Set the `value` corresponding to the `key`. + * + * @param key String type key. + * @param value Set the `value` of the `key`. + */ +export function setKV(key: string, value: unknown) { + KV[key] = value; +} diff --git a/src/license/index.ts b/src/license/index.ts new file mode 100644 index 0000000..97bff85 --- /dev/null +++ b/src/license/index.ts @@ -0,0 +1,75 @@ +import { $fetch, convertKeys, requiredCheck } from "../internal"; +import type { + ActivateLicense, + DeactivateLicense, + ValidateLicense, +} from "./types"; + +/** + * Activate a license key. + * + * @param licenseKey The license key. + * @param instanceName A label for the new instance to identify it in Lemon Squeezy. + * @returns A response object containing `activated`, `error`, `license_key`, `instance`, `meta`. + */ +export async function activateLicense( + licenseKey: string, + instanceName: string +) { + requiredCheck({ licenseKey, instanceName }); + return $fetch( + { + path: "/v1/licenses/activate", + method: "POST", + body: convertKeys({ licenseKey, instanceName }), + }, + false + ); +} + +/** + * Validate a license key or license key instance. + * + * @param licenseKey The license key. + * @param [instanceId] (Optional) If included, validate a license key instance, otherwise a license key. If no `instance_id` is provided, the response will contain "instance": `null`. + * @returns A response object containing `valid`, `error`, `license_key`, `instance`, `meta`. + */ +export async function validateLicense(licenseKey: string, instanceId?: string) { + requiredCheck({ licenseKey }); + return $fetch( + { + path: "/v1/licenses/validate", + method: "POST", + body: convertKeys({ + licenseKey, + instanceId, + }), + }, + false + ); +} + +/** + * Deactivate a license key instance. + * + * @param licenseKey The license key. + * @param instanceId The instance ID returned when activating a license key. + * @returns A response object containing `deactivated`, `error`, `license_key`, `meta`. + */ +export async function deactivateLicense( + licenseKey: string, + instanceId: string +) { + requiredCheck({ licenseKey, instanceId }); + return $fetch( + { + path: "/v1/licenses/deactivate", + method: "POST", + body: convertKeys({ + licenseKey, + instanceId, + }), + }, + false + ); +} diff --git a/src/license/types.ts b/src/license/types.ts new file mode 100644 index 0000000..96364b4 --- /dev/null +++ b/src/license/types.ts @@ -0,0 +1,44 @@ +type Meta = { + store_id: number; + order_id: number; + order_item_id: number; + product_id: number; + product_name: string; + variant_id: number; + variant_name: string; + customer_id: number; + customer_name: string; + customer_email: string; +}; +type LicenseKeyStatus = "inactive" | "active" | "expired" | "disabled"; +type LicenseKey = { + id: number; + status: LicenseKeyStatus; + key: string; + activation_limit: number; + activation_usage: number; + created_at: string; + expires_at: string | null; + test_mode: boolean; +}; +type Instance = { + id: string; + name: string; + created_at: string; +}; +type LicenseResponse = { + error: string | null; + license_key: LicenseKey; + instance?: Instance | null; + meta: Meta; +}; + +export type ActivateLicense = { + activated: boolean; +} & LicenseResponse; +export type ValidateLicense = { + valid: boolean; +} & LicenseResponse; +export type DeactivateLicense = { + deactivated: boolean; +} & Omit; diff --git a/src/licenseKeyInstances/index.ts b/src/licenseKeyInstances/index.ts new file mode 100644 index 0000000..372e507 --- /dev/null +++ b/src/licenseKeyInstances/index.ts @@ -0,0 +1,50 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetLicenseKeyInstanceParams, + LicenseKeyInstance, + ListLicenseKeyInstances, + ListLicenseKeyInstancesParams, +} from "./types"; + +/** + * Retrieve a license key instance. + * + * @param licenseKeyInstanceId The given license key instance id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A license key instance object. + */ +export function getLicenseKeyInstance( + licenseKeyInstanceId: number | string, + params: GetLicenseKeyInstanceParams = {} +) { + requiredCheck({ licenseKeyInstanceId }); + return $fetch({ + path: `/v1/license-key-instances/${licenseKeyInstanceId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all license key instances. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.licenseKeyId] (Optional) The license key ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of license key instance objects ordered by `id`. + */ +export function listLicenseKeyInstances( + params: ListLicenseKeyInstancesParams = {} +) { + return $fetch({ + path: `/v1/license-key-instances${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/licenseKeyInstances/types.ts b/src/licenseKeyInstances/types.ts new file mode 100644 index 0000000..14b1f94 --- /dev/null +++ b/src/licenseKeyInstances/types.ts @@ -0,0 +1,53 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the license key this instance belongs to. + */ + license_key_id: number; + /** + * The unique identifier (UUID) for this instance. This is the `instance_id` returned when [activating a license key](https://docs.lemonsqueezy.com/help/licensing/license-api#post-v1-licenses-activate). + */ + identifier: string; + /** + * The name of the license key instance. + */ + name: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; +}; +type LicenseKeyInstanceData = Data< + Attributes, + Pick +>; + +export type GetLicenseKeyInstanceParams = Pick< + Params<(keyof LicenseKeyInstanceData["relationships"])[]>, + "include" +>; +export type ListLicenseKeyInstancesParams = Params< + GetLicenseKeyInstanceParams["include"], + { licenseKeyId?: number | string } +>; +export type LicenseKeyInstance = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListLicenseKeyInstances = LemonSqueezyResponse< + LicenseKeyInstanceData[], + Pick, + Pick +>; diff --git a/src/licenseKeys/index.ts b/src/licenseKeys/index.ts new file mode 100644 index 0000000..ab41966 --- /dev/null +++ b/src/licenseKeys/index.ts @@ -0,0 +1,84 @@ +import { + $fetch, + convertIncludeToQueryString, + convertKeys, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetLicenseKeyParams, + LicenseKey, + ListLicenseKeys, + ListLicenseKeysParams, + UpdateLicenseKey, +} from "./types"; + +/** + * Retrieve a license key. + * + * @param licenseKeyId The license key id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A license key object. + */ +export function getLicenseKey( + licenseKeyId: number | string, + params: GetLicenseKeyParams = {} +) { + requiredCheck({ licenseKeyId }); + return $fetch({ + path: `/v1/license-keys/${licenseKeyId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all license keys. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return license keys belonging to the store with this ID. + * @param [params.filter.orderId] (Optional) (Optional) Only return license keys belonging to the order with this ID. + * @param [params.filter.orderItemId] (Optional) Only return license keys belonging to the order item with this ID. + * @param [params.filter.productId] (Optional) Only return license keys belonging to the product with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of license key objects ordered by `id`. + */ +export function listLicenseKeys(params: ListLicenseKeysParams = {}) { + return $fetch({ + path: `/v1/license-keys${convertListParamsToQueryString(params)}`, + }); +} + +/** + * Update a license key. + * + * @param licenseKeyId The license key id. + * @param licenseKey (Optional) Values to be updated. + * @param [licenseKey.activationLimit] (Optional) The activation limit of this license key. Assign `null` to set the activation limit to "unlimited". + * @param [licenseKey.disabled] (Optional) If `true`, the license key will have "disabled" status. + * @returns A license key object. + */ +export function updateLicenseKey( + licenseKeyId: string | number, + licenseKey: UpdateLicenseKey +) { + requiredCheck({ licenseKeyId }); + + const { activationLimit, disabled = false } = licenseKey; + const attributes = convertKeys({ activationLimit, disabled }); + + return $fetch({ + path: `/v1/license-keys/${licenseKeyId}`, + method: "PATCH", + body: { + data: { + type: "license-keys", + id: licenseKeyId.toString(), + attributes, + }, + }, + }); +} diff --git a/src/licenseKeys/types.ts b/src/licenseKeys/types.ts new file mode 100644 index 0000000..845c161 --- /dev/null +++ b/src/licenseKeys/types.ts @@ -0,0 +1,151 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type LicenseKeyStatus = "inactive" | "active" | "expired" | "disabled"; +type Attributes = { + /** + * The ID of the store this license key belongs to. + */ + store_id: number; + /** + * The ID of the customer this license key belongs to. + */ + customer_id: number; + /** + * The ID of the order associated with this license key. + */ + order_id: number; + /** + * The ID of the order item associated with this license key. + */ + order_item_id: number; + /** + * The ID of the product associated with this license key. + */ + product_id: number; + /** + * The full name of the customer. + */ + user_name: string; + /** + * The email address of the customer. + */ + user_email: string; + /** + * The full license key. + */ + key: string; + /** + * A “short” representation of the license key, made up of the string “XXXX-” followed by the last 12 characters of the license key. + */ + key_short: string; + /** + * The activation limit of this license key. + */ + activation_limit: number; + /** + * A count of the number of instances this license key has been activated on. + */ + instances_count: number; + /** + * Has the value `true` if this license key has been disabled. + */ + disabled: number; + /** + * The status of the license key. One of + * + * - `inactive` + * - `active` + * - `expired` + * - `disabled` + */ + status: LicenseKeyStatus; + /** + * The formatted status of the license key. + */ + status_formatted: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the license key expires. Can be `null` if the license key is perpetual. + */ + expires_at: string | null; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type LicenseKeyData = Data< + Attributes, + Pick< + Relationships, + | "store" + | "customer" + | "order" + | "order-item" + | "product" + | "license-key-instances" + > +>; + +export type GetLicenseKeyParams = Pick< + Params<(keyof LicenseKeyData["relationships"])[]>, + "include" +>; +export type ListLicenseKeysParams = Params< + GetLicenseKeyParams["include"], + { + /** + * Only return license keys belonging to the store with this ID. + */ + storeId?: number | string; + /** + * Only return license keys belonging to the order with this ID. + */ + orderId?: number | string; + /** + * Only return license keys belonging to the order item with this ID. + */ + orderItemId?: number | string; + /** + * Only return license keys belonging to the product with this ID. + */ + productId?: number | string; + /** + * Only return license keys with this status. + */ + status?: LicenseKeyStatus; + } +>; +export type UpdateLicenseKey = { + /** + * The activation limit of this license key. Assign `null` to set the activation limit to "unlimited". + */ + activationLimit?: number | null; + /** + * If `true`, the license key will have "disabled" status. + */ + disabled?: boolean; +}; + +export type LicenseKey = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListLicenseKeys = LemonSqueezyResponse< + LicenseKeyData[], + Pick, + Pick +>; diff --git a/src/orderItems/index.ts b/src/orderItems/index.ts new file mode 100644 index 0000000..4918292 --- /dev/null +++ b/src/orderItems/index.ts @@ -0,0 +1,50 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetOrderItemParams, + ListOrderItems, + ListOrderItemsParams, + OrderItem, +} from "./types"; + +/** + * Retrieve an order item. + * + * @param orderItemId The given order item id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns An order item object. + */ +export function getOrderItem( + orderItemId: number | string, + params: GetOrderItemParams = {} +) { + requiredCheck({ orderItemId }); + return $fetch({ + path: `/v1/order-items/${orderItemId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all order items. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.orderId] (Optional) Only return order items belonging to the order with this ID. + * @param [params.filter.productId] (Optional) Only return order items belonging to the product with this ID. + * @param [params.filter.variantId] (Optional) Only return order items belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of order item objects ordered by `id`. + */ +export function listOrderItems(params: ListOrderItemsParams = {}) { + return $fetch({ + path: `/v1/order-items${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/orderItems/types.ts b/src/orderItems/types.ts new file mode 100644 index 0000000..7797dfd --- /dev/null +++ b/src/orderItems/types.ts @@ -0,0 +1,87 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the order this order item belongs to. + */ + order_id: number; + /** + * The ID of the product associated with this order item. + */ + product_id: number; + /** + * The ID of the variant associated with this order item. + */ + variant_id: number; + /** + * The ID of the price. + * + * Note: Not in the documentation, but in the response + */ + price_id: number; + /** + * The name of the product. + */ + product_name: string; + /** + * The name of the variant. + */ + variant_name: string; + /** + * A positive integer in cents representing the price of this order item (in the order currency). + * + * Note, for “pay what you want” products the price will be whatever the customer entered at checkout. + */ + price: number; + /** + * A positive integer representing the quantity of this order item. + */ + quantity: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the order was made in test mode. + * + * Note: Not in the documentation, but in the response + */ + test_mode: boolean; +}; +type OrderItemData = Data< + Attributes, + Pick +>; + +export type GetOrderItemParams = Pick< + Params<(keyof OrderItemData["relationships"])[]>, + "include" +>; +export type ListOrderItemsParams = Params< + GetOrderItemParams["include"], + { + orderId?: string | number; + productId?: string | number; + variantId?: string | number; + } +>; +export type OrderItem = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListOrderItems = LemonSqueezyResponse< + OrderItemData[], + Pick, + Pick +>; diff --git a/src/orders/index.ts b/src/orders/index.ts new file mode 100644 index 0000000..9d64a47 --- /dev/null +++ b/src/orders/index.ts @@ -0,0 +1,49 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetOrderParams, + ListOrders, + ListOrdersParams, + Order, +} from "./types"; + +/** + * Retrieve an order. + * + * @param orderId The given order id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns An order object. + */ +export function getOrder( + orderId: number | string, + params: GetOrderParams = {} +) { + requiredCheck({ orderId }); + return $fetch({ + path: `/v1/orders/${orderId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all orders. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return orders belonging to the store with this ID. + * @param [params.filter.userEmail] (Optional) Only return orders where the `user_email` field is equal to this email address. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of order objects ordered by `created_at` (descending). + */ +export function listOrders(params: ListOrdersParams = {}) { + return $fetch({ + path: `/v1/orders${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/orders/types.ts b/src/orders/types.ts new file mode 100644 index 0000000..6266d65 --- /dev/null +++ b/src/orders/types.ts @@ -0,0 +1,241 @@ +import type { + Data, + ISO4217CurrencyCode, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type OrderStatus = "pending" | "failed" | "paid" | "refunded"; +type FirstOrderItem = { + /** + * The ID of the order item. + */ + id: number; + /** + * The ID of the order. + */ + order_id: number; + /** + * The ID of the product. + */ + product_id: number; + /** + * The ID of the product variant. + */ + variant_id: number; + /** + * The ID of the price. + * + * Note: Not in the documentation, but in the response + */ + price_id: number; + /** + * A positive integer representing the quantity of this order item. + * + * Note: Not in the documentation, but in the response + */ + quantity: number; + /** + * The name of the product. + */ + product_name: string; + /** + * The name of the product variant. + */ + variant_name: string; + /** + * A positive integer in cents representing the price of the order item in the order currency. + */ + price: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the order item was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the order item was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the order was made in test mode. + */ + test_mode: boolean; +}; +type Attributes = { + /** + * The ID of the store this order belongs to. + */ + store_id: number; + /** + * The ID of the customer this order belongs to. + */ + customer_id: number; + /** + * The unique identifier (UUID) for this order. + */ + identifier: string; + /** + * An integer representing the sequential order number for this store. + */ + order_number: number; + /** + * The full name of the customer. + */ + user_name: string; + /** + * The email address of the customer. + */ + user_email: string; + /** + * The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code for the order (e.g. `USD`, `GBP`, etc). + */ + currency: ISO4217CurrencyCode; + /** + * If the order currency is USD, this will always be `1.0`. Otherwise, this is the currency conversion rate used to determine the cost of the order in USD at the time of purchase. + */ + currency_rate: string; + /** + * A positive integer in cents representing the subtotal of the order in the order currency. + */ + subtotal: number; + /** + * A positive integer in cents representing the total discount value applied to the order in the order currency. + */ + discount_total: number; + /** + * A positive integer in cents representing the tax applied to the order in the order currency. + */ + tax: number; + /** + * A positive integer in cents representing the total cost of the order in the order currency. + */ + total: number; + /** + * A positive integer in cents representing the subtotal of the order in USD. + */ + subtotal_usd: number; + /** + * A positive integer in cents representing the total discount value applied to the order in USD. + */ + discount_total_usd: number; + /** + * A positive integer in cents representing the tax applied to the order in USD. + */ + tax_usd: number; + /** + * A positive integer in cents representing the total cost of the order in USD. + */ + total_usd: number; + /** + * The name of the tax rate (e.g. `VAT`, `Sales Tax`, etc) applied to the order. Will be `null` if no tax was applied. + */ + tax_name: string | null; + /** + * If tax is applied to the order, this will be the rate of tax as a decimal percentage. + */ + tax_rate: string; + /** + * The status of the order. One of + * + * - `pending` + * - `failed` + * - `paid` + * - `refunded` + */ + status: OrderStatus; + /** + * The formatted status of the order. + */ + status_formatted: string; + /** + * Has the value `true` if the order has been refunded. + */ + refunded: boolean; + /** + * If the order has been refunded, this will be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the order was refunded. + */ + refunded_at: Date | null; + /** + * A human-readable string representing the subtotal of the order in the order currency (e.g. `$9.99`). + */ + subtotal_formatted: string; + /** + * A human-readable string representing the total discount value applied to the order in the order currency (e.g. `$9.99`). + */ + discount_total_formatted: string; + /** + * A human-readable string representing the tax applied to the order in the order currency (e.g. `$9.99)`. + */ + tax_formatted: string; + /** + * A human-readable string representing the total cost of the order in the order currency (e.g. `$9.99`). + */ + total_formatted: string; + /** + * An object representing the [first order](https://docs.lemonsqueezy.com/api/order-items) item belonging to this order. + * + * - `id` - The ID of the order item. + * - `order_id` - The ID of the order. + * - `product_id` - The ID of the product. + * - `variant_id` - The ID of the product variant. + * - `product_name` - The name of the product. + * - `variant_name` - The name of the product variant. + * - `price` - A positive integer in cents representing the price of the order item in the order currency. + * - `created_at` - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the order item was created. + * - `updated_at` - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the order item was last updated. + * - `test_mode` - A boolean indicating if the order was made in test mode. + */ + first_order_item: FirstOrderItem; + /** + * An object of customer-facing URLs for this order. It contains: + * + * - `receipt` - A pre-signed URL for viewing the order in the customer's [My Orders](https://docs.lemonsqueezy.com/help/online-store/my-orders) page. + */ + urls: { + receipt: string; + }; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type OrderData = Data< + Attributes, + Pick< + Relationships, + | "store" + | "customer" + | "order-items" + | "subscriptions" + | "license-keys" + | "discount-redemptions" + > +>; + +export type GetOrderParams = Pick< + Params<(keyof OrderData["relationships"])[]>, + "include" +>; +export type ListOrdersParams = Params< + GetOrderParams["include"], + { storeId?: string | number; userEmail?: string } +>; +export type Order = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListOrders = LemonSqueezyResponse< + OrderData[], + Pick, + Pick +>; diff --git a/src/prices/index.ts b/src/prices/index.ts new file mode 100644 index 0000000..851d8d5 --- /dev/null +++ b/src/prices/index.ts @@ -0,0 +1,48 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetPriceParams, + ListPrices, + ListPricesParams, + Price, +} from "./types"; + +/** + * Retrieve a price. + * + * @param priceId The given price id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A price object. + */ +export function getPrice( + priceId: number | string, + params: GetPriceParams = {} +) { + requiredCheck({ priceId }); + return $fetch({ + path: `/v1/prices/${priceId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all prices. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.variantId] Only return prices belonging to the variant with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of price objects ordered by `created_at` (descending). + */ +export function listPrices(params: ListPricesParams = {}) { + return $fetch({ + path: `/v1/prices${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/prices/types.ts b/src/prices/types.ts new file mode 100644 index 0000000..66104bb --- /dev/null +++ b/src/prices/types.ts @@ -0,0 +1,196 @@ +import type { + Data, + IntervalUnit, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Category = "one_time" | "subscription" | "lead_magnet" | "pwyw"; +type Scheme = "standard" | "package" | "graduated" | "volume"; +type UsageAggregation = "sum" | "last_during_period" | "last_ever" | "max"; +type TaxCode = "eservice" | "ebook" | "saas"; +type Tier = { + /** + * The top limit of this tier. Will be an integer or `"inf"` (for "infinite") if this is the highest-level tier. + */ + last_unit: string | number; + /** + * A positive integer in cents representing the price of each unit. + */ + unit_price: number; + /** + * A positive decimal string in cents representing the price of each unit. Will be `null` if usage-based billing is not activated on this price's variant. + */ + unit_price_decimal: string | null; + /** + * An optional fixed fee charged alongside the unit price. + */ + fixed_fee: number; +}; + +type Attributes = { + /** + * The ID of the variant this price belongs to. + */ + variant_id: number; + /** + * The type of variant this price was created for. One of + * + * `one_time` - A regular product + * `subscription` - A subscription + * `lead_magnet` - A free lead magnet + * `pwyw` - "Pay what you want" product + */ + category: Category; + /** + * The pricing model for this price. One of + * + * `standard` + * `package` + * `graduated` + * `volume` + */ + scheme: Scheme; + /** + * The type of usage aggregation in use if usage-based billing is activated. One of + * + * - `sum` - Sum of usage during period + * - `last_during_period` - Most recent usage during a period + * - `last_ever` - Most recent usage + * - `max` - Maximum usage during period + * + * Will be `null` if usage-based billing is not activated on this price's variant. + */ + usage_aggregation: UsageAggregation | null; + /** + * A positive integer in cents representing the price. + * + * Not used for volume and graduated pricing (tier data is used instead). + * + * Note: If `usage_aggregation` is enabled for this price, `unit_price` will be null and `unit_price_decimal` will be used instead. + */ + unit_price: number; + /** + * A positive decimal string in cents representing the price. + * + * Not used for volume and graduated pricing (tier data is used instead). + * + * Note: If `usage_aggregation` is not enabled for this price, `unit_price_decimal` will be `null` and `unit_price` will be used instead. + */ + unit_price_decimal: string | null; + /** + * A boolean indicating if the price has a setup fee. + * Will be `null` for non-subscription pricing. + */ + setup_fee_enabled: boolean | null; + /** + * A positive integer in cents representing the setup fee. + * Will be `null` for non-subscription pricing. + */ + setup_fee: number | null; + /** + * The number of units included in each package when using package pricing. + * + * Will be `1` for standard, graduated and volume pricing. + */ + package_size: number; + /** + * A list of pricing tier objects when using volume and graduated pricing. + * + * Tiers have three values: + * + * - `last_unit` - The top limit of this tier. Will be an integer or `"inf"` (for "infinite") if this is the highest-level tier. + * - `unit_price` - A positive integer in cents representing the price of each unit. + * - `unit_price_decimal` - A positive decimal string in cents representing the price of each unit. Will be `null` if usage-based billing is not activated on this price's variant. + * - `fixed_fee` - An optional fixed fee charged alongside the unit price. + * + * Will be `null` for standard and package pricing. + */ + tiers: Tier[] | null; + /** + * If the price's variant is a subscription, the billing interval. One of + * + * - `day` + * - `week` + * - `week` + * - `year` + * + * Will be `null` if the product is not a subscription. + */ + renewal_interval_unit: IntervalUnit | null; + /** + * If the price's variant is a subscription, this is the number of intervals (specified in the `renewal_interval_unit` attribute) between subscription billings. + * + * For example, `renewal_interval_unit=month` and `renewal_interval_quantity=3` bills every 3 months. + * + * Will be `null` if the product is not a subscription. + */ + renewal_interval_quantity: number | null; + /** + * The interval unit of the free trial. One of + * + * - `day` + * - `week` + * - `week` + * - `year` + * + * Will be `null` if there is no trial. + */ + trial_interval_unit: IntervalUnit | null; + /** + * The interval count of the free trial. For example, a variant with `trial_interval_unit=day` and `trial_interval_quantity=14` would have a free trial that lasts 14 days. + * + * Will be `null` if there is no trial. + */ + trial_interval_quantity: number | null; + /** + * If `category` is `pwyw`, this is the minimum price this variant can be purchased for, as a positive integer in cents. + * + * Will be `null` for all other categories. + */ + min_price: number | null; + /** + * If `category` is `pwyw`, this is the suggested price for this variant shown at checkout, as a positive integer in cents. + * + * Will be `null` for all other categories. + */ + suggested_price: null; + /** + * The product's [tax category](https://docs.lemonsqueezy.com/help/products/tax-categories). One of + * + * - `eservice` + * - `ebook` + * - `saas` + */ + tax_code: TaxCode; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; +}; +type PriceData = Data>; + +export type GetPriceParams = Pick< + Params<(keyof PriceData["relationships"])[]>, + "include" +>; +export type ListPricesParams = Params< + GetPriceParams["include"], + { variantId?: string | number } +>; +export type Price = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListPrices = LemonSqueezyResponse< + PriceData[], + Pick, + Pick +>; diff --git a/src/products/index.ts b/src/products/index.ts new file mode 100644 index 0000000..ee4f80e --- /dev/null +++ b/src/products/index.ts @@ -0,0 +1,48 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetProductParams, + ListProducts, + ListProductsParams, + Product, +} from "./types"; + +/** + * Retrieve a product. + * + * @param productId The given product id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A product object. + */ +export function getProduct( + productId: number | string, + params: GetProductParams = {} +) { + requiredCheck({ productId }); + return $fetch({ + path: `/v1/products/${productId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all products. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return products belonging to the store with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of product objects ordered by `name`. + */ +export function listProducts(params: ListProductsParams = {}) { + return $fetch({ + path: `/v1/products${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/products/types.ts b/src/products/types.ts new file mode 100644 index 0000000..b1449f4 --- /dev/null +++ b/src/products/types.ts @@ -0,0 +1,98 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the store this product belongs to. + */ + store_id: number; + /** + * The name of the product. + */ + name: string; + /** + * The slug used to identify the product. + */ + slug: string; + /** + * The description of the product in HTML. + */ + description: string; + /** + * The status of the product. Either `draft` or `published`. + */ + status: "published" | "draft"; + /** + * The formatted status of the product. + */ + status_formatted: string; + /** + * A URL to the thumbnail image for this product (if one exists). The image will be 100x100px in size. + */ + thumb_url: string; + /** + * A URL to the large thumbnail image for this product (if one exists). The image will be 1000x1000px in size. + */ + large_thumb_url: string; + /** + * A positive integer in cents representing the price of the product. + */ + price: number; + /** + * A human-readable string representing the price of the product (e.g. `$9.99`). + */ + price_formatted: string; + /** + * If this product has multiple variants, this will be a positive integer in cents representing the price of the cheapest variant. Otherwise, it will be `null`. + */ + from_price: number | null; + /** + * If this product has multiple variants, this will be a positive integer in cents representing the price of the most expensive variant. Otherwise, it will be `null`. + */ + to_price: number | null; + /** + * Has the value `true` if this is a “pay what you want” product where the price can be set by the customer at checkout. + */ + pay_what_you_want: boolean; + /** + * A URL to purchase this product using the Lemon Squeezy checkout. + */ + buy_now_url: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type ProductData = Data>; + +export type GetProductParams = Pick< + Params<(keyof ProductData["relationships"])[]>, + "include" +>; +export type ListProductsParams = Params< + GetProductParams["include"], + { storeId?: string | number } +>; +export type Product = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListProducts = LemonSqueezyResponse< + ProductData[], + Pick, + Pick +>; diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..9857c42 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,46 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetStoreParams, + ListStores, + ListStoresParams, + Store, +} from "./types"; + +/** + * Retrieve a store. + * + * @param storeId (Required) The given store id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A store object. + */ +export function getStore( + storeId: number | string, + params: GetStoreParams = {} +) { + requiredCheck({ storeId }); + return $fetch({ + path: `/v1/stores/${storeId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all stores. + * + * @param [params] (Optional) Additional parameters. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of `store` objects ordered by name. + */ +export function listStores(params: ListStoresParams = {}) { + return $fetch({ + path: `/v1/stores${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/stores/types.ts b/src/stores/types.ts new file mode 100644 index 0000000..732f688 --- /dev/null +++ b/src/stores/types.ts @@ -0,0 +1,103 @@ +import type { + Data, + ISO3166Alpha2CountryCode, + ISO4217CurrencyCode, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The name of the store. + */ + name: string; + /** + * The slug used to identify the store. + */ + slug: string; + /** + * The domain of the store, either in the format `{slug}.lemonsqueezy.com` or a [custom domain](https://docs.lemonsqueezy.com/help/domains/adding-a-custom-domain). + */ + domain: string; + /** + * The fully-qualified URL for the store (e.g. `https://{slug}.lemonsqueezy.com` or `https://customdomain.com` when a [custom domain](https://docs.lemonsqueezy.com/help/domains/adding-a-custom-domain) is set up). + */ + url: string; + /** + * The URL for the store avatar. + */ + avatar_url: string; + /** + * The current billing plan for the store (e.g. `fresh`, `sweet`). + */ + plan: string; + /** + * The [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) two-letter country code for the store (e.g.`US`, `GB`, etc). + */ + country: ISO3166Alpha2CountryCode; + /** + * The full country name for the store (e.g. `United States`, `United Kingdom`, etc). + */ + country_nicename: string; + /** + * The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code for the store (e.g. `USD`, `GBP`, etc). + */ + currency: ISO4217CurrencyCode; + /** + * A count of the all-time total sales made by this store. + */ + total_sales: number; + /** + * A positive integer in cents representing the total all-time revenue of the store in USD. + */ + total_revenue: number; + /** + * A count of the sales made by this store in the last 30 days. + */ + thirty_day_sales: number; + /** + * A positive integer in cents representing the total revenue of the store in USD in the last 30 days. + */ + thirty_day_revenue: number; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; +}; +type StoreData = Data< + Attributes, + Pick< + Relationships, + | "products" + | "orders" + | "subscriptions" + | "discounts" + | "license-keys" + | "webhooks" + > +>; + +export type Store = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListStores = LemonSqueezyResponse< + StoreData[], + Pick, + Pick +>; +export type GetStoreParams = Pick< + Params<(keyof StoreData["relationships"])[]>, + "include" +>; +export type ListStoresParams = Omit< + Params, + "filter" +>; diff --git a/src/subscriptionInvoices/index.ts b/src/subscriptionInvoices/index.ts new file mode 100644 index 0000000..e4dcb55 --- /dev/null +++ b/src/subscriptionInvoices/index.ts @@ -0,0 +1,53 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetSubscriptionInvoiceParams, + ListSubscriptionInvoices, + ListSubscriptionInvoicesParams, + SubscriptionInvoice, +} from "./types"; + +/** + * Retrieve a subscription invoice. + * + * @param subscriptionInvoiceId The given subscription invoice id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription invoice object. + */ +export function getSubscriptionInvoice( + subscriptionInvoiceId: number | string, + params: GetSubscriptionInvoiceParams = {} +) { + requiredCheck({ subscriptionInvoiceId }); + return $fetch({ + path: `/v1/subscription-invoices/${subscriptionInvoiceId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all subscription invoices. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return subscription invoices belonging to the store with this ID. + * @param [params.filter.status] (Optional) Only return subscription invoices with this status. + * @param [params.filter.refunded] (Optional) Only return subscription invoices that are `refunded` (the value should be `true` or `false`). + * @param [params.filter.subscriptionId] (Optional) Only return subscription invoices belonging to a subscription with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of subscription invoice objects ordered by `created_at` (descending). + */ +export function listSubscriptionInvoices( + params: ListSubscriptionInvoicesParams = {} +) { + return $fetch({ + path: `/v1/subscription-invoices${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/subscriptionInvoices/types.ts b/src/subscriptionInvoices/types.ts new file mode 100644 index 0000000..344c821 --- /dev/null +++ b/src/subscriptionInvoices/types.ts @@ -0,0 +1,204 @@ +import type { + Data, + ISO4217CurrencyCode, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type InvoiceBillingReason = "initial" | "renewal" | "renewal"; +type InvoiceCardBrand = + | "visa" + | "mastercard" + | "amex" + | "discover" + | "jcb" + | "diners" + | "unionpay"; +type InvoiceStatus = "pending" | "paid" | "void" | "refunded"; +type Attributes = { + /** + * The ID of the [Store](https://docs.lemonsqueezy.com/api/stores#the-store-object) this subscription invoice belongs to. + */ + store_id: number; + /** + * The ID of the [Subscription](https://docs.lemonsqueezy.com/api/subscriptions#the-subscription-object) associated with this subscription invoice. + */ + subscription_id: number; + /** + * The ID of the customer this subscription invoice belongs to. + */ + customer_id: number; + /** + * The full name of the customer. + */ + user_name: string; + /** + * The email address of the customer. + */ + user_email: string; + /** + * The reason for the invoice being generated. + * + * - `initial` - The initial invoice generated when the subscription is created. + * - `renewal` - A renewal invoice generated when the subscription is renewed. + * - `renewal` - An invoice generated when the subscription is updated. + */ + billing_reason: InvoiceBillingReason; + /** + * Lowercase brand of the card used to pay for the invoice. One of + * + * - `visa` + * - `mastercard` + * - `amex` + * - `discover` + * - `jcb` + * - `diners` + * - `unionpay` + * + * Will be empty for non-card payments. + */ + card_brand: InvoiceCardBrand | null; + /** + * The last 4 digits of the card used to pay for the invoice. Will be empty for non-card payments. + */ + card_last_four: string | null; + /** + * The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code for the invoice (e.g. `USD`, `GBP`, etc). + */ + currency: ISO4217CurrencyCode; + /** + * If the invoice currency is USD, this will always be `1.0`. Otherwise, this is the currency conversion rate used to determine the cost of the invoice in USD at the time of payment. + */ + currency_rate: string; + /** + * The status of the invoice. One of + * + * - `pending` - The invoice is waiting for payment. + * - `paid` - The invoice has been paid. + * - `void` - The invoice was cancelled or cannot be paid. + * - `refunded` - The invoice was paid but has since been fully refunded. + */ + status: InvoiceStatus; + /** + * The formatted status of the invoice. + */ + status_formatted: string; + /** + * A boolean value indicating whether the invoice has been refunded. + */ + refunded: boolean; + /** + * If the invoice has been refunded, this will be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the invoice was refunded. Otherwise, it will be `null`. + */ + refunded_at: string | null; + /** + * A positive integer in cents representing the subtotal of the invoice in the invoice currency. + */ + subtotal: number; + /** + * A positive integer in cents representing the total discount value applied to the invoice in the invoice currency. + */ + discount_total: number; + /** + * A positive integer in cents representing the tax applied to the invoice in the invoice currency. + */ + tax: number; + /** + * A positive integer in cents representing the total cost of the invoice in the invoice currency. + */ + total: number; + /** + * A positive integer in cents representing the subtotal of the invoice in USD. + */ + subtotal_usd: number; + /** + * A positive integer in cents representing the total discount value applied to the invoice in USD. + */ + discount_total_usd: number; + /** + * A positive integer in cents representing the tax applied to the invoice in USD. + */ + tax_usd: number; + /** + * A positive integer in cents representing the total cost of the invoice in USD. + */ + total_usd: number; + /** + * A human-readable string representing the subtotal of the invoice in the invoice currency (e.g. `$9.99`). + */ + subtotal_formatted: string; + /** + * A human-readable string representing the total discount value applied to the invoice in the invoice currency (e.g. `$9.99`). + */ + discount_total_formatted: string; + /** + * A human-readable string representing the tax applied to the invoice in the invoice currency (e.g. `$9.99`). + */ + tax_formatted: string; + /** + * A human-readable string representing the total cost of the invoice in the invoice currency (e.g. `$9.99`). + */ + total_formatted: string; + /** + * An object of customer-facing URLs for the invoice. It contains: + * + * - `invoice_url` - The unique URL to download a PDF of the invoice. Note: for security reasons, download URLs are signed (but do not expire). Will be `null` if status is `pending`. + */ + urls: { + invoice_url: string | null; + }; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the invoice was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the invoice was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type SubscriptionInvoiceData = Data< + Attributes, + Pick +>; + +export type GetSubscriptionInvoiceParams = Pick< + Params<(keyof SubscriptionInvoiceData["relationships"])[]>, + "include" +>; +export type ListSubscriptionInvoicesParams = Params< + GetSubscriptionInvoiceParams["include"], + { + /** + * Only return subscription invoices belonging to the store with this ID. + */ + storeId?: string | number; + /** + * Only return subscription invoices with this status. + */ + status?: InvoiceStatus; + /** + * Only return subscription invoices that are `refunded` (the value should be `true` or `false`). + */ + refunded?: boolean; + /** + * Only return subscription invoices belonging to a subscription with this ID. + */ + subscriptionId?: string | number; + } +>; +export type SubscriptionInvoice = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListSubscriptionInvoices = LemonSqueezyResponse< + SubscriptionInvoiceData[], + Pick, + Pick +>; diff --git a/src/subscriptionItems/index.ts b/src/subscriptionItems/index.ts new file mode 100644 index 0000000..9d08b5f --- /dev/null +++ b/src/subscriptionItems/index.ts @@ -0,0 +1,99 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; + +import type { + GetSubscriptionItemParams, + ListSubscriptionItems, + ListSubscriptionItemsParams, + SubscriptionItem, + SubscriptionItemCurrentUsage, +} from "./types"; + +/** + * Retrieve a subscription item. + * + * @param subscriptionItemId The given subscription item id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription item object. + */ +export function getSubscriptionItem( + subscriptionItemId: number | string, + params: GetSubscriptionItemParams = {} +) { + return $fetch({ + path: `/v1/subscription-items/${subscriptionItemId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * Retrieve a subscription item's current usage. + * + * Note: this endpoint is only for subscriptions with usage-based billing enabled. It will return a `404 Not Found` response if the related subscription product/variant does not have usage-based billing enabled. + * + * @param subscriptionItemId The given subscription item id. + * @returns A meta object containing usage information. + */ +export function getSubscriptionItemCurrentUsage( + subscriptionItemId: number | string +) { + requiredCheck({ subscriptionItemId }); + return $fetch({ + path: `/v1/subscription-items/${subscriptionItemId}/current-usage`, + }); +} + +/** + * List all subscription items. + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.subscriptionId] (Optional) Only return subscription items belonging to a subscription with this ID. + * @param [params.filter.priceId] (Optional) Only return subscription items belonging to a price with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of subscription item objects ordered by `created_at` (descending). + */ +export function listSubscriptionItems( + params: ListSubscriptionItemsParams = {} +) { + return $fetch({ + path: `/v1/subscription-items${convertListParamsToQueryString(params)}`, + }); +} + +/** + * Update a subscription item. + * + * Note: this endpoint is only used with quantity-based billing. + * If the related subscription's product/variant has usage-based billing + * enabled, this endpoint will return a `422 Unprocessable Entity` response. + * + * @param subscriptionItemId The given subscription item id. + * @param quantity The unit quantity of the subscription. + * @returns A subscription item object. + */ +export function updateSubscriptionItem( + subscriptionItemId: string | number, + quantity: number +) { + requiredCheck({ subscriptionItemId }); + return $fetch({ + path: `/v1/subscription-items/${subscriptionItemId}`, + method: "PATCH", + body: { + data: { + type: "subscription-items", + id: subscriptionItemId.toString(), + attributes: { + quantity, + }, + }, + }, + }); +} diff --git a/src/subscriptionItems/types.ts b/src/subscriptionItems/types.ts new file mode 100644 index 0000000..6419dd9 --- /dev/null +++ b/src/subscriptionItems/types.ts @@ -0,0 +1,73 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Attributes = { + /** + * The ID of the [Subscription](https://docs.lemonsqueezy.com/api/subscriptions#the-subscription-object) associated with this subscription item. + */ + subscription_id: number; + /** + * The ID of the [Price](https://docs.lemonsqueezy.com/api/prices#the-price-object) associated with this subscription item. + */ + price_id: number; + /** + * A positive integer representing the unit quantity of this subscription item. + * + * Will be if the related subscription product/variant has usage-based billing enabled. + */ + quantity: number; + /** + * A boolean value indicating whether the related subscription product/variant has usage-based billing enabled. + */ + is_usage_based: boolean; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was last updated. + */ + updated_at: string; +}; +type SubscriptionItemData = Data< + Attributes, + Pick +>; + +export type GetSubscriptionItemParams = Pick< + Params<(keyof SubscriptionItemData["relationships"])[]>, + "include" +>; +export type ListSubscriptionItemsParams = Params< + GetSubscriptionItemParams["include"], + { subscriptionId?: number | string; priceId?: number | string } +>; +export type SubscriptionItem = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type SubscriptionItemCurrentUsage = Omit< + LemonSqueezyResponse< + unknown, + Pick< + Meta, + | "period_start" + | "period_end" + | "quantity" + | "interval_unit" + | "interval_quantity" + > + >, + "data" | "links" +>; +export type ListSubscriptionItems = LemonSqueezyResponse< + SubscriptionItemData[], + Pick, + Pick +>; diff --git a/src/subscriptions/index.ts b/src/subscriptions/index.ts new file mode 100644 index 0000000..a480e8f --- /dev/null +++ b/src/subscriptions/index.ts @@ -0,0 +1,113 @@ +import { + $fetch, + convertIncludeToQueryString, + convertKeys, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetSubscriptionParams, + ListSubscriptions, + ListSubscriptionsParams, + Subscription, + UpdateSubscription, +} from "./types"; + +/** + * Retrieve a subscription. + * + * @param subscriptionId The given subscription id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A subscription object. + */ +export function getSubscription( + subscriptionId: number | string, + params: GetSubscriptionParams = {} +) { + requiredCheck({ subscriptionId }); + return $fetch({ + path: `/v1/subscriptions/${subscriptionId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * Update a subscription. + * + * @param subscriptionId The given subscription id. + * @param subscription Subscription information that needs to be updated. + * @returns A subscription object. + */ +export function updateSubscription( + subscriptionId: string | number, + updateSubscription: UpdateSubscription +) { + requiredCheck({ subscriptionId }); + const { + variantId, + cancelled, + billingAnchor, + invoiceImmediately, + disableProrations, + pause, + } = updateSubscription; + + const attributes = convertKeys({ + variantId, + cancelled, + billingAnchor, + invoiceImmediately, + disableProrations, + pause, + }); + + return $fetch({ + path: `/v1/subscriptions/${subscriptionId}`, + method: "PATCH", + body: { + data: { + type: "subscriptions", + id: subscriptionId.toString(), + attributes, + }, + }, + }); +} + +/** + * Cancel a subscription. + * + * @param subscriptionId The given subscription id + * @returns The Subscription object in a cancelled state. + */ +export function cancelSubscription(subscriptionId: string | number) { + requiredCheck({ subscriptionId }); + + return $fetch({ + path: `/v1/subscriptions/${subscriptionId}`, + method: "DELETE", + }); +} + +/** + * List all subscriptions. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter.storeId] (Optional) Only return subscriptions belonging to the store with this ID. + * @param [params.filter.orderId] (Optional) Only return subscriptions belonging to the order with this ID. + * @param [params.filter.orderItemId] (Optional) Only return subscriptions belonging to the order item with this ID. + * @param [params.filter.productId] (Optional) Only return subscriptions belonging to the product with this ID. + * @param [params.filter.variantId] (Optional) Only return subscriptions belonging to the variant with this ID. + * @param [params.filter.userEmail] (Optional) Only return subscriptions where the `user_email` field is equal to this email address. + * @param [params.filter.status] (Optional) Only return subscriptions with this status. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of subscription objects ordered by `created_at` (descending). + */ +export function listSubscriptions(params: ListSubscriptionsParams = {}) { + return $fetch({ + path: `/v1/subscriptions${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/subscriptions/types.ts b/src/subscriptions/types.ts new file mode 100644 index 0000000..762f219 --- /dev/null +++ b/src/subscriptions/types.ts @@ -0,0 +1,323 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; +type SubscriptionStatus = + | "on_trial" + | "active" + | "paused" + | "pause" + | "past_due" + | "unpaid" + | "cancelled" + | "expired" + | "cancelled"; +type CardBrand = + | "visa" + | "mastercard" + | "amex" + | "discover" + | "jcb" + | "diners" + | "unionpay"; +type Pause = { + /** + * - `void` - If you can't offer your services for a period of time (for maintenance as an example), you can void invoices so your customers aren't charged. + * - `free` - Offer your subscription services for free, whilst halting payment collection. + */ + mode: "void" | "free"; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription will continue collecting payments. + */ + resumes_at?: string | null; +}; +type FirstSubscriptionItem = { + /** + * The ID of the subscription item. + */ + id: number; + /** + * The ID of the subscription. + */ + subscription_id: number; + /** + * The ID of the price + */ + price_id: number; + /** + * The quantity of the subscription item. + */ + quantity: number; + /** + * A boolean value indicating whether the related subscription product/variant has usage-based billing enabled. + * + * Note: Not in the documentation, but in the response + */ + is_usage_based: boolean; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was last updated. + */ + updated_at: string; +}; + +type Attributes = { + /** + * The ID of the store this subscription belongs to. + */ + store_id: number; + /** + * The ID of the customer this subscription belongs to. + */ + customer_id: number; + /** + * The ID of the order associated with this subscription. + */ + order_id: number; + /** + * The ID of the order item associated with this subscription. + */ + order_item_id: number; + /** + * The ID of the product associated with this subscription. + */ + product_id: number; + /** + * The ID of the variant associated with this subscription. + */ + variant_id: number; + /** + * The name of the product. + */ + product_name: string; + /** + * The name of the variant. + */ + variant_name: string; + /** + * The full name of the customer. + */ + user_name: string; + /** + * The email address of the customer. + */ + user_email: string; + /** + * The status of the subscription. One of + * + * - `on_trial` + * - `active` + * - `paused` - The subscription's payment collection has been paused. See the `pause` attribute below for more information. + * - `past_due` - A renewal payment has failed. The subscription will go through [4 payment retries](https://docs.lemonsqueezy.com/help/online-store/recovery-dunning#failed-payments) over the course of 2 weeks. If a retry is successful, the subscription's status changes back to `active`. If all four retries are unsuccessful, the status is changed to `unpaid`. + * - `unpaid` - [Payment recovery](https://docs.lemonsqueezy.com/help/online-store/recovery-dunning#failed-payments) has been unsuccessful in capturing a payment after 4 attempts. If dunning is enabled in your store, your dunning rules now will determine if the subscription becomes `expired` after a certain period. If dunning is turned off, the status remains `unpaid` (it is up to you to determine what this means for users of your product). + * - `cancelled` - The customer or store owner has cancelled future payments, but the subscription is still technically active and valid (on a "grace period"). The `ends_at` value shows the date-time when the subscription is scheduled to expire. + * - `expired` - The subscription has ended (either it had previously been `cancelled` and the grace period created from its final payment has run out, or it was previously `unpaid` and the subscription was not re-activated during dunning). The `ends_at` value shows the date-time when the subscription expired. Customers should no longer have access to your product. + */ + status: SubscriptionStatus; + /** + * The title-case formatted status of the subscription. + * + * For example, when `status` is `active`, `status_formatted` will be `Active` and `past_due` will be `Past due`. + */ + status_formatted: string; + /** + * Lowercase brand of the card used to pay for the latest subscription payment. One of + * + * - `visa` + * - `mastercard` + * - `amex` + * - `discover` + * - `jcb` + * - `diners` + * - `unionpay` + * + * Will be empty for non-card payments. + */ + card_brand: CardBrand | null; + /** + * The last 4 digits of the card used to pay for the latest subscription payment. Will be empty for non-card payments. + */ + card_last_four: string | null; + /** + * An object containing the payment collection pause behavior options for the subscription, if set. Options include: + * + * - `mode` - Defines payment pause behavior, can be one of: + * - `void` - If you can't offer your services for a period of time (for maintenance as an example), you can void invoices so your customers aren't charged. + * - `free` - Offer your subscription services for free, whilst halting payment collection. + * - `resumes_at` - (Optional) An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription will continue collecting payments. + * + * For a subscription that isn't in the `paused` state, the pause object will be `null`. + */ + pause: Pause | null; + /** + * A boolean indicating if the subscription has been cancelled. + * + * When `cancelled` is `true`: + * + * - `status` will be `cancelled` + * - `ends_at` will be populated with a date-time string + */ + cancelled: boolean; + /** + * If the subscription has a free trial (`status` is `on_trial`), this will be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the trial period ends. For all other status values, this will be `null`. + */ + trial_ends_at: string | null; + /** + * An integer representing a day of the month (`21` equals `21st day of the month`). This is the day on which subscription invoice payments are collected. + */ + billing_anchor: number; + /** + * An object representing the first subscription item belonging to this subscription. + * + * - `id` - The ID of the subscription item. + * - `subscription_id` - The ID of the subscription. + * - `price_id` - The ID of the price + * - `quantity` - The quantity of the subscription item. + * - `created_at` - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was created. + * - `updated_at` - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription item was last updated. + * + * Will be `null` if there is no subscription item, for example if the subscription is currently in a free trial. + */ + first_subscription_item: FirstSubscriptionItem | null; + /** + * An object of customer-facing URLs for managing the subscription. It contains + * + * - `update_payment_method` - A pre-signed URL for managing payment and billing information for the subscription. This can be used in conjunction with [Lemon.js](https://docs.lemonsqueezy.com/help/lemonjs/what-is-lemonjs) to allow your customer to change their billing information from within your application. The URL is valid for 24 hours from time of request. + * - `customer_portal` - A pre-signed URL to the [Customer Portal](https://docs.lemonsqueezy.com/help/online-store/customer-portal), which allows customers to fully manage their subscriptions and billing information from within your application. The URL is valid for 24 hours from time of request. + */ + urls: { + update_payment_method: string; + customer_portal: string; + }; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating the end of the current billing cycle, and when the next invoice will be issued. This also applies to `past_due` subscriptions; `renews_at` will reflect the next renewal charge attempt. + */ + renews_at: string; + /** + * f the subscription has as `status` of `cancelled` or `expired`, this will be an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription expires (or expired). For all other `status` values, this will be `null`. + */ + ends_at: string | null; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type SubscriptionData = Data< + Attributes, + Pick< + Relationships, + | "store" + | "customer" + | "order" + | "order-item" + | "product" + | "variant" + | "subscription-items" + | "subscription-invoices" + > +>; + +export type GetSubscriptionParams = Pick< + Params<(keyof SubscriptionData["relationships"])[]>, + "include" +>; +export type ListSubscriptionsParams = Params< + GetSubscriptionParams["include"], + { + /** + * Only return subscriptions belonging to the store with this ID. + */ + storeId?: string | number; + /** + * Only return subscriptions belonging to the order with this ID. + */ + orderId?: string | number; + /** + * Only return subscriptions belonging to the order item with this ID. + */ + orderItemId?: string | number; + /** + * Only return subscriptions belonging to the product with this ID. + */ + productId?: string | number; + /** + * Only return subscriptions belonging to the variant with this ID. + */ + variantId?: string | number; + /** + * Only return subscriptions where the `user_email` field is equal to this email address. + */ + userEmail?: string; + /** + * Only return subscriptions with this status. + */ + status?: SubscriptionStatus; + } +>; +export type UpdateSubscription = Partial<{ + /** + * The ID of the [Variant](https://docs.lemonsqueezy.com/api/variants) you want to switch this subscription to. Required if changing a subscription's product/variant. + */ + variantId: number; + /** + * An object containing the payment collection pause behavior options for the subscription. Options include: + * + * - `mode` - Defines payment pause behavior, can be one of: + * - `void` - If you can't offer your services for a period of time (for maintenance as an example), you can void invoices so your customers aren't charged. + * - `free` - Offer your subscription services for free, whilst halting payment collection. + * - `resumes_at` (optional) - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the subscription will continue collecting payments. + * + * You can set the pause object to `null` to unpause the subscription. + */ + pause: { + mode: "void" | "free"; + resumesAt?: string | null; + }; + /** + * Set as `true` to cancel the subscription. You can resume a subscription (before the `ends_at` date) by setting this to `false`. + */ + cancelled: boolean; + /** + * - Use an integer representing a day of the month (`21` equals `21st day of the month`) to change the day on which subscription invoice payments are collected. + * - Use `null` or `0` to reset the billing anchor to the current date. Doing this will also remove an active trial. + * + * Setting this value to a valid integer (1-31) will set the billing anchor to the next occurrence of that day. For example, if on the 21st of January you set the subscription billing anchor to the 1st, the next occurrence of that day is February 1st. All invoices from that point on will be generated on the 1st of the month. + * + * If the current month doesn’t contain the day that matches your `billing_anchor` (for example, if the `billing_anchor` is 31 and the month is November), the customer will be charged on the last day of the month. + * + * When setting a new billing anchor day, we calculate the next occurrence and issue a paid, prorated trial which ends on the next occurrence date. When the trial ends, the customer is charged for the full prorated amount. + */ + billingAnchor: number | null; + /** + * If `true`, any updates to the subscription will be charged immediately. A new prorated invoice will be generated and payment attempted. Defaults to `false`. Note that this will be overridden by the `disable_prorations` option if used. + */ + invoiceImmediately: boolean; + /** + * If `true`, no proration will be charged and the customer will simply be charged the new price at the next renewal. Defaults to `false`. Note that this will override the `invoice_immediately` option if used. + */ + disableProrations: boolean; +}>; +export type Subscription = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListSubscriptions = LemonSqueezyResponse< + SubscriptionData[], + Pick, + Pick +>; diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..8473f8a --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,6 @@ +export type Flatten = T extends object + ? T extends Array + ? Flatten[] + : { [P in keyof T]: Flatten } + : T; +export type IntervalUnit = "day" | "week" | "month" | "year"; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..9adcfef --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,3 @@ +export * from "./response"; +export * from "./common"; +export * from "./iso"; diff --git a/src/types/iso.ts b/src/types/iso.ts new file mode 100644 index 0000000..6eb45aa --- /dev/null +++ b/src/types/iso.ts @@ -0,0 +1,437 @@ +/** + * ISO 3166-1 alpha-2 + * + * https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements + */ +export type ISO3166Alpha2CountryCode = + | "AD" + | "AE" + | "AF" + | "AG" + | "AI" + | "AL" + | "AM" + | "AO" + | "AQ" + | "AR" + | "AS" + | "AT" + | "AU" + | "AW" + | "AX" + | "AZ" + | "BA" + | "BB" + | "BD" + | "BE" + | "BF" + | "BG" + | "BH" + | "BI" + | "BJ" + | "BL" + | "BM" + | "BN" + | "BO" + | "BQ" + | "BR" + | "BS" + | "BT" + | "BV" + | "BW" + | "BY" + | "BZ" + | "CA" + | "CC" + | "CD" + | "CF" + | "CG" + | "CH" + | "CI" + | "CK" + | "CL" + | "CM" + | "CN" + | "CO" + | "CR" + | "CU" + | "CV" + | "CW" + | "CX" + | "CY" + | "CZ" + | "DE" + | "DJ" + | "DK" + | "DM" + | "DO" + | "DZ" + | "EC" + | "EE" + | "EG" + | "EH" + | "ER" + | "ES" + | "ET" + | "FI" + | "FJ" + | "FK" + | "FM" + | "FO" + | "FR" + | "GA" + | "GB" + | "GD" + | "GE" + | "GF" + | "GG" + | "GH" + | "GI" + | "GL" + | "GM" + | "GN" + | "GP" + | "GQ" + | "GR" + | "GS" + | "GT" + | "GU" + | "GW" + | "GY" + | "HK" + | "HM" + | "HN" + | "HR" + | "HT" + | "HU" + | "ID" + | "IE" + | "IL" + | "IM" + | "IN" + | "IO" + | "IQ" + | "IR" + | "IS" + | "IT" + | "JE" + | "JM" + | "JO" + | "JP" + | "KE" + | "KG" + | "KH" + | "KI" + | "KM" + | "KN" + | "KP" + | "KR" + | "KW" + | "KY" + | "KZ" + | "LA" + | "LB" + | "LC" + | "LI" + | "LK" + | "LR" + | "LS" + | "LT" + | "LU" + | "LV" + | "LY" + | "MA" + | "MC" + | "MD" + | "ME" + | "MF" + | "MG" + | "MH" + | "MK" + | "ML" + | "MM" + | "MN" + | "MO" + | "MP" + | "MQ" + | "MR" + | "MS" + | "MT" + | "MU" + | "MV" + | "MW" + | "MX" + | "MY" + | "MZ" + | "NA" + | "NC" + | "NE" + | "NF" + | "NG" + | "NI" + | "NL" + | "NO" + | "NP" + | "NR" + | "NU" + | "NZ" + | "OM" + | "PA" + | "PE" + | "PF" + | "PG" + | "PH" + | "PK" + | "PL" + | "PM" + | "PN" + | "PR" + | "PS" + | "PT" + | "PW" + | "PY" + | "QA" + | "RE" + | "RO" + | "RS" + | "RU" + | "RW" + | "SA" + | "SB" + | "SC" + | "SD" + | "SE" + | "SG" + | "SH" + | "SI" + | "SJ" + | "SK" + | "SL" + | "SM" + | "SN" + | "SO" + | "SR" + | "SS" + | "ST" + | "SV" + | "SX" + | "SY" + | "SZ" + | "TC" + | "TD" + | "TF" + | "TG" + | "TH" + | "TJ" + | "TK" + | "TL" + | "TM" + | "TN" + | "TO" + | "TR" + | "TT" + | "TV" + | "TW" + | "TZ" + | "UA" + | "UG" + | "UM" + | "US" + | "UY" + | "UZ" + | "VA" + | "VC" + | "VE" + | "VG" + | "VI" + | "VN" + | "VU" + | "WF" + | "WS" + | "YE" + | "YT" + | "ZA" + | "ZM" + | "ZW"; + +/** + * ISO 4217 Currency Code + * + * https://en.wikipedia.org/wiki/ISO_4217#Active_codes_(list_one) + */ +export type ISO4217CurrencyCode = + | "AED" + | "AFN" + | "ALL" + | "AMD" + | "ANG" + | "AOA" + | "ARS" + | "AUD" + | "AWG" + | "AZN" + | "BAM" + | "BBD" + | "BDT" + | "BGN" + | "BHD" + | "BIF" + | "BMD" + | "BND" + | "BOB" + | "BOV" + | "BRL" + | "BSD" + | "BTN" + | "BWP" + | "BYN" + | "BZD" + | "CAD" + | "CDF" + | "CHE" + | "CHF" + | "CHW" + | "CLF" + | "CLP" + | "CNY" + | "COP" + | "COU" + | "CRC" + | "CUP" + | "CVE" + | "CZK" + | "DJF" + | "DKK" + | "DOP" + | "DZD" + | "EGP" + | "ERN" + | "ETB" + | "EUR" + | "FJD" + | "FKP" + | "GBP" + | "GEL" + | "GHS" + | "GIP" + | "GMD" + | "GNF" + | "GTQ" + | "GYD" + | "HKD" + | "HNL" + | "HTG" + | "HUF" + | "IDR" + | "ILS" + | "INR" + | "IQD" + | "IRR" + | "ISK" + | "JMD" + | "JOD" + | "JPY" + | "KES" + | "KGS" + | "KHR" + | "KMF" + | "KPW" + | "KRW" + | "KWD" + | "KYD" + | "KZT" + | "LAK" + | "LBP" + | "LKR" + | "LRD" + | "LSL" + | "LYD" + | "MAD" + | "MDL" + | "MGA" + | "MKD" + | "MMK" + | "MNT" + | "MOP" + | "MRU" + | "MUR" + | "MVR" + | "MWK" + | "MXN" + | "MXV" + | "MYR" + | "MZN" + | "NAD" + | "NGN" + | "NIO" + | "NOK" + | "NPR" + | "NZD" + | "OMR" + | "PAB" + | "PEN" + | "PGK" + | "PHP" + | "PKR" + | "PLN" + | "PYG" + | "QAR" + | "RON" + | "RSD" + | "RUB" + | "RWF" + | "SAR" + | "SBD" + | "SCR" + | "SDG" + | "SEK" + | "SGD" + | "SHP" + | "SLL" + | "SOS" + | "SRD" + | "SSP" + | "STN" + | "SYP" + | "SZL" + | "THB" + | "TJS" + | "TMT" + | "TND" + | "TOP" + | "TRY" + | "TTD" + | "TWD" + | "TZS" + | "UAH" + | "UGX" + | "USD" + | "USN" + | "UYI" + | "UYU" + | "UZS" + | "VES" + | "VND" + | "VUV" + | "WST" + | "XAF" + | "XAG" + | "XAU" + | "XBA" + | "XBB" + | "XBC" + | "XBD" + | "XCD" + | "XDR" + | "XFU" + | "XOF" + | "XPD" + | "XPF" + | "XPT" + | "XSU" + | "XTS" + | "XUA" + | "XXX" + | "YER" + | "ZAR" + | "ZMW"; diff --git a/src/types/response/data.ts b/src/types/response/data.ts new file mode 100644 index 0000000..39d7b86 --- /dev/null +++ b/src/types/response/data.ts @@ -0,0 +1,21 @@ +export type Data = { + /** + * The type of the resource (e.g. `products`, `orders`, etc.) + */ + type: string; + /** + * The id of the resource + */ + id: string; + /** + * An object representing the resources data + */ + attributes: A; + relationships: R; + /** + * API Url + */ + links: { + self: string; + }; +}; diff --git a/src/types/response/index.ts b/src/types/response/index.ts new file mode 100644 index 0000000..64ff68a --- /dev/null +++ b/src/types/response/index.ts @@ -0,0 +1,20 @@ +import type { Data } from "./data"; + +export type LemonSqueezyResponse< + D, + M = unknown, + L = unknown, + I = Data>[], +> = { + jsonapi: { version: string }; + links: L; + meta: M; + data: D; + included?: I; +}; + +export type { Links } from "./links"; +export type { Meta } from "./meta"; +export type { Data } from "./data"; +export type { Params } from "./params"; +export type { Relationships } from "./relationships"; diff --git a/src/types/response/links.ts b/src/types/response/links.ts new file mode 100644 index 0000000..e993ced --- /dev/null +++ b/src/types/response/links.ts @@ -0,0 +1,7 @@ +export type Links = { + self: string; + first: string; + last: string; + next?: string; + prev?: string; +}; diff --git a/src/types/response/meta.ts b/src/types/response/meta.ts new file mode 100644 index 0000000..e542e84 --- /dev/null +++ b/src/types/response/meta.ts @@ -0,0 +1,21 @@ +import type { IntervalUnit } from "../index"; + +type MetaPage = { + currentPage: number; + from: number; + lastPage: number; + perPage: number; + to: number; + total: number; +}; + +export type Meta = { + test_mode: boolean; + page: MetaPage; + // Here is the response meta to retrieve a subscription item's current usage + period_start: string; + period_end: string; + quantity: number; + interval_unit: IntervalUnit; + interval_quantity: number; +}; diff --git a/src/types/response/params.ts b/src/types/response/params.ts new file mode 100644 index 0000000..927a6f7 --- /dev/null +++ b/src/types/response/params.ts @@ -0,0 +1,9 @@ +type Page = { + number?: number; + size?: number; +}; +export type Params> = Partial<{ + include: I; + filter: F; + page: Page; +}>; diff --git a/src/types/response/relationships.ts b/src/types/response/relationships.ts new file mode 100644 index 0000000..e64b359 --- /dev/null +++ b/src/types/response/relationships.ts @@ -0,0 +1,62 @@ +type Types = + | "stores" + | "customers" + | "products" + | "variants" + | "prices" + | "files" + | "orders" + | "order-items" + | "subscriptions" + | "subscription-invoices" + | "subscription-items" + | "usage-records" + | "discounts" + | "discount-redemptions" + | "license-keys" + | "license-key-instances" + | "checkouts" + | "webhooks"; + +type RelationshipKeys = + | "store" + | "product" + | "variant" + | "customer" + | "order" + | "order-item" + | "subscription" + | "price" + | "price-model" + | "subscription-item" + | "discount" + | "license-key" + | Types; +// Data Type +// | 'stores' +// | 'customers' +// | 'products' +// | 'variants' +// | 'prices' +// | 'files' +// | 'orders' +// | 'order-items' +// | 'subscriptions' +// | 'subscription-invoices' +// | 'subscription-items' +// | 'usage-records' +// | 'discounts' +// | 'discount-redemptions' +// | 'license-keys' +// | 'license-key-instances' +// | 'checkouts' +// | 'webhooks' + +type RelationshipLinks = { + links: { + related: string; + self: string; + }; + data?: { id: string; type: Types }[]; +}; +export type Relationships = Record; diff --git a/src/usageRecords/index.ts b/src/usageRecords/index.ts new file mode 100644 index 0000000..5e4e5b5 --- /dev/null +++ b/src/usageRecords/index.ts @@ -0,0 +1,84 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetUsageRecordParams, + ListUsageRecords, + ListUsageRecordsParams, + NewUsageRecord, + UsageRecord, +} from "./types"; + +/** + * Retrieve a usage record. + * + * @param usageRecordId The usage record id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A usage record object. + */ +export function getUsageRecord( + usageRecordId: number | string, + params: GetUsageRecordParams = {} +) { + requiredCheck({ usageRecordId }); + return $fetch({ + path: `/v1/usage-records/${usageRecordId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all usage records. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.subscriptionItemId] (Optional) Only return usage records belonging to the subscription item with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of usage record objects ordered by `created_at` (descending). + */ +export function listUsageRecords(params: ListUsageRecordsParams = {}) { + return $fetch({ + path: `/v1/usage-records${convertListParamsToQueryString(params)}`, + }); +} + +/** + * Create a usage record. + * + * @param usageRecord (Required) New usage record info. + * @param usageRecord.quantity (Required) A positive integer representing the usage to be reported. + * @param usageRecord.subscriptionItemId (Required) The subscription item this usage record belongs to. + * @param [usageRecord.action] (Optional) The type of record. `increment` or `set`. Defaults to `increment` if omitted. + * @returns A usage record object. + */ +export function createUsageRecord(usageRecord: NewUsageRecord) { + const { quantity, action = "increment", subscriptionItemId } = usageRecord; + requiredCheck({ quantity, subscriptionItemId }); + return $fetch({ + path: "/v1/usage-records", + method: "POST", + body: { + data: { + type: "usage-records", + attributes: { + quantity, + action, + }, + relationships: { + "subscription-item": { + data: { + type: "subscription-items", + id: subscriptionItemId.toString(), + }, + }, + }, + }, + }, + }); +} diff --git a/src/usageRecords/types.ts b/src/usageRecords/types.ts new file mode 100644 index 0000000..c9d3f8e --- /dev/null +++ b/src/usageRecords/types.ts @@ -0,0 +1,80 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type UsageRecordAction = "increment" | "set"; +type Attributes = { + /** + * The ID of the subscription item this usage record belongs to. + */ + subscription_item_id: number; + /** + * A positive integer representing the usage to be reported. + */ + quantity: number; + /** + * The type of record. One of + * + * - `increment` - The provided quantity was added to existing records for the current billing period. + * - `set` - The provided quantity was set as the total usage for the current billing period. + */ + action: UsageRecordAction; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; +}; +type UsageRecordData = Data< + Attributes, + Pick +>; + +export type GetUsageRecordParams = Pick< + Params<(keyof UsageRecordData["relationships"])[]>, + "include" +>; +export type ListUsageRecordsParams = Params< + GetUsageRecordParams["include"], + { subscriptionItemId?: number | string } +>; +export type NewUsageRecord = { + /** + * A positive integer representing the usage to be reported. + */ + quantity: number; + /** + * The type of record. One of + * + * - `increment` - Add the provided quantity to existing records for the current billing period. + * - `set` - Set the quantity for the current billing period to the provided quantity. + * + * Defaults to `increment` if omitted. + * + * Note: increment should only be used alongside the "Sum of usage during period" aggregation setting. set should be only used alongside "Most recent usage during a period" and "Most recent usage" aggregation settings. [Read more about aggregation settings](https://docs.lemonsqueezy.com/help/products/usage-based-billing#usage-aggregation-setting). + * + * @default increment + */ + action?: UsageRecordAction; + /** + * The subscription item id this usage record belongs to. + */ + subscriptionItemId: number | string; +}; +export type UsageRecord = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListUsageRecords = LemonSqueezyResponse< + UsageRecordData[], + Pick, + Pick +>; diff --git a/src/users/index.ts b/src/users/index.ts new file mode 100644 index 0000000..585b508 --- /dev/null +++ b/src/users/index.ts @@ -0,0 +1,13 @@ +import { $fetch } from "../internal"; +import type { User } from "./types"; + +/** + * Retrieve the authenticated user. + * + * @returns A user object. + */ +export function getAuthenticatedUser() { + return $fetch({ + path: "/v1/users/me", + }); +} diff --git a/src/users/types.ts b/src/users/types.ts new file mode 100644 index 0000000..29a2f36 --- /dev/null +++ b/src/users/types.ts @@ -0,0 +1,40 @@ +import type { Data, LemonSqueezyResponse, Links, Meta } from "../types"; + +type Attributes = { + /** + * The name of the user. + */ + name: string; + /** + * The email address of the user. + */ + email: string; + /** + * A randomly generated hex color code for the user. We use this internally as the background color of an avatar if the user has not uploaded a custom avatar. + */ + color: string; + /** + * A URL to the avatar image for this user. If the user has not uploaded a custom avatar, this will point to their Gravatar URL. + */ + avatar_url: string; + /** + * Has the value `true` if the user has uploaded a custom avatar image. + */ + has_custom_avatar: boolean; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + * e.g. "2021-05-24T14:08:31.000000Z" + */ + createdAt: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + * e.g. "2021-08-26T13:24:54.000000Z" + */ + updatedAt: string; +}; + +export type User = LemonSqueezyResponse< + Omit, "relationships">, + Pick, + Pick +>; diff --git a/src/variants/index.ts b/src/variants/index.ts new file mode 100644 index 0000000..70ba518 --- /dev/null +++ b/src/variants/index.ts @@ -0,0 +1,49 @@ +import { + $fetch, + convertIncludeToQueryString, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetVariantParams, + ListVariants, + ListVariantsParams, + Variant, +} from "./types"; + +/** + * Retrieve a variant. + * + * @param variantId The given variant id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A variant object. + */ +export function getVariant( + variantId: number | string, + params: GetVariantParams = {} +) { + requiredCheck({ variantId }); + return $fetch({ + path: `/v1/variants/${variantId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * List all variants + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.productId] (Optional) Only return variants belonging to the product with this ID. + * @param [params.filter.status] (Optional) Only return variants with this status. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of variant objects ordered by `sort`. + */ +export function listVariants(params: ListVariantsParams = {}) { + return $fetch({ + path: `/v1/variants${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/variants/types.ts b/src/variants/types.ts new file mode 100644 index 0000000..0560de9 --- /dev/null +++ b/src/variants/types.ts @@ -0,0 +1,167 @@ +import type { + Data, + IntervalUnit, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +export type VariantStatus = "pending" | "draft" | "published"; + +type DeprecatedAttributes = { + /** + * `Deprecated` A positive integer in cents representing the price of the variant. + */ + price: number; + /** + * `Deprecated` Has the value true if this variant is a subscription. + */ + is_subscription: boolean; + /** + * `Deprecated` If this variant is a subscription, this is the frequency at which a subscription is billed. One of + * + * - `day` + * - `week` + * - `month` + * - `year` + */ + interval: null | IntervalUnit; + /** + * `Deprecated` If this variant is a subscription, this is the number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months. + */ + interval_count: null | number; + /** + * `Deprecated` Has the value `true` if this variant has a free trial period. Only available if the variant is a subscription. + */ + has_free_trial: boolean; + /** + * `Deprecated` The interval unit of the free trial. One of + * + * - `day` + * - `week` + * - `month` + * - `year` + */ + trial_interval: IntervalUnit; + /** + * `Deprecated` If interval count of the free trial. For example, a variant with `trial_interval=day` and `trial_interval_count=14` would have a free trial that lasts 14 days. + */ + trial_interval_count: number; + /** + * `Deprecated` Has the value true if this is a “pay what you want” variant where the price can be set by the customer at checkout. + */ + pay_what_you_want: boolean; + /** + * `Deprecated` If `pay_what_you_want` is `true`, this is the minimum price this variant can be purchased for, as a positive integer in cents. + */ + min_price: number; + /** + * `Deprecated` If `pay_what_you_want` is `true`, this is the suggested price for this variant shown at checkout, as a positive integer in cents. + */ + suggested_price: number; +}; + +type Attributes = { + /** + * The ID of the product this variant belongs to. + */ + product_id: number; + /** + * The name of the variant. + */ + name: string; + /** + * The slug used to identify the variant. + */ + slug: string; + /** + * The description of the variant in HTML. + */ + description: string; + /** + * Has the value `true` if this variant should generate license keys for the customer on purchase. + */ + has_license_keys: boolean; + /** + * The maximum number of times a license key can be activated for this variant. + */ + license_activation_limit: number; + /** + * Has the value `true` if license key activations are unlimited for this variant. + */ + is_license_limit_unlimited: boolean; + /** + * The number of units (specified in the `license_length_unit` attribute) until a license key expires. + */ + license_length_value: number; + /** + * The unit linked with the `license_length_value` attribute. One of + * + * - `days` + * - `months` + * - `years` + * + * For example, `license_length_value=3` and `license_length_unit=months` license keys will expire after 3 months. + */ + license_length_unit: "days" | "months" | "years"; + /** + * Has the value `true` if license keys should never expire. + * + * Note: If the variant is a subscription, the license key expiration will be linked to the status of the subscription (e.g. the license will expire when the subscription expires). + */ + is_license_length_unlimited: boolean; + /** + * An integer representing the order of this variant when displayed on the checkout. + */ + sort: number; + /** + * The status of the variant. One of + * + * - `pending` + * - `draft` + * - `published` + * + * Note: If a variant has a `pending` status and its product has no other variants, it is considered the “default” variant and is not shown as a separate option at checkout. + */ + status: VariantStatus; + /** + * The formatted status of the variant. + */ + status_formatted: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type VariantData = Data< + Attributes & DeprecatedAttributes, + Pick +>; + +export type GetVariantParams = Pick< + Params<(keyof VariantData["relationships"])[]>, + "include" +>; +export type ListVariantsParams = Params< + GetVariantParams["include"], + { productId?: number | string; status?: VariantStatus } +>; +export type Variant = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListVariants = LemonSqueezyResponse< + VariantData[], + Pick, + Pick +>; diff --git a/src/webhooks/index.ts b/src/webhooks/index.ts new file mode 100644 index 0000000..a1f999f --- /dev/null +++ b/src/webhooks/index.ts @@ -0,0 +1,134 @@ +import { + $fetch, + convertIncludeToQueryString, + convertKeys, + convertListParamsToQueryString, + requiredCheck, +} from "../internal"; +import type { + GetWebhookParams, + ListWebhooks, + ListWebhooksParams, + NewWebhook, + UpdateWebhook, + Webhook, +} from "./types"; + +/** + * Create a webhook. + * + * @param storeId The store id. + * @param webhook a new webhook info. + * @returns A webhook object. + */ +export function createWebhook(storeId: number | string, webhook: NewWebhook) { + requiredCheck({ storeId }); + + const { url, events, secret, testMode } = webhook; + + return $fetch({ + path: "/v1/webhooks", + method: "POST", + body: { + data: { + type: "webhooks", + attributes: convertKeys({ + url, + events, + secret, + testMode, + }), + relationships: { + store: { + data: { + type: "stores", + id: storeId.toString(), + }, + }, + }, + }, + }, + }); +} + +/** + * Retrieve a webhook. + * + * @param webhookId The given webhook id. + * @param [params] (Optional) Additional parameters. + * @param [params.include] (Optional) Related resources. + * @returns A webhook object. + */ +export function getWebhook( + webhookId: number | string, + params: GetWebhookParams = {} +) { + requiredCheck({ webhookId }); + return $fetch({ + path: `/v1/webhooks/${webhookId}${convertIncludeToQueryString(params.include)}`, + }); +} + +/** + * Update a webhook. + * + * @param webhookId The webhook id. + * @param webhook The webhook info you want to update. + * @returns A webhook object. + */ +export function updateWebhook( + webhookId: number | string, + webhook: UpdateWebhook +) { + requiredCheck({ webhookId }); + + const { url, events, secret } = webhook; + + return $fetch({ + path: `/v1/webhooks/${webhookId}`, + method: "PATCH", + body: { + data: { + id: webhookId.toString(), + type: "webhooks", + attributes: convertKeys({ + url, + events, + secret, + }), + }, + }, + }); +} + +/** + * Delete a webhook. + * + * @param webhookId The webhook id. + * @returns A `204` status code and `No Content` response on success. + */ +export function deleteWebhook(webhookId: number | string) { + requiredCheck({ webhookId }); + return $fetch({ + path: `/v1/webhooks/${webhookId}`, + method: "DELETE", + }); +} + +/** + * List all webhooks. + * + * @param [params] (Optional) Additional parameters. + * @param [params.filter] (Optional) Filter parameters. + * @param [params.filter.storeId] (Optional) Only return webhooks belonging to the store with this ID. + * @param [params.page] (Optional) Custom paginated queries. + * @param [params.page.number] (Optional) The parameter determine which page to retrieve. + * @param [params.page.size] (Optional) The parameter to determine how many results to return per page. + * @param [params.include] (Optional) Related resources. + * @returns A paginated list of webhook objects ordered by `created_at`. + */ +export function listWebhooks(params: ListWebhooksParams = {}) { + return $fetch({ + path: `/v1/webhooks${convertListParamsToQueryString(params)}`, + }); +} diff --git a/src/webhooks/types.ts b/src/webhooks/types.ts new file mode 100644 index 0000000..615eba1 --- /dev/null +++ b/src/webhooks/types.ts @@ -0,0 +1,100 @@ +import type { + Data, + LemonSqueezyResponse, + Links, + Meta, + Params, + Relationships, +} from "../types"; + +type Events = + | "order_created" + | "order_refunded" + | "subscription_created" + | "subscription_updated" + | "subscription_cancelled" + | "subscription_resumed" + | "subscription_expired" + | "subscription_paused" + | "subscription_unpaused" + | "subscription_payment_success" + | "subscription_payment_failed" + | "subscription_payment_recovered" + | "subscription_payment_refunded" + | "license_key_created" + | "license_key_updated"; + +type Attributes = { + /** + * The ID of the store this webhook belongs to. + */ + store_id: number; + /** + * The URL that events will be sent to. + */ + url: string; + /** + * An array of events that will be sent. + */ + events: Events[]; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the last webhook event was sent. Will be `null` if no events have been sent yet. + */ + last_sent_at: string | null; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was created. + */ + created_at: string; + /** + * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date-time string indicating when the object was last updated. + */ + updated_at: string; + /** + * A boolean indicating if the object was created within test mode. + */ + test_mode: boolean; +}; +type WebhookData = Data>; + +export type GetWebhookParams = Pick< + Params<(keyof WebhookData["relationships"])[]>, + "include" +>; +export type ListWebhooksParams = Params< + GetWebhookParams["include"], + { storeId?: number | string } +>; +export type Webhook = Omit< + LemonSqueezyResponse>, + "meta" +>; +export type ListWebhooks = LemonSqueezyResponse< + WebhookData[], + Pick, + Pick +>; +export type NewWebhook = { + /** + * (Required) A valid URL of the endpoint that should receive webhook events. + */ + url: string; + /** + * (Required) An array of webhook event types that should be sent to the webhook endpoint. [See the list of available event types](https://docs.lemonsqueezy.com/help/webhooks#event-types). + */ + events: Events[]; + /** + * (Required) A string used by Lemon Squeezy to sign requests for increased security. (Learn about receiving signed requests)[https://docs.lemonsqueezy.com/help/webhooks#signing-requests]. + * + * Note: The `secret` is never returned in the API. To view the secret of a webhook, open the webhook in your dashboard. + */ + secret: string; + /** + * Set this to `true` if the webhook should be created in test mode. + */ + testMode?: boolean; +}; +export type UpdateWebhook = { + url?: string; + events?: Events[]; + secret?: string; +}; diff --git a/test/checkouts/index.test.ts b/test/checkouts/index.test.ts new file mode 100644 index 0000000..3dbaabb --- /dev/null +++ b/test/checkouts/index.test.ts @@ -0,0 +1,990 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + createCheckout, + getCheckout, + lemonSqueezySetup, + listCheckouts, + listVariants, +} from "../../src"; +import type { NewCheckout } from "../../src/checkouts/types"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "checkouts"; +const storeId = import.meta.env.LEMON_SQUEEZY_STORE_ID!; +let variantId: number | string; +let newCheckoutId: number | string; + +beforeAll(async () => { + lemonSqueezySetup({ + apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY, + }); + const { data } = await listVariants(); + variantId = data!.data[0].id; +}); + +describe("Create a checkout", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await createCheckout(storeId, ""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("A new checkout should be created with the given store id and variant id", async () => { + const { + error, + data: _data, + statusCode, + } = await createCheckout(storeId, variantId); + expect(error).toBeNull(); + expect(statusCode).toEqual(201); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + } = attributes; + const items = [ + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(variant_id).toEqual(Number(variantId)); + console.log("url", url); + + // product_options + const { + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + } = product_options; + const productOptionItems = [ + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + ]; + for (const item of productOptionItems) expect(item).toBeDefined(); + expect(Object.keys(product_options).length).toEqual( + productOptionItems.length + ); + + // checkout_options + const { + embed, + media: checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + } = checkout_options; + const checkoutOptionItems = [ + embed, + checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + ]; + for (const item of checkoutOptionItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_options).length).toEqual( + checkoutOptionItems.length + ); + + // checkout_data + const { + email, + name: checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + } = checkout_data; + const checkoutDataItems = [ + email, + checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + ]; + for (const item of checkoutDataItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_data).length).toEqual(checkoutDataItems.length); + + // preview + if (preview) { + const { + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + } = preview; + const previewItems = [ + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + ]; + for (const item of previewItems) expect(item).toBeDefined(); + expect(Object.keys(preview).length).toEqual(previewItems.length); + } else { + } + + const { variant, store } = relationships; + expect(variant.links).toBeDefined(); + expect(store.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + + newCheckoutId = id; + }); + + it("A new checkout should be created with the given store id, variant id and new checkout info", async () => { + // NewCheckout + const newCheckout = { + // customPrice: 1, + productOptions: { + name: "New Checkout Test", + description: "a new checkout test", + media: ["https://google.com"], + redirectUrl: "https://google.com", + receiptButtonText: "Text Receipt", + receiptLinkUrl: "https://lemonsqueezy.com", + receiptThankYouNote: "Thanks to lemonsqueezy", + enabledVariants: [Number(variantId)], + confirmationTitle: "Thank you for your support", + confirmationMessage: "Thank you for subscribing and have a great day", + confirmationButtonText: "View Order", + }, + checkoutOptions: { + embed: true, + media: true, + logo: true, + desc: true, + dark: true, + discount: false, + buttonColor: "#ccc", + subscriptionPreview: true, + }, + checkoutData: { + email: "tita0x00@gmail.com", + name: "Lemon Squeezy Test", + billingAddress: { + country: "US", + }, + taxNumber: "12345", + // discountCode: 'Q3MJI5MG', + custom: { + userId: "1234567890", + userName: "Mrs.A", + nickName: "AAA", + }, + variantQuantities: [], + }, + expiresAt: null, + preview: true, + testMode: true, + }; + const { + error, + data: _data, + statusCode, + } = await createCheckout(storeId, variantId, newCheckout as NewCheckout); + expect(error).toBeNull(); + expect(statusCode).toEqual(201); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + } = attributes; + const items = [ + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(variant_id).toEqual(Number(variantId)); + console.log("url", url); + + // product_options + const { + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + } = product_options; + const productOptionItems = [ + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + ]; + for (const item of productOptionItems) expect(item).toBeDefined(); + expect(Object.keys(product_options).length).toEqual( + productOptionItems.length + ); + expect(name).toEqual(newCheckout.productOptions.name); + expect(description).toEqual(newCheckout.productOptions.description); + expect(receipt_button_text).toEqual( + newCheckout.productOptions.receiptButtonText + ); + expect(receipt_link_url).toEqual(newCheckout.productOptions.receiptLinkUrl); + expect(receipt_thank_you_note).toEqual( + newCheckout.productOptions.receiptThankYouNote + ); + + // checkout_options + const { + embed, + media: checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + } = checkout_options; + const checkoutOptionItems = [ + embed, + checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + ]; + for (const item of checkoutOptionItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_options).length).toEqual( + checkoutOptionItems.length + ); + expect(logo).toEqual(newCheckout.checkoutOptions.logo); + expect(desc).toEqual(newCheckout.checkoutOptions.desc); + expect(dark).toEqual(newCheckout.checkoutOptions.dark); + expect(button_color).toEqual(newCheckout.checkoutOptions.buttonColor); + expect(subscription_preview).toEqual( + newCheckout.checkoutOptions.subscriptionPreview + ); + + // checkout_data + const { + email, + name: checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + } = checkout_data; + const checkoutDataItems = [ + email, + checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + ]; + for (const item of checkoutDataItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_data).length).toEqual(checkoutDataItems.length); + expect(email).toEqual(newCheckout.checkoutData.email); + expect(checkoutDataName).toEqual(newCheckout.checkoutData.name); + + // preview + if (preview) { + const { + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + setup_fee, + setup_fee_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + setup_fee_formatted, + tax_formatted, + total_formatted, + } = preview; + const previewItems = [ + currency, + currency_rate, + subtotal, + discount_total, + tax, + setup_fee, + setup_fee_usd, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + setup_fee_formatted, + tax_formatted, + total_formatted, + ]; + for (const item of previewItems) expect(item).toBeDefined(); + expect(Object.keys(preview).length).toEqual(previewItems.length); + } else { + } + + const { variant, store } = relationships; + expect(variant.links).toBeDefined(); + expect(store.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + }); + + it("Failed to create a new checkout with 404 status code", async () => { + const { error, data, statusCode } = await createCheckout(storeId, "123"); + expect(error).toBeDefined(); + expect(statusCode).toEqual(404); + expect(data).toBeNull(); + }); +}); + +describe("Retrieve a checkout", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getCheckout(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the checkout with the given checkout id", async () => { + const { error, data: _data, statusCode } = await getCheckout(newCheckoutId); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(newCheckoutId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + } = attributes; + const items = [ + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(variant_id).toEqual(Number(variantId)); + expect(url).toMatch(id); + console.log("url", url); + + // product_options + const { + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + } = product_options; + const productOptionItems = [ + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + ]; + for (const item of productOptionItems) expect(item).toBeDefined(); + expect(Object.keys(product_options).length).toEqual( + productOptionItems.length + ); + + // checkout_options + const { + embed, + media: checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + } = checkout_options; + const checkoutOptionItems = [ + embed, + checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + ]; + for (const item of checkoutOptionItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_options).length).toEqual( + checkoutOptionItems.length + ); + + // checkout_data + const { + email, + name: checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + } = checkout_data; + const checkoutDataItems = [ + email, + checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + ]; + for (const item of checkoutDataItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_data).length).toEqual(checkoutDataItems.length); + + // preview + if (preview) { + const { + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + } = preview; + const previewItems = [ + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + ]; + for (const item of previewItems) expect(item).toBeDefined(); + expect(Object.keys(preview).length).toEqual(previewItems.length); + } else { + } + + const { variant, store } = relationships; + expect(variant.links).toBeDefined(); + expect(store.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${newCheckoutId}`); + }); + + it("Should return the checkout with the given checkout id and related resources", async () => { + const { + error, + data: _data, + statusCode, + } = await getCheckout(newCheckoutId, { include: ["store"] }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "stores")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(newCheckoutId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + } = attributes; + const items = [ + store_id, + variant_id, + custom_price, + product_options, + checkout_options, + checkout_data, + preview, + expires_at, + created_at, + updated_at, + test_mode, + url, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(variant_id).toEqual(Number(variantId)); + expect(url).toMatch(id); + console.log("url", url); + + // product_options + const { + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + } = product_options; + const productOptionItems = [ + name, + description, + media, + redirect_url, + receipt_button_text, + receipt_link_url, + receipt_thank_you_note, + enabled_variants, + confirmation_title, + confirmation_message, + confirmation_button_text, + ]; + for (const item of productOptionItems) expect(item).toBeDefined(); + expect(Object.keys(product_options).length).toEqual( + productOptionItems.length + ); + + // checkout_options + const { + embed, + media: checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + } = checkout_options; + const checkoutOptionItems = [ + embed, + checkoutOptionMedia, + logo, + desc, + discount, + quantity, + dark, + subscription_preview, + button_color, + ]; + for (const item of checkoutOptionItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_options).length).toEqual( + checkoutOptionItems.length + ); + + // checkout_data + const { + email, + name: checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + } = checkout_data; + const checkoutDataItems = [ + email, + checkoutDataName, + billing_address, + tax_number, + discount_code, + custom, + variant_quantities, + ]; + for (const item of checkoutDataItems) expect(item).toBeDefined(); + expect(Object.keys(checkout_data).length).toEqual(checkoutDataItems.length); + + // preview + if (preview) { + const { + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + } = preview; + const previewItems = [ + currency, + currency_rate, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + ]; + for (const item of previewItems) expect(item).toBeDefined(); + expect(Object.keys(preview).length).toEqual(previewItems.length); + } else { + } + + const { variant, store } = relationships; + expect(variant.links).toBeDefined(); + expect(store.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${newCheckoutId}`); + }); +}); + +describe("List all checkouts", () => { + it("Should return a paginated list of checkouts", async () => { + const { error, data: _data, statusCode } = await listCheckouts(); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of checkouts with related resources", async () => { + const { + error, + data: _data, + statusCode, + } = await listCheckouts({ include: ["store"] }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "stores")).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of checkouts filtered by store id", async () => { + const { + error, + data: _data, + statusCode, + } = await listCheckouts({ + filter: { storeId }, + }); + + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of checkouts filtered by variant id", async () => { + const { + error, + data: _data, + statusCode, + } = await listCheckouts({ + filter: { variantId }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect( + data.filter((item) => item.attributes.variant_id === Number(variantId)) + .length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of checkouts with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listCheckouts({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); diff --git a/test/customers/index.test.ts b/test/customers/index.test.ts new file mode 100644 index 0000000..3b3f5a3 --- /dev/null +++ b/test/customers/index.test.ts @@ -0,0 +1,689 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + archiveCustomer, + createCustomer, + getCustomer, + lemonSqueezySetup, + listCustomers, + updateCustomer, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const _getRandomString = (length: number) => + [...Array(length)].map(() => Math.random().toString(36).charAt(2)).join(""); + +const DATA_TYPE = "customers"; +const storeId = import.meta.env.LEMON_SQUEEZY_STORE_ID; +let newCustomerId: number | string; +let newCustomerEmail: string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("Create a customer", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await createCustomer("", { name: "", email: "" }); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("A new customer should be created with the given store id", async () => { + const name = _getRandomString(6); + const email = `${name}@example.com`; + + const { + error, + data: _data, + statusCode, + } = await createCustomer(storeId, { + name, + email, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(201); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(newName).toEqual(name); + expect(newEmail).toEqual(email); + expect(status).toEqual("subscribed"); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + + newCustomerId = Number(id); + newCustomerEmail = email; + }); +}); + +describe("Update a customer", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await updateCustomer("", { name: "", email: "" }); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return new name and new email with the given customerId", async () => { + const name = _getRandomString(6); + const email = `${name}@example.com`; + const { + error, + data: _data, + statusCode, + } = await updateCustomer(newCustomerId, { + name, + email, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(newCustomerId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(newName).toEqual(name); + expect(newEmail).toEqual(email); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + }); + + it("Should return new city, new region and new country with the given customer id", async () => { + const { + error, + data: _data, + statusCode, + } = await updateCustomer(newCustomerId, { + city: "Piggotts", + region: "SG", + country: "AG", + }); + + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(newCustomerId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(city).toEqual("Piggotts"); + expect(region).toEqual("SG"); + expect(country).toEqual("AG"); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + }); + + it("Should return the status of `archived` with the given customer id", async () => { + const { + error, + data: _data, + statusCode, + } = await archiveCustomer(newCustomerId); + + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(newCustomerId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(status).toEqual("archived"); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + }); + + it("An error and a statusCode of 422 should be returned", async () => { + const { + error, + data: _data, + statusCode, + } = await updateCustomer(newCustomerId, { + status: "unsubscribed" as any, + }); + + expect(error).toBeDefined(); + expect(error?.message).toMatch("Unprocessable Entity"); + expect(error?.cause).toBeArray(); + expect(statusCode).toEqual(422); + expect(_data).toBeNull(); + }); +}); + +describe("Retrieve a customer", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getCustomer(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the customer with the given customer id", async () => { + const { error, data: _data, statusCode } = await getCustomer(newCustomerId); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, attributes, id, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(newCustomerId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(status).toEqual("archived"); + expect(urls.customer_portal).toBeNull(); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + }); + + it("Should return the customer with the given customer id and include", async () => { + const { + error, + data: _data, + statusCode, + } = await getCustomer(newCustomerId, { include: ["store"] }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.find((item) => item.type === "stores")).toBeTrue(); + + const { type, attributes, id, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(newCustomerId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + // attributes + const { + store_id, + name: newName, + email: newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + newName, + newEmail, + status, + city, + region, + country, + total_revenue_currency, + mrr, + status_formatted, + country_formatted, + total_revenue_currency_formatted, + mrr_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(status).toEqual("archived"); + expect(urls.customer_portal).toBeNull(); + + // relationships + const { + orders, + "license-keys": licenseKeys, + subscriptions, + store, + } = relationships; + const relationshipItems = [orders, licenseKeys, subscriptions, store]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/customers/${id}`); + }); +}); + +describe("List all customers", () => { + it("Should return a paginated list of customers", async () => { + const { statusCode, error, data: _data } = await listCustomers(); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of customers filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listCustomers({ + filter: { storeId }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of customers filtered by email", async () => { + const { + statusCode, + error, + data: _data, + } = await listCustomers({ + filter: { email: newCustomerEmail }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.email === newCustomerEmail).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of customers with include", async () => { + const { + statusCode, + error, + data: _data, + } = await listCustomers({ + include: ["store"], + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.find((item) => item.type === "stores")).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of customers with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listCustomers({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); diff --git a/test/discountRedemptions/index.test.ts b/test/discountRedemptions/index.test.ts new file mode 100644 index 0000000..4282e4b --- /dev/null +++ b/test/discountRedemptions/index.test.ts @@ -0,0 +1,271 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + getDiscountRedemption, + lemonSqueezySetup, + listDiscountRedemptions, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "discount-redemptions"; +const PATH = "/v1/discount-redemptions/"; +let discountRedemptionId: number | string; +let discountId: number | string; +let orderId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all discount redemptions", () => { + it("Should return a paginated list of discount redemptions", async () => { + const { statusCode, error, data: _data } = await listDiscountRedemptions(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { id, attributes } = data[0]; + const { discount_id, order_id } = attributes; + discountRedemptionId = id; + discountId = discount_id; + orderId = order_id; + }); + + it("Should return a paginated list of discount redemptions with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listDiscountRedemptions({ include: ["order"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "orders")).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discount redemptions filtered by discount id", async () => { + const { + statusCode, + error, + data: _data, + } = await listDiscountRedemptions({ + filter: { discountId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect( + data.filter((item) => item.attributes.discount_id === Number(discountId)) + .length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discount redemptions filtered by order id", async () => { + const { + statusCode, + error, + data: _data, + } = await listDiscountRedemptions({ + filter: { orderId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect( + data.filter((item) => item.attributes.order_id === Number(orderId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discount redemptions with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listDiscountRedemptions({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a discount redemption", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getDiscountRedemption(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a discount redemption object", async () => { + const { + statusCode, + error, + data: _data, + } = await getDiscountRedemption(discountRedemptionId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${discountRedemptionId}`); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(discountRedemptionId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + discount_id, + order_id, + discount_name, + discount_code, + discount_amount, + discount_amount_type, + amount, + created_at, + updated_at, + } = attributes; + const items = [ + discount_id, + order_id, + discount_name, + discount_code, + discount_amount, + discount_amount_type, + amount, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(discount_id).toEqual(Number(discountId)); + expect(order_id).toEqual(Number(orderId)); + + const { discount, order } = relationships; + const relationshipItems = [discount, order]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); + + it("Should return a discount redemption object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getDiscountRedemption(discountRedemptionId, { + include: ["order"], + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${discountRedemptionId}`); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "orders")).toBeTrue(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(discountRedemptionId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + discount_id, + order_id, + discount_name, + discount_code, + discount_amount, + discount_amount_type, + amount, + created_at, + updated_at, + } = attributes; + const items = [ + discount_id, + order_id, + discount_name, + discount_code, + discount_amount, + discount_amount_type, + amount, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(discount_id).toEqual(Number(discountId)); + expect(order_id).toEqual(Number(orderId)); + + const { discount, order } = relationships; + const relationshipItems = [discount, order]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); +}); diff --git a/test/discounts/index.test.ts b/test/discounts/index.test.ts new file mode 100644 index 0000000..bd25168 --- /dev/null +++ b/test/discounts/index.test.ts @@ -0,0 +1,471 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + createDiscount, + deleteDiscount, + getDiscount, + lemonSqueezySetup, + listDiscounts, + listVariants, +} from "../../src"; +import type { NewDiscount } from "../../src/discounts/types"; +import { API_BASE_URL, generateDiscount } from "../../src/internal"; + +const DATA_TYPE = "discounts"; +const PATH = "/v1/discounts/"; +const storeId = import.meta.env.LEMON_SQUEEZY_STORE_ID!; +let discountId: number | string; +let variantIds: Array; + +beforeAll(async () => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); + const { data } = await listVariants(); + variantIds = data?.data.map((item) => item.id) ?? []; +}); + +describe("Create a discount", () => { + it("Should return a new discount object", async () => { + const discountInfo: NewDiscount = { + name: generateDiscount(), + amount: 50, + amountType: "percent", + storeId, + variantIds, + }; + const { + statusCode, + error, + data: _data, + } = await createDiscount(discountInfo); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(name).toEqual(discountInfo.name); + expect(amount).toEqual(discountInfo.amount); + expect(amount_type).toEqual(discountInfo.amountType); + + const { + store, + variants, + "discount-redemptions": discountRedemptions, + } = relationships; + const relationshipItems = [store, variants, discountRedemptions]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${id}`); + + discountId = id; + }); + + it("Should return a new discount object using the given code", async () => { + const discountInfo: NewDiscount = { + name: generateDiscount(), + code: generateDiscount(), + amount: 20, + amountType: "percent", + storeId, + variantIds, + }; + const { + statusCode, + error, + data: _data, + } = await createDiscount(discountInfo); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(name).toEqual(discountInfo.name); + expect(code).toEqual(discountInfo.code!); + expect(amount).toEqual(discountInfo.amount); + expect(amount_type).toEqual(discountInfo.amountType); + + const { + store, + variants, + "discount-redemptions": discountRedemptions, + } = relationships; + const relationshipItems = [store, variants, discountRedemptions]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${id}`); + }); +}); + +describe("Retrieve a discount", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getDiscount(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a discount object", async () => { + const { statusCode, error, data: _data } = await getDiscount(discountId); + + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(id).toEqual(discountId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + + const { + store, + variants, + "discount-redemptions": discountRedemptions, + } = relationships; + const relationshipItems = [store, variants, discountRedemptions]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${discountId}`); + }); + + it("Should return a discount object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getDiscount(discountId, { include: ["variants"] }); + + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")); + + const { type, id, attributes, relationships } = data; + expect(id).toEqual(discountId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + code, + amount, + amount_type, + is_limited_to_products, + is_limited_redemptions, + max_redemptions, + starts_at, + expires_at, + duration, + duration_in_months, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + + const { + store, + variants, + "discount-redemptions": discountRedemptions, + } = relationships; + const relationshipItems = [store, variants, discountRedemptions]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${discountId}`); + }); +}); + +describe("List all discounts", () => { + it("Should return a paginated list of discounts", async () => { + const { statusCode, error, data: _data } = await listDiscounts(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discounts with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listDiscounts({ include: ["variants"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discounts filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listDiscounts({ filter: { storeId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of discounts with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listDiscounts({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Delete a discount", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await deleteDiscount(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a `204 No Content` response on success", async () => { + const { statusCode, error, data } = await deleteDiscount(discountId); + expect(statusCode).toEqual(204); + expect(error).toBeNull(); + expect(data).toBeNull(); + }); +}); diff --git a/test/files/index.test.ts b/test/files/index.test.ts new file mode 100644 index 0000000..68017c6 --- /dev/null +++ b/test/files/index.test.ts @@ -0,0 +1,252 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getFile, lemonSqueezySetup, listFiles } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "files"; +const PATH = "/v1/files/"; +let fileId: any; +let variantId: any; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all files", () => { + it("Should return a paginated list of files", async () => { + const { statusCode, error, data: _data } = await listFiles(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + + fileId = data[0].id; + variantId = data[0].attributes.variant_id; + }); + + it("Should return a paginated list of files with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listFiles({ include: ["variant"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")).toBeTrue(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + }); + + it("Should return a paginated list of files filtered by variant id", async () => { + const { + statusCode, + error, + data: _data, + } = await listFiles({ + filter: { + variantId, + }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.variant_id === Number(variantId)) + .length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + }); + + it("Should return a paginated list of files with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listFiles({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a file", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getFile(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Retrieves the file with the given id", async () => { + const { statusCode, error, data: _data } = await getFile(fileId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${fileId}`); + expect(data).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(fileId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + variant_id, + identifier, + name, + extension, + download_url, + size, + size_formatted, + version, + sort, + status, + createdAt, + updatedAt, + test_mode, + } = attributes; + const items = [ + variant_id, + identifier, + name, + extension, + download_url, + size, + size_formatted, + version, + sort, + status, + createdAt, + updatedAt, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(variant_id).toEqual(Number(variantId)); + + const { variant } = relationships; + expect(variant.links).toBeDefined(); + }); + + it("Retrieves the file with the given id and related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getFile(fileId, { include: ["variant"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${fileId}`); + expect(data).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(fileId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + variant_id, + identifier, + name, + extension, + download_url, + size, + size_formatted, + version, + sort, + status, + createdAt, + updatedAt, + test_mode, + } = attributes; + const items = [ + variant_id, + identifier, + name, + extension, + download_url, + size, + size_formatted, + version, + sort, + status, + createdAt, + updatedAt, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(variant_id).toEqual(Number(variantId)); + + const { variant } = relationships; + expect(variant.links).toBeDefined(); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..b596cda --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from "bun:test"; +import * as exports from "../src"; + +describe("Export", () => { + it("Should return all exported", () => { + const shouldBeExports = [ + // LemonSqueezy deprecated class + "LemonSqueezy", + + // Setup + "lemonSqueezySetup", + + // Users + "getAuthenticatedUser", + + // Stores + "getStoreById", + "getAllStores", + + // Customers + "listCustomers", + "getCustomer", + "createCustomer", + "archiveCustomer", + "updateCustomer", + + // Products + "getProduct", + "listProducts", + + // Variants + "getVariant", + "listVariants", + + // Prices + "getPrice", + "listPrices", + + // Files + "getFile", + "listFiles", + + // Orders + "getOrder", + "listOrders", + + // Order Items + "getOrderItem", + "listOrderItems", + + // Subscriptions + "getSubscription", + "listSubscriptions", + "updateSubscription", + "cancelSubscription", + + // Subscriptions Invoices + "getSubscriptionInvoice", + "listSubscriptionInvoices", + + // Subscriptions Items + "getSubscriptionItem", + "listSubscriptionItems", + "getSubscriptionItemCurrentUsage", + "updateSubscriptionItem", + + // Usage Records + "listUsageRecords", + "getUsageRecord", + "createUsageRecord", + + // Discounts + "listDiscounts", + "getDiscount", + "createDiscount", + "deleteDiscount", + + // Discount Redemptions + "listDiscountRedemptions", + "getDiscountRedemption", + + // License keys + "listLicenseKeys", + "getLicenseKey", + "updateLicenseKey", + + // License Key Instances + "listLicenseKeyInstances", + "getLicenseKeyInstance", + + // Checkouts + "listCheckouts", + "getCheckout", + "createCheckout", + + // Webhooks + "listWebhooks", + "getWebhook", + "createWebhook", + "updateWebhook", + "deleteWebhook", + + // License + "activateLicense", + "validateLicense", + "deactivateLicense", + ]; + expect(Object.keys(exports).length).toBe(shouldBeExports.length); + }); +}); diff --git a/test/internal/configure.test.ts b/test/internal/configure.test.ts new file mode 100644 index 0000000..4002c35 --- /dev/null +++ b/test/internal/configure.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test"; +import { CONFIG_KEY, getKV, lemonSqueezySetup } from "../../src/internal"; + +describe("Lemon Squeezy Setup", () => { + const API_KEY = "0123456789"; + const onError = (error: Error) => { + console.log(error); + }; + + it("Configuration created successfully", () => { + const config = lemonSqueezySetup({ apiKey: API_KEY, onError }); + + expect(config).toEqual(getKV(CONFIG_KEY)); + expect(config.apiKey).toEqual(API_KEY); + expect(config.onError).toBeFunction(); + }); +}); diff --git a/test/internal/fetch.test.ts b/test/internal/fetch.test.ts new file mode 100644 index 0000000..605e06a --- /dev/null +++ b/test/internal/fetch.test.ts @@ -0,0 +1,97 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { lemonSqueezySetup } from "../../src"; +import { $fetch } from "../../src/internal"; + +const StoreId = import.meta.env.LEMON_SQUEEZY_STORE_ID; +beforeAll(() => { + lemonSqueezySetup({ + apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY, + }); +}); + +describe("$fetch test", () => { + it("Should call success", async () => { + const { error, data, statusCode } = await $fetch({ path: "/v1/users/me" }); + expect(statusCode).toEqual(200); + expect(data).toBeDefined(); + expect(error).toBeNull(); + }); + + it("Should return an error that the Lemon Squeezy API key was not provided", async () => { + lemonSqueezySetup({ apiKey: "" }); + const { error, data, statusCode } = await $fetch({ path: "/v1/user/me" }); + expect(data).toBeNull(); + expect(statusCode).toBeNull(); + expect(error?.message).toMatch("Lemon Squeezy API"); + }); + + it("The configured `onError` method should be executed", async () => { + lemonSqueezySetup({ + apiKey: "", + onError(error) { + expect(error.message).toMatch("Lemon Squeezy API"); + }, + }); + const { error, data, statusCode } = await $fetch({ path: "/v1/user/me" }); + expect(data).toBeNull(); + expect(statusCode).toBeNull(); + expect(error?.message).toMatch("Lemon Squeezy API"); + }); + + it("Should return a Lemon Squeezy API error", async () => { + lemonSqueezySetup({ apiKey: "0123456789" }); + const { error, data, statusCode } = await $fetch({ path: "/v1/user/me" }); + expect(data).toBeNull(); + expect(statusCode).toEqual(404); + expect(error).toBeDefined(); + expect(error?.cause).toBeArray(); + }); + + it("Should be called successfully with the query parameter", async () => { + lemonSqueezySetup({ + apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY, + }); + const { error, data, statusCode } = await $fetch({ + path: "/v1/products", + query: { + "filter[store_id]": StoreId, + }, + }); + + expect(data).toBeDefined(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + }); + + it("Should be called successfully with the body parameter", async () => { + const { + statusCode, + error, + data: _data, + } = await $fetch({ + path: "/v1/webhooks", + method: "POST", + body: { + data: { + type: "webhooks", + attributes: { + url: "https://google.com/webhooks", + events: ["subscription_created", "subscription_cancelled"], + secret: "SUBSCRIPTION_SECRET", + }, + relationships: { + store: { + data: { + type: "stores", + id: StoreId.toString(), + }, + }, + }, + }, + }, + }); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + }); +}); diff --git a/test/internal/utils.test.ts b/test/internal/utils.test.ts new file mode 100644 index 0000000..196b7b0 --- /dev/null +++ b/test/internal/utils.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "bun:test"; +import { camelToUnderscore, getKV, isObject, setKV } from "../../src/internal"; +import { type Config } from "../../src/internal/setup/types"; + +describe("Test isObject", () => { + it("String is not an object type", () => { + expect(isObject("test")).toBeFalse(); + }); + it("Number is not an object type", () => { + expect(isObject(1)).toBeFalse(); + }); + it("Boolean is not an object type", () => { + expect(isObject(true)).toBeFalse(); + }); + it("null is not an object type", () => { + expect(isObject(null)).toBeFalse(); + }); + it("undefined is not an object type", () => { + expect(isObject(undefined)).toBeFalse(); + }); + it("symbol is not an object type", () => { + expect(isObject(Symbol("test"))).toBeFalse(); + }); + it("Array is not an object type", () => { + expect(isObject([1])).toBeFalse(); + }); + it("Function is not an object type", () => { + expect(isObject(() => {})).toBeFalse(); + }); + it("Date is not an object type", () => { + expect(isObject(new Date())).toBeFalse(); + }); + it("RegExp is not an object type", () => { + expect(isObject(/(?:)/)).toBeFalse(); + }); + it("Map is not an object type", () => { + expect(isObject(new Map())).toBeFalse(); + }); + it("Set is not an object type", () => { + expect(isObject(new Set())).toBeFalse(); + }); + it("Object is an object type", () => { + expect(isObject(new Object())).toBeTrue(); + }); + it("Object is an object type", () => { + expect(isObject({})).toBeTrue(); + }); +}); + +describe("Test KV", () => { + const config = { apiKey: "0123456789" }; + const key = "Store"; + + it("Set value successfully", () => { + expect(getKV(key)).toBeUndefined(); + + setKV(key, config); + expect(getKV(key)).toEqual(config); + }); + + it("Get value successfully", () => { + const _config = getKV(key); + expect(_config).toEqual(config); + expect(_config.apiKey).toEqual(config.apiKey); + }); +}); + +describe("Test camelToUnderscore", () => { + it("Convert camel to underscore successfully", () => { + expect(camelToUnderscore("storeId")).toEqual("store_id"); + }); + + it("Convert no camel to no camel successfully", () => { + expect(camelToUnderscore("store")).toEqual("store"); + }); + + it("Convert camel to camel successfully", () => { + expect(camelToUnderscore("store_id")).toEqual("store_id"); + }); +}); + +// todo: other utils test diff --git a/test/license/index.test.ts b/test/license/index.test.ts new file mode 100644 index 0000000..c4fa050 --- /dev/null +++ b/test/license/index.test.ts @@ -0,0 +1,613 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + activateLicense, + deactivateLicense, + lemonSqueezySetup, + listLicenseKeys, + updateLicenseKey, + validateLicense, +} from "../../src"; + +const LicenseKey = import.meta.env.LEMON_SQUEEZY_LICENSE_KEY!; +let instanceId: string; +let licenseKeyId: string; + +beforeAll(async () => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); + const { data } = await listLicenseKeys(); + licenseKeyId = + data?.data.find((item) => item.attributes.key === LicenseKey)?.id ?? ""; +}); + +describe("Activate a license key", () => { + it("Should return a response", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await activateLicense(LicenseKey, "Test"); + expect(statusCode).toEqual(200); + expect(_error).toBeNull(); + expect(_data).toBeDefined(); + + const { activated, error, license_key, instance, meta } = _data!; + expect(activated).toBeTrue(); + expect(error).toBeNull(); + expect(license_key).toBeDefined(); + expect(instance).toBeDefined(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // instance + const instanceItems = [instance!.id, instance!.name, instance!.created_at]; + for (const item of instanceItems) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + + instanceId = instance!.id; + }); +}); + +describe("Failed to activate a license key", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await activateLicense("", ""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a response with 400 statusCode and a error when it fails", async () => { + // Before running, disable the license + await updateLicenseKey(licenseKeyId, { disabled: true }); + + const { + statusCode, + error: _error, + data: _data, + } = await activateLicense(LicenseKey, "Test"); + expect(statusCode).toEqual(400); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { activated, error, license_key, instance, meta } = _data!; + expect(activated).toBeFalse(); + expect(error).toBeDefined(); + expect(license_key).toBeDefined(); + expect(instance).toBeUndefined(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + + // After running, enable the license + await updateLicenseKey(licenseKeyId, { disabled: false }); + }); + + it("Should return a response with 404 statusCode and a error when it fails", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await activateLicense(`${LicenseKey}B`, "Test"); + + expect(statusCode).toEqual(404); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { activated, error } = _data!; + expect(activated).toBeFalse(); + expect(error).toBeDefined(); + }); +}); + +describe("Validate a license key", () => { + it("Should return a response that does not contain an instance", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await validateLicense(LicenseKey); + expect(statusCode).toEqual(200); + expect(_error).toBeNull(); + expect(_data).toBeDefined(); + + const { valid, error, license_key, meta } = _data!; + expect(valid).toBeTrue(); + expect(error).toBeNull(); + expect(license_key).toBeDefined(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + }); + + it("Should return a response containing the instance", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await validateLicense(LicenseKey, instanceId); + expect(statusCode).toEqual(200); + expect(_error).toBeNull(); + expect(_data).toBeDefined(); + + const { valid, error, license_key, meta, instance } = _data!; + expect(valid).toBeTrue(); + expect(error).toBeNull(); + expect(license_key).toBeDefined(); + expect(meta).toBeDefined(); + expect(instance).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // instance + const instanceItems = [instance!.id, instance!.name, instance!.created_at]; + for (const item of instanceItems) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + }); +}); + +describe("Failed to validate a license key", async () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await validateLicense(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a response with 400 statusCode and a error when it fails", async () => { + // Before running, disable the license + await updateLicenseKey(licenseKeyId, { disabled: true }); + + const { + statusCode, + error: _error, + data: _data, + } = await validateLicense(LicenseKey); + + expect(statusCode).toEqual(400); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { valid, error, license_key, instance, meta } = _data!; + expect(valid).toBeFalse(); + expect(error).toBeDefined(); + expect(license_key).toBeDefined(); + expect(instance).toBeNull(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + + // After running, enable the license + await updateLicenseKey(licenseKeyId, { disabled: false }); + }); + + it("Should return a response with 404 statusCode and a error when it fails", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await validateLicense(`${LicenseKey}B`); + + expect(statusCode).toEqual(404); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { valid, error } = _data!; + expect(valid).toBeFalse(); + expect(error).toBeDefined(); + }); +}); + +describe("Failed to deactivate a license key", async () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await deactivateLicense("", ""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a response with 400 statusCode and a error when it fails", async () => { + // Before running, disable the license + await updateLicenseKey(licenseKeyId, { disabled: true }); + + const { + statusCode, + error: _error, + data: _data, + } = await deactivateLicense(LicenseKey, instanceId); + + expect(statusCode).toEqual(400); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { deactivated, error, license_key, meta } = _data!; + expect(deactivated).toBeFalse(); + expect(error).toBeDefined(); + expect(license_key).toBeDefined(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + + // After running, enable the license + await updateLicenseKey(licenseKeyId, { disabled: false }); + }); + + it("Should return a response with 404 statusCode and a error when it fails", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await deactivateLicense(`${LicenseKey}B`, instanceId); + + expect(statusCode).toEqual(404); + expect(_error).toBeDefined(); + expect(_data).toBeDefined(); + + const { deactivated, error } = _data!; + expect(deactivated).toBeFalse(); + expect(error).toBeDefined(); + }); +}); + +describe("Deactivate a license key", () => { + it("Should return a response", async () => { + const { + statusCode, + error: _error, + data: _data, + } = await deactivateLicense(LicenseKey, instanceId); + expect(statusCode).toEqual(200); + expect(_error).toBeNull(); + expect(_data).toBeDefined(); + + const { deactivated, error, license_key, meta } = _data!; + expect(deactivated).toBeTrue(); + expect(error).toBeNull(); + expect(license_key).toBeDefined(); + expect(meta).toBeDefined(); + + // license_key + const { + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + } = license_key; + const items = [ + id, + status, + key, + activation_limit, + activation_usage, + created_at, + expires_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + + // meta + const { + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + } = meta; + const metaItems = [ + store_id, + order_id, + order_item_id, + variant_id, + variant_name, + product_id, + product_name, + customer_id, + customer_name, + customer_email, + ]; + for (const item of metaItems) expect(item).toBeDefined(); + }); +}); diff --git a/test/licenseKeyInstances/index.test.ts b/test/licenseKeyInstances/index.test.ts new file mode 100644 index 0000000..51a4cf5 --- /dev/null +++ b/test/licenseKeyInstances/index.test.ts @@ -0,0 +1,223 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + getLicenseKeyInstance, + lemonSqueezySetup, + listLicenseKeyInstances, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "license-key-instances"; +const PATH = `/v1/${DATA_TYPE}/`; +let licenseKeyInstanceId: number | string; +let licenseKeyId: number | string; + +beforeAll(async () => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); + await fetch("https://api.lemonsqueezy.com/v1/licenses/activate", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + license_key: import.meta.env.LEMON_SQUEEZY_LICENSE_KEY, + instance_name: "Test", + }), + }); +}); + +describe("List all license key instances", () => { + it("Should return all license key instances", async () => { + const { statusCode, error, data: _data } = await listLicenseKeyInstances(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { id, attributes } = data[0]; + const { license_key_id } = attributes; + licenseKeyInstanceId = id; + licenseKeyId = license_key_id; + }); + + it("Should return all license key instances with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeyInstances({ include: ["license-key"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "license-keys") + ).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return license key instances filtered by license key id", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeyInstances({ filter: { licenseKeyId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter( + (item) => item.attributes.license_key_id === Number(licenseKeyId) + ) + ).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return license key instances with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listLicenseKeyInstances({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a license key instance", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getLicenseKeyInstance(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a license key instance", async () => { + const { + statusCode, + error, + data: _data, + } = await getLicenseKeyInstance(licenseKeyInstanceId); + + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyInstanceId}`); + + const { type, id, attributes, relationships } = data; + expect(id).toEqual(licenseKeyInstanceId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { license_key_id, identifier, name, created_at, updated_at } = + attributes; + const items = [license_key_id, identifier, name, created_at, updated_at]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(license_key_id).toEqual(Number(licenseKeyId)); + + const { "license-key": licenseKey } = relationships; + const relationshipItems = [licenseKey]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); + + it("Should return a license key instance with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getLicenseKeyInstance(licenseKeyInstanceId, { + include: ["license-key"], + }); + + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyInstanceId}`); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "license-keys") + ).toBeTrue(); + + const { type, id, attributes, relationships } = data; + expect(id).toEqual(licenseKeyInstanceId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { license_key_id, identifier, name, created_at, updated_at } = + attributes; + const items = [license_key_id, identifier, name, created_at, updated_at]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(license_key_id).toEqual(Number(licenseKeyId)); + + const { "license-key": licenseKey } = relationships; + const relationshipItems = [licenseKey]; + for (const item of relationshipItems) expect(item.links).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); +}); diff --git a/test/licenseKeys/index.test.ts b/test/licenseKeys/index.test.ts new file mode 100644 index 0000000..a9440ad --- /dev/null +++ b/test/licenseKeys/index.test.ts @@ -0,0 +1,627 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + getLicenseKey, + lemonSqueezySetup, + listLicenseKeys, + updateLicenseKey, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; +import type { LicenseKey } from "../../src/licenseKeys/types"; + +const DATA_TYPE = "license-keys"; +const PATH = `/v1/${DATA_TYPE}/`; +let licenseKeyId: number | string; +let storeId: number | string; +let orderId: number | string; +let orderItemId: number | string; +let productId: number | string; +let licenseKeyStatus: LicenseKey["data"]["attributes"]["status"]; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all license keys", () => { + it("Should return all license keys", async () => { + const { statusCode, error, data: _data } = await listLicenseKeys(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { id, attributes } = data[0]; + const { store_id, order_id, order_item_id, product_id, status } = + attributes; + licenseKeyId = id; + storeId = store_id; + orderId = order_id; + orderItemId = order_item_id; + productId = product_id; + licenseKeyStatus = status; + }); + + it("Should return all license keys with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ include: ["order"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "orders")).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { id, attributes } = data[0]; + const { store_id, order_id, order_item_id, product_id, status } = + attributes; + licenseKeyId = id; + storeId = store_id; + orderId = order_id; + orderItemId = order_item_id; + productId = product_id; + licenseKeyStatus = status; + }); + + it("Should return all license keys filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ + filter: { storeId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return all license keys filtered by order id", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ + filter: { orderId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.order_id === Number(orderId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return all license keys filtered by order item id", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ + filter: { orderItemId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter( + (item) => item.attributes.order_item_id === Number(orderItemId) + ).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return all license keys filtered by product id", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ + filter: { productId }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.product_id === Number(productId)) + .length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return all license keys filtered by status", async () => { + const { + statusCode, + error, + data: _data, + } = await listLicenseKeys({ filter: { status: licenseKeyStatus } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.status === licenseKeyStatus).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return all license keys with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listLicenseKeys({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a license key", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getLicenseKey(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a license key object", async () => { + const { + statusCode, + error, + data: _data, + } = await getLicenseKey(licenseKeyId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyId}`); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(licenseKeyId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + user_name, + user_email, + key, + key_short, + activation_limit, + instances_count, + disabled, + status, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(order_id).toEqual(Number(orderId)); + expect(order_item_id).toEqual(Number(orderItemId)); + expect(product_id).toEqual(Number(productId)); + expect(status).toEqual(licenseKeyStatus); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + "license-key-instances": licenseKeyInstances, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + licenseKeyInstances, + ]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); + + it("Should return a license key object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getLicenseKey(licenseKeyId, { include: ["order"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyId}`); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "orders")).toBeTrue(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(licenseKeyId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + user_name, + user_email, + key, + key_short, + activation_limit, + instances_count, + disabled, + status, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(order_id).toEqual(Number(orderId)); + expect(order_item_id).toEqual(Number(orderItemId)); + expect(product_id).toEqual(Number(productId)); + expect(status).toEqual(licenseKeyStatus); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + "license-key-instances": licenseKeyInstances, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + licenseKeyInstances, + ]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); +}); + +describe("Update a license key", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await updateLicenseKey("", {}); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a license key object with activation_limit is 5", async () => { + const newActivationLimit = 5; + const { + statusCode, + error, + data: _data, + } = await updateLicenseKey(licenseKeyId, { + activationLimit: newActivationLimit, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyId}`); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(licenseKeyId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(order_id).toEqual(Number(orderId)); + expect(order_item_id).toEqual(Number(orderItemId)); + expect(product_id).toEqual(Number(productId)); + expect(activation_limit).toEqual(newActivationLimit); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + "license-key-instances": licenseKeyInstances, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + licenseKeyInstances, + ]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); + + it("Should return a license key object with disabled is true", async () => { + const { + statusCode, + error, + data: _data, + } = await updateLicenseKey(licenseKeyId, { + disabled: true, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${licenseKeyId}`); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(licenseKeyId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + status, + key, + user_name, + user_email, + key_short, + activation_limit, + instances_count, + disabled, + status_formatted, + expires_at, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(order_id).toEqual(Number(orderId)); + expect(order_item_id).toEqual(Number(orderItemId)); + expect(product_id).toEqual(Number(productId)); + expect(disabled).toBeTrue(); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + "license-key-instances": licenseKeyInstances, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + licenseKeyInstances, + ]; + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); +}); diff --git a/test/orderItems/index.test.ts b/test/orderItems/index.test.ts new file mode 100644 index 0000000..a6ad48f --- /dev/null +++ b/test/orderItems/index.test.ts @@ -0,0 +1,290 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getOrderItem, lemonSqueezySetup, listOrderItems } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "order-items"; +const PATH = "/v1/order-items/"; +let orderItemId: any; +let orderId: any; +let productId: any; +let variantId: any; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all order items", () => { + it("Should return a paginated list of order items", async () => { + const { statusCode, error, data: _data } = await listOrderItems(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + orderItemId = data[0].id; + orderId = data[0].attributes.order_id; + productId = data[0].attributes.product_id; + variantId = data[0].attributes.variant_id; + }); + + it("Should return a paginated list of order items with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrderItems({ include: ["product"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "products")).toBeTrue(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + }); + + it("Should return a paginated list of order items filtered by order id", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrderItems({ filter: { orderId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.order_id === Number(orderId)).length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + }); + + it("Should return a paginated list of order items filtered by product id", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrderItems({ filter: { productId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.product_id === Number(productId)) + .length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + }); + + it("Should return a paginated list of order items filtered by variant id", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrderItems({ filter: { variantId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.variant_id === Number(variantId)) + .length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + }); + + it("Should return a paginated list of order items with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listOrderItems({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve an order item", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getOrderItem(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the order with the given order item id", async () => { + const { statusCode, error, data: _data } = await getOrderItem(orderItemId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${orderItemId}`); + expect(data).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(orderItemId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at, + updated_at, + test_mode, + ]; + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) { + expect(item).toBeDefined(); + } + + const { order, product, variant } = relationships; + for (const item of [order, product, variant]) { + expect(item.links).toBeDefined(); + } + }); + + it("Should return the order with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getOrderItem(orderItemId, { include: ["order"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${orderItemId}`); + expect(data).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "orders")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(orderItemId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at, + updated_at, + test_mode, + ]; + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) { + expect(item).toBeDefined(); + } + + const { order, product, variant } = relationships; + for (const item of [order, product, variant]) { + expect(item.links).toBeDefined(); + } + }); +}); diff --git a/test/orders/index.test.ts b/test/orders/index.test.ts new file mode 100644 index 0000000..4276e3a --- /dev/null +++ b/test/orders/index.test.ts @@ -0,0 +1,498 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getOrder, lemonSqueezySetup, listOrders } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "orders"; +const PATH = "/v1/orders/"; +let storeId: any; +let orderId: any; +let userEmail: any; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all orders", () => { + it("Should return a paginated list of orders", async () => { + const { statusCode, error, data: _data } = await listOrders(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = data[0].relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + + orderId = data[0].id; + storeId = data[0].attributes.store_id; + userEmail = data[0].attributes.user_email; + }); + + it("Should return a paginated list of orders with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrders({ include: ["customer"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "customers")).toBeTrue(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = data[0].relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + }); + + it("Should return a paginated list of orders filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrders({ filter: { storeId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = data[0].relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + }); + + it("Should return a paginated list of orders filtered by user email", async () => { + const { + statusCode, + error, + data: _data, + } = await listOrders({ filter: { userEmail } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.user_email === userEmail).length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = data[0].relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + }); + + it("Should return a paginated list of orders with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listOrders({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve an order", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getOrder(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the order with the given order id", async () => { + const { statusCode, error, data: _data } = await getOrder(orderId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${orderId}`); + expect(data).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(orderId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + identifier, + order_number, + user_name, + user_email, + currency, + currency_rate, + subtotal, + discount_total, + tax, + subtotal_usd, + tax_usd, + total_usd, + tax_name, + tax_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + first_order_item, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + expect(store_id).toEqual(Number(storeId)); + expect(urls.receipt).toBeDefined(); + for (const item of [ + customer_id, + identifier, + order_number, + user_name, + user_email, + currency, + currency_rate, + subtotal, + discount_total, + tax, + subtotal_usd, + tax_usd, + total_usd, + tax_name, + tax_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + first_order_item, + urls, + created_at, + updated_at, + test_mode, + ]) { + expect(item).toBeDefined(); + } + + const { + id: firstOrderItemId, + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at: createdAt, + updated_at: updatedAt, + test_mode: testMode, + } = first_order_item; + for (const item of [ + firstOrderItemId, + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + createdAt, + updatedAt, + testMode, + ]) { + expect(item).toBeDefined(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + }); + + it("Should return the order with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getOrder(orderId, { include: ["customer"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${orderId}`); + expect(data).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "customers")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(orderId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + identifier, + order_number, + user_name, + user_email, + currency, + currency_rate, + subtotal, + discount_total, + tax, + subtotal_usd, + tax_usd, + total_usd, + tax_name, + tax_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + first_order_item, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + expect(store_id).toEqual(Number(storeId)); + expect(urls.receipt).toBeDefined(); + for (const item of [ + customer_id, + identifier, + order_number, + user_name, + user_email, + currency, + currency_rate, + subtotal, + discount_total, + tax, + subtotal_usd, + tax_usd, + total_usd, + tax_name, + tax_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + first_order_item, + urls, + created_at, + updated_at, + test_mode, + ]) { + expect(item).toBeDefined(); + } + + const { + id: firstOrderItemId, + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + created_at: createdAt, + updated_at: updatedAt, + test_mode: testMode, + } = first_order_item; + for (const item of [ + firstOrderItemId, + order_id, + product_id, + variant_id, + price_id, + product_name, + variant_name, + price, + quantity, + createdAt, + updatedAt, + testMode, + ]) { + expect(item).toBeDefined(); + } + + const { + store, + customer, + "order-items": orderItems, + subscriptions, + "license-keys": licenseKeys, + "discount-redemptions": discountRedemptions, + } = relationships; + for (const item of [ + store, + customer, + orderItems, + subscriptions, + licenseKeys, + discountRedemptions, + ]) { + expect(item.links).toBeDefined(); + } + }); +}); diff --git a/test/prices/index.test.ts b/test/prices/index.test.ts new file mode 100644 index 0000000..d0d7686 --- /dev/null +++ b/test/prices/index.test.ts @@ -0,0 +1,272 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getPrice, lemonSqueezySetup, listPrices } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "prices"; +const PATH = "/v1/prices/"; +let priceId: number | string; +let variantId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all prices", () => { + it("Should return a paginated list of prices", async () => { + const { statusCode, error, data: _data } = await listPrices(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + + priceId = data[0].id; + variantId = data[0].attributes.variant_id; + }); + + it("Should return a paginated list of prices with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listPrices({ include: ["variant"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")).toBeTrue(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + }); + + it("Should return a paginated list of prices filtered by variant id", async () => { + const { + statusCode, + error, + data: _data, + } = await listPrices({ filter: { variantId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.variant_id === Number(variantId)) + .length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + const { variant } = data[0].relationships; + expect(variant.links).toBeDefined(); + }); + + it("Should return a paginated list of prices with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listPrices({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a price", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getPrice(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the price with the given price id", async () => { + const { statusCode, error, data: _data } = await getPrice(priceId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${priceId}`); + expect(data).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(priceId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + variant_id, + category, + scheme, + usage_aggregation, + unit_price, + unit_price_decimal, + setup_fee_enabled, + setup_fee, + package_size, + tiers, + renewal_interval_quantity, + renewal_interval_unit, + trial_interval_unit, + trial_interval_quantity, + min_price, + suggested_price, + tax_code, + created_at, + updated_at, + } = attributes; + const items = [ + variant_id, + category, + scheme, + usage_aggregation, + unit_price, + unit_price_decimal, + setup_fee_enabled, + setup_fee, + package_size, + tiers, + renewal_interval_quantity, + renewal_interval_unit, + trial_interval_unit, + trial_interval_quantity, + min_price, + suggested_price, + tax_code, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(variant_id).toEqual(Number(variantId)); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { variant } = relationships; + expect(variant.links).toBeDefined(); + }); + + it("Should return a price object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getPrice(priceId, { include: ["variant"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${priceId}`); + expect(data).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "variants")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(priceId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + variant_id, + category, + scheme, + usage_aggregation, + unit_price, + unit_price_decimal, + setup_fee_enabled, + setup_fee, + package_size, + tiers, + renewal_interval_quantity, + renewal_interval_unit, + trial_interval_unit, + trial_interval_quantity, + min_price, + suggested_price, + tax_code, + created_at, + updated_at, + } = attributes; + const items = [ + variant_id, + category, + scheme, + usage_aggregation, + unit_price, + unit_price_decimal, + setup_fee_enabled, + setup_fee, + package_size, + tiers, + renewal_interval_quantity, + renewal_interval_unit, + trial_interval_unit, + trial_interval_quantity, + min_price, + suggested_price, + tax_code, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(variant_id).toEqual(Number(variantId)); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { variant } = relationships; + expect(variant.links).toBeDefined(); + }); +}); diff --git a/test/products/index.test.ts b/test/products/index.test.ts new file mode 100644 index 0000000..e759812 --- /dev/null +++ b/test/products/index.test.ts @@ -0,0 +1,262 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getProduct, lemonSqueezySetup, listProducts } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const STORE_ID = import.meta.env.LEMON_SQUEEZY_STORE_ID; +const DATA_TYPE = "products"; +let productId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all products", () => { + it("Should return a paginated list of products", async () => { + const { statusCode, error, data: _data } = await listProducts(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data.length).toBeGreaterThan(0); + expect(links).toBeDefined(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + for (const item of [currentPage, from, lastPage, perPage, to, total]) { + expect(item).toBeNumber(); + } + + const { first, last } = links; + expect(first).toBeString(); + expect(last).toBeString(); + + productId = data[0].id; + }); + + it("Should return a paginated list of products filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listProducts({ + filter: { storeId: STORE_ID }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(STORE_ID)) + .length + ).toEqual(data.length); + + const { first, last } = links; + expect(first).toBeString(); + expect(last).toBeString(); + }); + + it("Should return a paginated list of products with include", async () => { + const { + statusCode, + error, + data: _data, + } = await listProducts({ + include: ["variants"], + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.find((item) => item.type === "variants")).toBeTrue(); + + const { first, last } = links; + expect(first).toBeString(); + expect(last).toBeString(); + }); + + it("Should return a paginated list of products with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listProducts({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a product", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getProduct(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a product object with the given product id", async () => { + const { statusCode, error, data: _data } = await getProduct(productId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${productId}`); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(productId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined; + expect(relationships).toBeDefined; + + const { + store_id, + name, + slug, + description, + status, + status_formatted, + thumb_url, + large_thumb_url, + price, + price_formatted, + from_price, + to_price, + pay_what_you_want, + buy_now_url, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + slug, + description, + status, + status_formatted, + thumb_url, + large_thumb_url, + price, + price_formatted, + from_price, + to_price, + pay_what_you_want, + buy_now_url, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(store_id).toEqual(Number(STORE_ID)); + expect(status).toEqual("published"); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { store, variants } = relationships; + expect(store.links).toBeDefined(); + expect(variants.links).toBeDefined(); + }); + + it("Should return a product object and includes related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getProduct(productId, { include: ["variants"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${productId}`); + expect(included).toBeArray(); + expect(!!included?.find((item) => item.type === "variants")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(productId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined; + expect(relationships).toBeDefined; + + const { + store_id, + name, + slug, + description, + status, + status_formatted, + thumb_url, + large_thumb_url, + price, + price_formatted, + from_price, + to_price, + pay_what_you_want, + buy_now_url, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + name, + slug, + description, + status, + status_formatted, + thumb_url, + large_thumb_url, + price, + price_formatted, + from_price, + to_price, + pay_what_you_want, + buy_now_url, + created_at, + updated_at, + test_mode, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(store_id).toEqual(Number(STORE_ID)); + expect(status).toEqual("published"); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { store, variants } = relationships; + expect(store.links).toBeDefined(); + expect(variants.links).toBeDefined(); + }); +}); diff --git a/test/stores/index.test.ts b/test/stores/index.test.ts new file mode 100644 index 0000000..b65bf03 --- /dev/null +++ b/test/stores/index.test.ts @@ -0,0 +1,257 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getStore, lemonSqueezySetup, listStores } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "stores"; +let storeId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all stores", () => { + it("Should return a paginated list of stores", async () => { + const { error, data: _data, statusCode } = await listStores(); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, to, from, lastPage, perPage, total } = meta.page; + const items = [currentPage, to, from, lastPage, perPage, total]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + storeId = data[0].id; + }); + + it("Should return a paginated list of stores with page", async () => { + const page = { number: 1, size: 5 }; + const { + error, + data: _data, + statusCode, + } = await listStores({ + page, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, to, from, lastPage, perPage, total } = meta.page; + const items = [currentPage, to, from, lastPage, perPage, total]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(page.number); + expect(perPage).toEqual(page.size); + }); + + it("Should return a paginated list of stores with include", async () => { + const { + error, + data: _data, + statusCode, + } = await listStores({ + include: ["orders"], + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + included?.filter((item) => item.type === "orders").length + ).toBeGreaterThan(0); + + const { currentPage, to, from, lastPage, perPage, total } = meta.page; + const items = [currentPage, to, from, lastPage, perPage, total]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); +}); + +describe("Retrieve a store", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getStore(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the store object with the given store id", async () => { + const { error, data: _data, statusCode } = await getStore(storeId); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(storeId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + expect(links.self).toBe(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + + const { + name, + slug, + domain, + url, + avatar_url, + plan, + country, + country_nicename, + currency, + total_sales, + total_revenue, + thirty_day_sales, + thirty_day_revenue, + created_at, + updated_at, + } = attributes; + const items = [ + name, + slug, + domain, + url, + avatar_url, + plan, + country, + country_nicename, + currency, + total_sales, + total_revenue, + thirty_day_sales, + thirty_day_revenue, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { + products, + orders, + subscriptions, + discounts, + "license-keys": licenseKeys, + webhooks, + } = relationships; + const relationshipItems = [ + products, + orders, + subscriptions, + discounts, + licenseKeys, + webhooks, + ]; + + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); + + it("Should return the store object with the given store id and include", async () => { + const { + error, + data: _data, + statusCode, + } = await getStore(storeId, { include: ["orders"] }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect( + included?.filter((item) => item.type === "orders").length + ).toBeGreaterThan(0); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(storeId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + expect(links.self).toBe(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + + const { + name, + slug, + domain, + url, + avatar_url, + plan, + country, + country_nicename, + currency, + total_sales, + total_revenue, + thirty_day_sales, + thirty_day_revenue, + created_at, + updated_at, + } = attributes; + const items = [ + name, + slug, + domain, + url, + avatar_url, + plan, + country, + country_nicename, + currency, + total_sales, + total_revenue, + thirty_day_sales, + thirty_day_revenue, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + + const { + products, + orders, + subscriptions, + discounts, + "license-keys": licenseKeys, + webhooks, + } = relationships; + const relationshipItems = [ + products, + orders, + subscriptions, + discounts, + licenseKeys, + webhooks, + ]; + + for (const item of relationshipItems) expect(item).toBeDefined(); + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + }); +}); diff --git a/test/subscriptionInvoices/index.test.ts b/test/subscriptionInvoices/index.test.ts new file mode 100644 index 0000000..c45c1d9 --- /dev/null +++ b/test/subscriptionInvoices/index.test.ts @@ -0,0 +1,390 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + getSubscriptionInvoice, + lemonSqueezySetup, + listSubscriptionInvoices, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "subscription-invoices"; +const PATH = "/v1/subscription-invoices/"; +let subscriptionInvoiceId: string | number; +let storeId: string | number; +let invoiceStatus: any; +let invoiceRefunded: boolean; +let subscriptionId: string | number; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all subscription invoices", () => { + it("Should return a paginated list of subscription invoices", async () => { + const { statusCode, error, data: _data } = await listSubscriptionInvoices(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + + const { id, attributes } = data[0]; + const { store_id, status, refunded, subscription_id } = attributes; + subscriptionInvoiceId = id; + storeId = store_id; + invoiceStatus = status; + invoiceRefunded = refunded; + subscriptionId = subscription_id; + }); + + it("Should return a paginated list of subscription invoices with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionInvoices({ include: ["store"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "stores")).toBeTrue(); + }); + + it("Should return a paginated list of subscription invoices filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionInvoices({ filter: { storeId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription invoices filtered by invoice status", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionInvoices({ + filter: { status: invoiceStatus }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.status === invoiceStatus).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription invoices filtered by invoice refunded", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionInvoices({ + filter: { refunded: invoiceRefunded }, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.refunded === invoiceRefunded).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription invoices filtered by subscription id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionInvoices({ filter: { subscriptionId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter( + (item) => item.attributes.subscription_id === Number(subscriptionId) + ).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription invoices with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listSubscriptionInvoices({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a subscription invoice", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getSubscriptionInvoice(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the subscription invoice with the given subscription invoice id", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscriptionInvoice(subscriptionInvoiceId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual( + `${API_BASE_URL}${PATH}${subscriptionInvoiceId}` + ); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(subscriptionInvoiceId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + subscription_id, + customer_id, + user_name, + user_email, + billing_reason, + card_brand, + card_last_four, + currency, + currency_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + subscription_id, + customer_id, + user_name, + user_email, + billing_reason, + card_brand, + card_last_four, + currency, + currency_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + expect(subscription_id).toEqual(Number(subscriptionId)); + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + expect(urls.invoice_url).toBeDefined(); + + const { store, subscription, customer } = relationships; + const relationshipItems = [store, subscription, customer]; + expect(Object.keys(relationshipItems).length).toEqual( + relationshipItems.length + ); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); + + it("Should return the subscription invoice with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscriptionInvoice(subscriptionInvoiceId, { + include: ["subscription"], + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual( + `${API_BASE_URL}${PATH}${subscriptionInvoiceId}` + ); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "subscriptions") + ).toBeTrue(); + + const { type, id, attributes, relationships } = data; + expect(type).toEqual(DATA_TYPE); + expect(id).toEqual(subscriptionInvoiceId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + subscription_id, + customer_id, + user_name, + user_email, + billing_reason, + card_brand, + card_last_four, + currency, + currency_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + urls, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + subscription_id, + customer_id, + user_name, + user_email, + billing_reason, + card_brand, + card_last_four, + currency, + currency_rate, + status, + status_formatted, + refunded, + refunded_at, + subtotal, + discount_total, + tax, + total, + subtotal_usd, + discount_total_usd, + tax_usd, + total_usd, + subtotal_formatted, + discount_total_formatted, + tax_formatted, + total_formatted, + urls, + created_at, + updated_at, + test_mode, + ]; + expect(subscription_id).toEqual(Number(subscriptionId)); + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + expect(urls.invoice_url).toBeDefined(); + + const { store, subscription, customer } = relationships; + const relationshipItems = [store, subscription, customer]; + expect(Object.keys(relationshipItems).length).toEqual( + relationshipItems.length + ); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); +}); diff --git a/test/subscriptionItems/index.test.ts b/test/subscriptionItems/index.test.ts new file mode 100644 index 0000000..ca04e62 --- /dev/null +++ b/test/subscriptionItems/index.test.ts @@ -0,0 +1,363 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + getSubscriptionItem, + getSubscriptionItemCurrentUsage, + lemonSqueezySetup, + listSubscriptionItems, + updateSubscriptionItem, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "subscription-items"; +const PATH = "/v1/subscription-items/"; +let subscriptionItemId: number | string; +let noUsageBasedSubscriptionItemId: number | string; +let subscriptionId: number | string; +let priceId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all subscription items", () => { + it("Should return a paginated list of subscription items", async () => { + const { statusCode, error, data: _data } = await listSubscriptionItems(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + + const { id, attributes } = data.find( + (item) => item.attributes.is_usage_based + )!; + const { subscription_id, price_id } = attributes; + subscriptionItemId = id; + noUsageBasedSubscriptionItemId = data.find( + (item) => !item.attributes.is_usage_based + )!.id; + subscriptionId = subscription_id; + priceId = price_id; + }); + + it("Should return a paginated list of subscription items with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionItems({ include: ["subscription"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "subscriptions") + ).toBeTrue(); + }); + + it("Should return a paginated list of subscription items filtered by subscription id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionItems({ filter: { subscriptionId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.subscription_id === subscriptionId) + .length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription items filtered by price id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptionItems({ filter: { priceId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.price_id === priceId).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscription items with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listSubscriptionItems({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a subscription item", () => { + it("Should return the subscription item object", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscriptionItem(subscriptionItemId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionItemId}`); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(subscriptionItemId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + } = attributes; + const items = [ + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + ]; + expect(subscription_id).toEqual(Number(subscriptionId)); + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + + const { + subscription, + price, + "usage-records": usageRecords, + } = relationships; + const relationshipItems = [subscription, price, usageRecords]; + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); + + it("Should return the subscription item object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscriptionItem(subscriptionItemId, { + include: ["subscription"], + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionItemId}`); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "subscriptions") + ).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(subscriptionItemId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + } = attributes; + const items = [ + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + ]; + expect(subscription_id).toEqual(Number(subscriptionId)); + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + + const { + subscription, + price, + "usage-records": usageRecords, + } = relationships; + const relationshipItems = [subscription, price, usageRecords]; + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); +}); + +describe(`Retrieve a subscription item's current usage`, () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getSubscriptionItem(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a meta object containing usage information", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscriptionItemCurrentUsage(subscriptionItemId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta } = _data!; + expect(meta).toBeDefined(); + + const { + period_start, + period_end, + quantity, + interval_quantity, + interval_unit, + } = meta; + const items = [ + period_start, + period_end, + quantity, + interval_quantity, + interval_unit, + ]; + expect(Object.keys(meta).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + }); + + it("Should return a 404 Not Found response", async () => { + const { error, statusCode, data } = await getSubscriptionItemCurrentUsage( + noUsageBasedSubscriptionItemId + ); + expect(error).toBeDefined(); + expect(statusCode).toEqual(404); + expect(data).toBeNull(); + }); +}); + +describe("Update a subscription item", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await updateSubscriptionItem("", {} as any); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a updated subscription item object", async () => { + const { + error, + statusCode, + data: _data, + } = await updateSubscriptionItem(noUsageBasedSubscriptionItemId, 10); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual( + `${API_BASE_URL}${PATH}${noUsageBasedSubscriptionItemId}` + ); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(noUsageBasedSubscriptionItemId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + } = attributes; + const items = [ + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + ]; + expect(quantity).toEqual(10); + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + + const { + subscription, + price, + "usage-records": usageRecords, + } = relationships; + const relationshipItems = [subscription, price, usageRecords]; + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); +}); diff --git a/test/subscriptions/index.test.ts b/test/subscriptions/index.test.ts new file mode 100644 index 0000000..918c521 --- /dev/null +++ b/test/subscriptions/index.test.ts @@ -0,0 +1,664 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + cancelSubscription, + getSubscription, + lemonSqueezySetup, + listSubscriptions, + updateSubscription, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "subscriptions"; +const PATH = "/v1/subscriptions/"; +let subscriptionId: number | string; +let storeId: number | string; +let orderId: number | string; +let orderItemId: number | string; +let productId: number | string; +let variantId: number | string; +let userEmail: string; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all subscriptions", () => { + it("Should return a paginated list of subscriptions", async () => { + const { statusCode, error, data: _data } = await listSubscriptions(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + + const { id, attributes } = data[0]; + const { + store_id, + order_id, + order_item_id, + product_id, + variant_id, + user_email, + } = attributes; + subscriptionId = id; + storeId = store_id; + orderId = order_id; + orderItemId = order_item_id; + productId = product_id; + variantId = variant_id; + userEmail = user_email; + }); + + it("Should return a paginated list of subscriptions with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ include: ["product"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect(data).toBeArray(); + expect(data[0]).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "products")).toBeTrue(); + }); + + it("Should return a paginated list of subscriptions filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { storeId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions filtered by order id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { orderId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.order_id === Number(orderId)).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions filtered by product id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { productId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.product_id === Number(productId)) + .length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions filtered by order item id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { orderItemId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter( + (item) => item.attributes.order_item_id === Number(orderItemId) + ).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions filtered by variant id", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { variantId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.variant_id === Number(variantId)) + .length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions filtered by user email", async () => { + const { + statusCode, + error, + data: _data, + } = await listSubscriptions({ filter: { userEmail } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeString(); + expect(links.last).toBeString(); + expect( + data.filter((item) => item.attributes.user_email === userEmail).length + ).toEqual(data.length); + }); + + it("Should return a paginated list of subscriptions with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listSubscriptions({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a subscription", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getSubscription(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the subscription with the given subscription id", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscription(subscriptionId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes, relationships } = data; + expect(type).toBe(DATA_TYPE); + expect(id).toEqual(subscriptionId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + variant_id, + product_name, + variant_name, + user_name, + user_email, + status, + status_formatted, + card_brand, + card_last_four, + pause, + cancelled, + trial_ends_at, + billing_anchor, + first_subscription_item, + urls, + renews_at, + ends_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + variant_id, + product_name, + variant_name, + user_name, + user_email, + status, + status_formatted, + card_brand, + card_last_four, + pause, + cancelled, + trial_ends_at, + billing_anchor, + first_subscription_item, + urls, + renews_at, + ends_at, + created_at, + updated_at, + test_mode, + ]; + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + + if (first_subscription_item) { + const { + id, + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + } = first_subscription_item; + const firstItems = [ + id, + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + ]; + expect(Object.keys(first_subscription_item).length).toEqual( + firstItems.length + ); + for (const item of firstItems) expect(item).toBeDefined(); + } else expect(first_subscription_item).toBeNull(); + + expect(urls.update_payment_method).toBeString(); + expect(urls.customer_portal).toBeString(); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + variant, + "subscription-items": subscriptionItems, + "subscription-invoices": subscriptionInvoices, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + variant, + subscriptionItems, + subscriptionInvoices, + ]; + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); + + it("Should return the subscription with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getSubscription(subscriptionId, { include: ["product"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "products")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(type).toBe(DATA_TYPE); + expect(id).toEqual(subscriptionId.toString()); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + customer_id, + order_id, + order_item_id, + product_id, + variant_id, + product_name, + variant_name, + user_name, + user_email, + status, + status_formatted, + card_brand, + card_last_four, + pause, + cancelled, + trial_ends_at, + billing_anchor, + first_subscription_item, + urls, + renews_at, + ends_at, + created_at, + updated_at, + test_mode, + } = attributes; + const items = [ + store_id, + customer_id, + order_id, + order_item_id, + product_id, + variant_id, + product_name, + variant_name, + user_name, + user_email, + status, + status_formatted, + card_brand, + card_last_four, + pause, + cancelled, + trial_ends_at, + billing_anchor, + first_subscription_item, + urls, + renews_at, + ends_at, + created_at, + updated_at, + test_mode, + ]; + expect(Object.keys(attributes).length).toEqual(items.length); + for (const item of items) expect(item).toBeDefined(); + + if (first_subscription_item) { + const { + id, + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + } = first_subscription_item; + const firstItems = [ + id, + subscription_id, + price_id, + quantity, + is_usage_based, + created_at, + updated_at, + ]; + expect(Object.keys(first_subscription_item).length).toEqual( + firstItems.length + ); + for (const item of firstItems) expect(item).toBeDefined(); + } else expect(first_subscription_item).toBeNull(); + + expect(urls.update_payment_method).toBeString(); + expect(urls.customer_portal).toBeString(); + + const { + store, + customer, + order, + "order-item": orderItem, + product, + variant, + "subscription-items": subscriptionItems, + "subscription-invoices": subscriptionInvoices, + } = relationships; + const relationshipItems = [ + store, + customer, + order, + orderItem, + product, + variant, + subscriptionItems, + subscriptionInvoices, + ]; + expect(Object.keys(relationships).length).toEqual(relationshipItems.length); + for (const item of relationshipItems) expect(item.links).toBeDefined(); + }); +}); + +describe("Cancel a subscription", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await cancelSubscription(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("The current subscription should be successfully canceled", async () => { + const { + statusCode, + error, + data: _data, + } = await cancelSubscription(subscriptionId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes.status).toEqual("cancelled"); + }); +}); + +describe("Update a subscription", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await updateSubscription("", {}); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("The subscription should be successfully canceled", async () => { + const { + statusCode, + error, + data: _data, + } = await updateSubscription(subscriptionId, { + cancelled: true, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes).toBeDefined(); + + const { cancelled } = attributes; + expect(cancelled).toBeTrue(); + }); + + it("The subscription should be successfully active", async () => { + const { + statusCode, + error, + data: _data, + } = await updateSubscription(subscriptionId, { + cancelled: false, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes).toBeDefined(); + + const { cancelled } = attributes; + expect(cancelled).toBeFalse(); + }); + + it("The payment should be changed to pause", async () => { + const mode = "void"; + const billingAnchor = 25; + const { + statusCode, + error, + data: _data, + } = await updateSubscription(subscriptionId, { + pause: { + mode, + }, + billingAnchor, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes).toBeDefined(); + + const { pause, billing_anchor } = attributes; + expect(pause).toEqual({ mode, resumes_at: null }); + expect(billing_anchor).toEqual(billingAnchor); + }); + + it("Successful call with invoiceImmediately parameter", async () => { + const { + statusCode, + error, + data: _data, + } = await updateSubscription(subscriptionId, { + invoiceImmediately: true, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes).toBeDefined(); + }); + + it("Successful call with disableProrations parameter", async () => { + const { + statusCode, + error, + data: _data, + } = await updateSubscription(subscriptionId, { + disableProrations: true, + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${subscriptionId}`); + + const { id, type, attributes } = data; + expect(id).toEqual(subscriptionId.toString()); + expect(type).toBe(DATA_TYPE); + expect(attributes).toBeDefined(); + }); +}); diff --git a/test/usageRecords/index.test.ts b/test/usageRecords/index.test.ts new file mode 100644 index 0000000..922f6d5 --- /dev/null +++ b/test/usageRecords/index.test.ts @@ -0,0 +1,310 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + createUsageRecord, + getUsageRecord, + lemonSqueezySetup, + listSubscriptionItems, + listUsageRecords, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "usage-records"; +const PATH = "/v1/usage-records/"; +let usageRecordId: number | string; +let subscriptionItemId: number | string; + +beforeAll(async () => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); + const { data } = await listSubscriptionItems(); + subscriptionItemId = data!.data[0].id; +}); + +describe("Create a usage record", async () => { + it("Should return a usage record object using the subscription item id & quantity", async () => { + const newQuantity = 10; + const { + error, + statusCode, + data: _data, + } = await createUsageRecord({ + quantity: newQuantity, + subscriptionItemId, + }); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { subscription_item_id, quantity, action, created_at, updated_at } = + attributes; + const items = [ + subscription_item_id, + quantity, + action, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(subscription_item_id).toEqual(Number(subscriptionItemId)); + expect(quantity).toEqual(newQuantity); + expect(action).toEqual("increment"); + + const { "subscription-item": subscriptionItem } = relationships; + expect(subscriptionItem.links).toBeDefined(); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${id}`); + + usageRecordId = id; + }); + + it("Should return a usage record object using the subscription item id, quantity and action=set", async () => { + const newQuantity = 5; + const { + error, + statusCode, + data: _data, + } = await createUsageRecord({ + quantity: newQuantity, + action: "set", + subscriptionItemId, + }); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { type, id, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { subscription_item_id, quantity, action, created_at, updated_at } = + attributes; + const items = [ + subscription_item_id, + quantity, + action, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(subscription_item_id).toEqual(Number(subscriptionItemId)); + expect(quantity).toEqual(newQuantity); + expect(action).toEqual("set"); + + const { "subscription-item": subscriptionItem } = relationships; + expect(subscriptionItem.links).toBeDefined(); + + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${id}`); + }); +}); + +describe("List all usage records", () => { + it("Should return a paginated list of usage records", async () => { + const { error, statusCode, data: _data } = await listUsageRecords(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, lastPage, perPage, total } = meta.page; + const pageItems = [currentPage, from, to, lastPage, perPage, total]; + for (const item of pageItems) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(pageItems.length); + }); + + it("Should return a paginated list of usage records with related resources", async () => { + const { + error, + statusCode, + data: _data, + } = await listUsageRecords({ include: ["subscription-item"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "subscription-items") + ).toBeTrue(); + + const { currentPage, from, to, lastPage, perPage, total } = meta.page; + const pageItems = [currentPage, from, to, lastPage, perPage, total]; + for (const item of pageItems) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(pageItems.length); + }); + + it("Should return a paginated list of usage records filtered by subscription item id", async () => { + const { + error, + statusCode, + data: _data, + } = await listUsageRecords({ filter: { subscriptionItemId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter( + (item) => + item.attributes.subscription_item_id === Number(subscriptionItemId) + ).length + ).toEqual(data.length); + + const { currentPage, from, to, lastPage, perPage, total } = meta.page; + const pageItems = [currentPage, from, to, lastPage, perPage, total]; + for (const item of pageItems) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(pageItems.length); + }); + + it("Should return a paginated list of usage records with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listUsageRecords({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a usage record", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getUsageRecord(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a usage record object", async () => { + const { + error, + statusCode, + data: _data, + } = await getUsageRecord(usageRecordId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${usageRecordId}`); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(usageRecordId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { subscription_item_id, quantity, action, created_at, updated_at } = + attributes; + const items = [ + subscription_item_id, + quantity, + action, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(subscription_item_id).toEqual(Number(subscriptionItemId)); + expect(quantity).toBeInteger(); + expect(["increment", "set"].includes(action)).toBeTrue(); + + const { "subscription-item": subscriptionItem } = relationships; + expect(subscriptionItem.links).toBeDefined(); + }); + + it("Should return a usage record object with related resources", async () => { + const { + error, + statusCode, + data: _data, + } = await getUsageRecord(usageRecordId, { include: ["subscription-item"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${usageRecordId}`); + expect(included).toBeArray(); + expect( + !!included?.filter((item) => item.type === "subscription-items") + ).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(usageRecordId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { subscription_item_id, quantity, action, created_at, updated_at } = + attributes; + const items = [ + subscription_item_id, + quantity, + action, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(subscription_item_id).toEqual(Number(subscriptionItemId)); + expect(quantity).toBeInteger(); + expect(["increment", "set"].includes(action)).toBeTrue(); + + const { "subscription-item": subscriptionItem } = relationships; + expect(subscriptionItem.links).toBeDefined(); + }); +}); diff --git a/test/users/index.test.ts b/test/users/index.test.ts new file mode 100644 index 0000000..c00e12b --- /dev/null +++ b/test/users/index.test.ts @@ -0,0 +1,72 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getAuthenticatedUser, lemonSqueezySetup } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "users"; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("Retrieve the authenticated user", () => { + it("The currently authenticated user should be returned", async () => { + const { error, data: _data, statusCode } = await getAuthenticatedUser(); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + expect(statusCode).toEqual(200); + + const { meta, links, data } = _data!; + expect(meta).toBeDefined(); + expect(links).toBeDefined(); + expect(data).toBeDefined(); + + const { test_mode } = meta; + expect(test_mode).toBeDefined(); + + const { self } = links; + expect(self).toBe(`${API_BASE_URL}/v1/${DATA_TYPE}/${data.id}`); + + const { id, type, attributes } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + + const { + name, + email, + color, + avatar_url, + has_custom_avatar, + createdAt, + updatedAt, + } = attributes; + const items = [ + name, + email, + color, + avatar_url, + has_custom_avatar, + createdAt, + updatedAt, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + }); + + it("An error should be returned when no `apiKey` is provided", async () => { + lemonSqueezySetup({ apiKey: "" }); + const { error, data, statusCode } = await getAuthenticatedUser(); + expect(data).toBeNull(); + expect(statusCode).toBeNull(); + expect(error).toBeDefined(); + }); + + it("An error and a 401 statusCode should be returned when the `apiKey` is incorrect", async () => { + lemonSqueezySetup({ apiKey: "INCORRECT_API_KEY" }); + const { error, data, statusCode } = await getAuthenticatedUser(); + expect(data).toBeNull(); + expect(statusCode).toEqual(401); + expect(error).toBeDefined(); + expect(error?.cause).toBeArray(); + }); +}); diff --git a/test/variants/index.test.ts b/test/variants/index.test.ts new file mode 100644 index 0000000..648a721 --- /dev/null +++ b/test/variants/index.test.ts @@ -0,0 +1,333 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { getVariant, lemonSqueezySetup, listVariants } from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "variants"; +const PATH = "/v1/variants/"; +let variantId: number | string; +let productId: number | string; +let status: any; + +beforeAll(() => { + lemonSqueezySetup({ apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY }); +}); + +describe("List all variants", () => { + it("Should return a paginated list of variants", async () => { + const { statusCode, error, data: _data } = await listVariants(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + + variantId = data[0].id; + productId = data[0].attributes.product_id; + status = data[0].attributes.status; + }); + + it("Should return a paginated list of variants with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listVariants({ include: ["product"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data, included } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(data).toBeArray(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "products")).toBeTrue(); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of variants filtered by product id", async () => { + const { + statusCode, + error, + data: _data, + } = await listVariants({ filter: { productId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.product_id === Number(productId)) + .length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of orders filtered by status", async () => { + const { + statusCode, + error, + data: _data, + } = await listVariants({ filter: { status } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, links, data } = _data!; + expect(meta.page).toBeDefined(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect( + data.filter((item) => item.attributes.status === status).length + ).toEqual(data.length); + + const { currentPage, from, lastPage, perPage, to, total } = meta.page; + const items = [currentPage, from, lastPage, perPage, to, total]; + for (const item of items) expect(item).toBeNumber(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of orders with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listVariants({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Retrieve a variant", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getVariant(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return the variant with the given variant id", async () => { + const { statusCode, error, data: _data } = await getVariant(variantId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${variantId}`); + expect(data).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(variantId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + product_id, + name, + slug, + description, + has_license_keys, + license_activation_limit, + is_license_limit_unlimited, + license_length_value, + license_length_unit, + is_license_length_unlimited, + sort, + status, + status_formatted, + created_at, + updated_at, + test_mode, + // -> deprecated + price, + is_subscription, + interval, + interval_count, + has_free_trial, + trial_interval, + trial_interval_count, + pay_what_you_want, + min_price, + suggested_price, + // <- deprecated + } = attributes; + const items = [ + product_id, + name, + slug, + description, + has_license_keys, + license_activation_limit, + is_license_limit_unlimited, + license_length_value, + license_length_unit, + is_license_length_unlimited, + sort, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + const deprecatedItems = [ + price, + is_subscription, + interval, + interval_count, + has_free_trial, + trial_interval, + trial_interval_count, + pay_what_you_want, + min_price, + suggested_price, + ]; + for (const item of items) expect(item).toBeDefined(); + for (const item of deprecatedItems) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual( + items.length + deprecatedItems.length + ); + expect(product_id).toEqual(Number(productId)); + expect(status).toEqual("pending"); + + const { product } = relationships; + expect(product.links).toBeDefined(); + }); + + it("Should return the variant with the given variant id with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getVariant(variantId, { include: ["product"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { links, data, included } = _data!; + expect(links.self).toEqual(`${API_BASE_URL}${PATH}${variantId}`); + expect(data).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "products")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toEqual(variantId.toString()); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + product_id, + name, + slug, + description, + has_license_keys, + license_activation_limit, + is_license_limit_unlimited, + license_length_value, + license_length_unit, + is_license_length_unlimited, + sort, + status, + status_formatted, + created_at, + updated_at, + test_mode, + // -> deprecated + price, + is_subscription, + interval, + interval_count, + has_free_trial, + trial_interval, + trial_interval_count, + pay_what_you_want, + min_price, + suggested_price, + // <- deprecated + } = attributes; + const items = [ + product_id, + name, + slug, + description, + has_license_keys, + license_activation_limit, + is_license_limit_unlimited, + license_length_value, + license_length_unit, + is_license_length_unlimited, + sort, + status, + status_formatted, + created_at, + updated_at, + test_mode, + ]; + const deprecatedItems = [ + price, + is_subscription, + interval, + interval_count, + has_free_trial, + trial_interval, + trial_interval_count, + pay_what_you_want, + min_price, + suggested_price, + ]; + for (const item of items) expect(item).toBeDefined(); + for (const item of deprecatedItems) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual( + items.length + deprecatedItems.length + ); + expect(product_id).toEqual(Number(productId)); + expect(status).toEqual("pending"); + + const { product } = relationships; + expect(product.links).toBeDefined(); + }); +}); diff --git a/test/webhooks/index.test.ts b/test/webhooks/index.test.ts new file mode 100644 index 0000000..b6635df --- /dev/null +++ b/test/webhooks/index.test.ts @@ -0,0 +1,400 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + createWebhook, + deleteWebhook, + getWebhook, + lemonSqueezySetup, + listWebhooks, + updateWebhook, +} from "../../src"; +import { API_BASE_URL } from "../../src/internal"; + +const DATA_TYPE = "webhooks"; +const storeId = import.meta.env.LEMON_SQUEEZY_STORE_ID; +let webhookId: number | string; + +beforeAll(() => { + lemonSqueezySetup({ + apiKey: import.meta.env.LEMON_SQUEEZY_API_KEY, + }); +}); + +describe("Create a webhook", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await createWebhook("", {} as any); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a webhook object", async () => { + const { + statusCode, + error, + data: _data, + } = await createWebhook(storeId, { + url: "https://google.com/webhooks", + events: ["subscription_created", "subscription_cancelled"], + secret: "SUBSCRIPTION_SECRET", + }); + expect(statusCode).toEqual(201); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + } = attributes; + const items = [ + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(url).toEqual("https://google.com/webhooks"); + expect(events).toEqual(["subscription_created", "subscription_cancelled"]); + + const { store } = relationships; + expect(store.links).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + + webhookId = id; + }); +}); + +describe("Update a checkout", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await updateWebhook("", {}); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a webhook object", async () => { + const { + statusCode, + error, + data: _data, + } = await updateWebhook(webhookId, { + url: "https://google.com/webhooks2", + events: [ + "subscription_created", + "subscription_cancelled", + "subscription_paused", + ], + secret: "SUBSCRIPTION_SECRET_2", + }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + } = attributes; + const items = [ + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(url).toEqual("https://google.com/webhooks2"); + expect(events).toEqual([ + "subscription_created", + "subscription_cancelled", + "subscription_paused", + ]); + + const { store } = relationships; + expect(store).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + }); +}); + +describe("Retrieve a checkout", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await getWebhook(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a webhook object", async () => { + const { statusCode, error, data: _data } = await getWebhook(webhookId); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + } = attributes; + const items = [ + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(url).toEqual("https://google.com/webhooks2"); + expect(events).toEqual([ + "subscription_created", + "subscription_cancelled", + "subscription_paused", + ]); + + const { store } = relationships; + expect(store).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + }); + + it("Should return a webhook object with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await getWebhook(webhookId, { include: ["store"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { data, links, included } = _data!; + expect(data).toBeDefined(); + expect(links).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "stores")).toBeTrue(); + + const { id, type, attributes, relationships } = data; + expect(id).toBeDefined(); + expect(type).toEqual(DATA_TYPE); + expect(attributes).toBeDefined(); + expect(relationships).toBeDefined(); + + const { + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + } = attributes; + const items = [ + store_id, + url, + events, + last_sent_at, + test_mode, + created_at, + updated_at, + ]; + for (const item of items) expect(item).toBeDefined(); + expect(Object.keys(attributes).length).toEqual(items.length); + expect(store_id).toEqual(Number(storeId)); + expect(url).toEqual("https://google.com/webhooks2"); + expect(events).toEqual([ + "subscription_created", + "subscription_cancelled", + "subscription_paused", + ]); + + const { store } = relationships; + expect(store).toBeDefined(); + + const { self } = links; + expect(self).toEqual(`${API_BASE_URL}/v1/${DATA_TYPE}/${id}`); + }); +}); + +describe("List all webhooks", () => { + it("Should return a paginated list of webhooks", async () => { + const { statusCode, error, data: _data } = await listWebhooks(); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of webhooks with related resources", async () => { + const { + statusCode, + error, + data: _data, + } = await listWebhooks({ include: ["store"] }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links, included } = _data!; + expect(meta.page).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + expect(included).toBeArray(); + expect(!!included?.filter((item) => item.type === "stores")).toBeTrue(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of webhooks filtered by store id", async () => { + const { + statusCode, + error, + data: _data, + } = await listWebhooks({ filter: { storeId } }); + expect(statusCode).toEqual(200); + expect(error).toBeNull(); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta.page).toBeDefined(); + expect( + data.filter((item) => item.attributes.store_id === Number(storeId)).length + ).toEqual(data.length); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + }); + + it("Should return a paginated list of webhooks with page_number = 1 and page_size = 5", async () => { + const { + error, + data: _data, + statusCode, + } = await listWebhooks({ + page: { + number: 1, + size: 5, + }, + }); + expect(error).toBeNull(); + expect(statusCode).toEqual(200); + expect(_data).toBeDefined(); + + const { meta, data, links } = _data!; + expect(meta).toBeDefined(); + expect(data).toBeArray(); + expect(links.first).toBeDefined(); + expect(links.last).toBeDefined(); + + const { currentPage, from, to, perPage, lastPage, total } = meta.page; + const items = [currentPage, from, to, perPage, lastPage, total]; + for (const item of items) expect(item).toBeInteger(); + expect(Object.keys(meta.page).length).toEqual(items.length); + expect(currentPage).toEqual(1); + expect(perPage).toEqual(5); + }); +}); + +describe("Delete a checkout", () => { + it("Throw an error about a parameter that must be provided", async () => { + try { + await deleteWebhook(""); + } catch (error) { + expect((error as Error).message).toMatch( + "Please provide the required parameter:" + ); + } + }); + + it("Should return a 204 No Content response on success", async () => { + const { statusCode, data, error } = await deleteWebhook(webhookId); + expect(statusCode).toEqual(204); + expect(data).toBeNull(); + expect(error).toBeNull(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 214f2e2..c6b84e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,24 @@ { "compilerOptions": { + "lib": ["ESNext"], + "allowJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, "module": "ESNext", - "noUncheckedIndexedAccess": true, + "moduleDetection": "force", + "target": "ESNext", + + /* Bundler mode */ + "allowImportingTsExtensions": true, + "moduleResolution": "bundler", + "noEmit": true, + "verbatimModuleSyntax": true, + + /* Linting */ + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "skipLibCheck": true, "strict": true, - "target": "ESNext" }, - "include": ["./src/**/*.ts"] } diff --git a/tsup.config.ts b/tsup.config.ts index 304fe24..4467203 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,15 +1,10 @@ import { defineConfig } from "tsup"; -const isProduction = process.env.NODE_ENV === "production"; - -export default defineConfig(({ watch = false }) => ({ - entry: { - index: "./src/index.ts", - }, +export default defineConfig({ + entry: ["./src/index.ts"], clean: true, dts: true, format: ["cjs", "esm"], - minify: isProduction, + minify: true, outDir: "dist", - watch, -})); +});