Преглед изворни кода

fix(docs): Normalize code block language identifiers for Shiki

- Change GraphQL/GraphQl to graphql (case-sensitive)
- Change HTML to html
- Change JSON to json
- Change SQL to sql
- Change YAML to yaml
- Change sh/shell to bash (standard Shiki alias)
- Change po to text (unsupported language)

These changes ensure compatibility with Shiki syntax highlighter
which requires lowercase language identifiers.
David Höck пре 19 часа
родитељ
комит
d435b877d9
50 измењених фајлова са 6802 додато и 10 уклоњено
  1. 246 0
      docs/docs/guides/deployment/deploy-to-digital-ocean-app-platform/index.mdx
  2. 202 0
      docs/docs/guides/deployment/deploy-to-railway/index.mdx
  3. 221 0
      docs/docs/guides/deployment/deploy-to-render/index.mdx
  4. 2 2
      docs/docs/guides/deployment/horizontal-scaling.mdx
  5. 3 3
      docs/docs/guides/deployment/production-configuration/index.mdx
  6. 2 2
      docs/docs/guides/deployment/using-docker.mdx
  7. 137 0
      docs/docs/guides/developer-guide/dataloaders/index.mdx
  8. 264 0
      docs/docs/guides/developer-guide/security/index.mdx
  9. 2 2
      docs/docs/guides/developer-guide/stand-alone-scripts/index.mdx
  10. 1 1
      docs/docs/guides/developer-guide/testing/index.mdx
  11. 113 0
      docs/docs/guides/extending-the-admin-ui/adding-ui-translations/index.mdx
  12. 205 0
      docs/docs/guides/extending-the-admin-ui/using-other-frameworks/index.mdx
  13. 94 0
      docs/docs/guides/extending-the-dashboard/localization/index.mdx
  14. 231 0
      docs/docs/guides/getting-started/installation/index.mdx
  15. 63 0
      docs/docs/reference/admin-ui-api/components/chip-component.mdx
  16. 174 0
      docs/docs/reference/admin-ui-api/components/currency-input-component.mdx
  17. 238 0
      docs/docs/reference/admin-ui-api/components/data-table-component.mdx
  18. 262 0
      docs/docs/reference/admin-ui-api/components/datetime-picker-component.mdx
  19. 86 0
      docs/docs/reference/admin-ui-api/components/dropdown-component.mdx
  20. 155 0
      docs/docs/reference/admin-ui-api/components/facet-value-selector-component.mdx
  21. 86 0
      docs/docs/reference/admin-ui-api/components/object-tree-component.mdx
  22. 41 0
      docs/docs/reference/admin-ui-api/components/order-state-label-component.mdx
  23. 75 0
      docs/docs/reference/admin-ui-api/components/product-variant-selector-component.mdx
  24. 108 0
      docs/docs/reference/admin-ui-api/components/rich-text-editor-component.mdx
  25. 41 0
      docs/docs/reference/admin-ui-api/pipes/asset-preview-pipe.mdx
  26. 51 0
      docs/docs/reference/admin-ui-api/pipes/has-permission-pipe.mdx
  27. 47 0
      docs/docs/reference/admin-ui-api/pipes/locale-currency-name-pipe.mdx
  28. 54 0
      docs/docs/reference/admin-ui-api/pipes/locale-currency-pipe.mdx
  29. 48 0
      docs/docs/reference/admin-ui-api/pipes/locale-date-pipe.mdx
  30. 47 0
      docs/docs/reference/admin-ui-api/pipes/locale-language-name-pipe.mdx
  31. 47 0
      docs/docs/reference/admin-ui-api/pipes/locale-region-name-pipe.mdx
  32. 44 0
      docs/docs/reference/admin-ui-api/pipes/time-ago-pipe.mdx
  33. 225 0
      docs/docs/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy.mdx
  34. 695 0
      docs/docs/reference/core-plugins/elasticsearch-plugin/elasticsearch-options.mdx
  35. 169 0
      docs/docs/reference/core-plugins/harden-plugin/index.mdx
  36. 211 0
      docs/docs/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin.mdx
  37. 93 0
      docs/docs/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-strategy.mdx
  38. 65 0
      docs/docs/reference/core-plugins/job-queue-plugin/pub-sub-job-queue-strategy.mdx
  39. 36 0
      docs/docs/reference/core-plugins/job-queue-plugin/pub-sub-plugin.mdx
  40. 350 0
      docs/docs/reference/core-plugins/payments-plugin/braintree-plugin.mdx
  41. 209 0
      docs/docs/reference/core-plugins/payments-plugin/mollie-plugin.mdx
  42. 355 0
      docs/docs/reference/core-plugins/payments-plugin/stripe-plugin.mdx
  43. 158 0
      docs/docs/reference/core-plugins/sentry-plugin/index.mdx
  44. 97 0
      docs/docs/reference/typescript-api/auth/authentication-strategy.mdx
  45. 85 0
      docs/docs/reference/typescript-api/cache/redis-cache-plugin.mdx
  46. 75 0
      docs/docs/reference/typescript-api/cache/redis-cache-strategy.mdx
  47. 239 0
      docs/docs/reference/typescript-api/data-access/list-query-builder.mdx
  48. 203 0
      docs/docs/reference/typescript-api/orders/active-order-strategy.mdx
  49. 31 0
      docs/docs/reference/typescript-api/payment/dummy-payment-handler.mdx
  50. 116 0
      docs/docs/reference/typescript-api/request/relations-decorator.mdx

+ 246 - 0
docs/docs/guides/deployment/deploy-to-digital-ocean-app-platform/index.mdx

@@ -0,0 +1,246 @@
+---
+title: "Deploying to Digital Ocean"
+---
+
+![Deploy to Digital Ocean App Platform](./deploy-to-do-app-platform.webp)
+
+[App Platform](https://www.digitalocean.com/products/app-platform) is a fully managed platform which allows you to deploy and scale your Vendure server and infrastructure with ease.
+
+:::note
+The configuration in this guide will cost around $22 per month to run.
+:::
+
+## Prerequisites
+
+First of all you'll need to [create a new Digital Ocean account](https://cloud.digitalocean.com/registrations/new) if you
+don't already have one.
+
+For this guide you'll need to have your Vendure project in a git repo on either GitHub or GitLab. App Platform also supports
+deploying from docker registries, but that is out of the scope of this guide.
+
+:::info
+If you'd like to quickly get started with a ready-made Vendure project which includes sample data, you can clone our
+[Vendure one-click-deploy repo](https://github.com/vendurehq/one-click-deploy).
+:::
+
+## Configuration
+
+### Database connection
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+Make sure your DB connection options uses the following environment variables:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+    // ...
+    dbConnectionOptions: {
+        // ...
+        type: 'postgres',
+        database: process.env.DB_NAME,
+        host: process.env.DB_HOST,
+        port: +process.env.DB_PORT,
+        username: process.env.DB_USERNAME,
+        password: process.env.DB_PASSWORD,
+        ssl: process.env.DB_CA_CERT ? {
+            ca: process.env.DB_CA_CERT,
+        } : undefined,
+    },
+};
+```
+### Asset storage
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+Since App Platform services do not include any persistent storage, we need to configure Vendure to use Digital Ocean's
+Spaces service, which is an S3-compatible object storage service. This means you'll need to make sure to have the
+following packages installed:
+
+```
+npm install @aws-sdk/client-s3 @aws-sdk/lib-storage
+```
+
+
+and set up your AssetServerPlugin like this:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AssetServerPlugin.init({
+            route: 'assets',
+            assetUploadDir: process.env.ASSET_UPLOAD_DIR || path.join(__dirname, '../static/assets'),
+            // highlight-start
+            // If the MINIO_ENDPOINT environment variable is set, we'll use
+            // Minio as the asset storage provider. Otherwise, we'll use the
+            // default local provider.
+            storageStrategyFactory: process.env.MINIO_ENDPOINT ?  configureS3AssetStorage({
+                bucket: 'vendure-assets',
+                credentials: {
+                    accessKeyId: process.env.MINIO_ACCESS_KEY,
+                    secretAccessKey: process.env.MINIO_SECRET_KEY,
+                },
+                nativeS3Configuration: {
+                    endpoint: process.env.MINIO_ENDPOINT,
+                    forcePathStyle: true,
+                    signatureVersion: 'v4',
+                    // The `region` is required by the AWS SDK even when using MinIO,
+                    // so we just use a dummy value here.
+                    region: 'eu-west-1',
+                },
+            }) : undefined,
+            // highlight-end
+        }),
+    ],
+    // ...
+};
+```
+
+## Create Spaces Object Storage
+
+First we'll create a Spaces bucket to store our assets. Click the "Spaces Object Storage" nav item and
+create a new space and call it "vendure-assets".
+
+![Create Spaces Object Storage](./01-create-space.webp)
+
+Next we need to create an access key and secret. Click the "API" nav item and generate a new key.
+
+![Create API key](./02-space-access-keys.webp)
+
+Name the key something meaningful like "vendure-assets-key" and then **make sure to copy the secret as it will only be
+shown once**. Store the access key and secret key in a safe place for later - we'll be using it when we set up our 
+app's environment variables.
+
+:::caution
+If you forget to copy the secret key, you'll need to delete the key and create a new one.
+:::
+
+## Create the server resource
+
+Now we're ready to create our app infrastructure! Click the "Create" button in the top bar and select "Apps".
+
+![Create App](./03-create-app.webp)
+
+Now connect to your git repo, and select the repo of your Vendure project.
+
+Depending on your repo, App Platform may suggest more than one app: in this screenshot we are using the one-click-deploy
+repo which contains a Dockerfile, so App Platform is suggesting two different ways to deploy the app. We'll select the
+Dockerfile option, but either option should work fine. Delete the unused resource.
+
+We need to edit the details of the server app. Click the "Edit" button and set the following:
+
+* **Resource Name**: "vendure-server"
+* **Resource Type**: Web Service
+* **Run Command**: `node ./dist/index.js`
+* **HTTP Port**: 3000
+
+At this point you can also click the "Edit Plan" button to select the resource allocation for the server, which will
+determine performance and price. For testing purposes the smallest Basic server (512MB, 1vCPU) is fine. This can also be changed later.
+
+### Add a database 
+
+Next click "Add Resource", select **Database** and click "Add", and then accept the default Postgres database. Click the
+"Create and Attach" to create the database and attach it to the server app.
+
+Your config should now look like this:
+
+![App setup](./04-configure-server.webp)
+
+## Set up environment variables
+
+Next we need to set up the environment variables. Since these will be shared by both the server and worker apps, we'll create
+them at the Global level. 
+
+You can use the "bulk editor" to paste in the following variables (making sure to replace the values in `<angle brackets>` with
+your own values):
+
+```bash
+DB_NAME=${db.DATABASE}
+DB_USERNAME=${db.USERNAME}
+DB_PASSWORD=${db.PASSWORD}
+DB_HOST=${db.HOSTNAME}
+DB_PORT=${db.PORT}
+DB_CA_CERT=${db.CA_CERT}
+// highlight-next-line
+COOKIE_SECRET=<add some random characters>
+SUPERADMIN_USERNAME=superadmin
+// highlight-next-line
+SUPERADMIN_PASSWORD=<create some strong password>
+// highlight-start
+MINIO_ACCESS_KEY=<use the key generated earlier>
+MINIO_SECRET_KEY=<use the secret generated earlier>
+MINIO_ENDPOINT=<use the endpoint of your spaces bucket>
+// highlight-end
+```
+
+:::note
+The values starting with `${db...}` are automatically populated by App Platform and should not be changed, unless you chose
+to name your database something other than `db`.
+:::
+
+:::note
+When using the App Platform with a Dev Database, DigitalOcean only provides a self-signed SSL certificate. This may prevent the Vendure app from starting. As a workaround, you can add the environment variable `NODE_TLS_REJECT_UNAUTHORIZED` and set it to `0`.
+:::
+
+After saving your environment variables you can click through the confirmation screens to create the app.
+
+## Create the worker resource
+
+Now we need to set up the [Vendure worker](/guides/developer-guide/worker-job-queue/) which will handle background tasks. From the dashboard of your newly-created
+app, click the "Create" button and then select "Create Resources From Source Code".
+
+You will be prompted to select the repo again, and then you'll need to set up a new single resource with the following 
+settings:
+
+* **Resource Name**: "vendure-worker"
+* **Resource Type**: Worker
+* **Run Command**: `node ./dist/index-worker.js`
+
+Again you can edit the plan, and the smallest Basic plan is fine for testing purposes.
+
+No new environment variables are needed since we'll be sharing the Global variables with the worker.
+
+## Enable access to the required routes
+
+To be able to access the UI and other routes, we need to declare them first.
+
+1. In the app dashboard, click on the **Settings** tab.
+2. Click on the **vendure-server** component.
+
+   ![Open server settings](./05-open-server-settings.webp)
+
+3. Scroll down to **HTTP Request Routes**.
+4. Click on **Edit**.
+5. Click on **+ Add new route** and fill in the form like this:
+
+   ![Admin app route](./06-admin-app-route.webp)
+
+6. Make sure to check the **Preserve Path Prefix** option.
+
+You need to do this for the following routes:
+- `/admin`
+- `/assets`
+- `/health`
+- `/admin-api`
+- `/shop-api`
+
+7. Click on **Save**.
+
+## Test your Vendure server
+
+Once everything has finished deploying, you can click the app URL to open your Vendure server in a new tab. 
+
+![Open app](./07-open-app.webp)
+
+:::info
+Append `/admin` to the URL to access the admin UI, and log in with the superadmin credentials you set in the environment variables.
+:::

+ 202 - 0
docs/docs/guides/deployment/deploy-to-railway/index.mdx

@@ -0,0 +1,202 @@
+---
+title: "Deploying to Railway"
+---
+
+![Deploy to Railway](./deploy-to-railway.webp)
+
+[Railway](https://railway.app/) is a managed hosting platform which allows you to deploy and scale your Vendure server and infrastructure with ease.
+
+:::note
+This guide should be runnable on the Railway free trial plan, which means you can deploy it for free and thereafter
+pay only for the resources you use, which should be around $5 per month.
+:::
+
+## Prerequisites
+
+First of all you'll need to create a new Railway account (click "login" on the website and enter your email address) if you
+don't already have one.
+
+You'll also need a GitHub account and you'll need to have your Vendure project hosted there. 
+
+In order to use the Railway trial plan, you'll need to connect your GitHub account to Railway via the Railway dashboard.
+
+:::info
+If you'd like to quickly get started with a ready-made Vendure project which includes sample data, you can clone our
+[Vendure one-click-deploy repo](https://github.com/vendurehq/one-click-deploy).
+:::
+
+## Configuration
+
+### Port
+
+Railway defines the port via the `PORT` environment variable, so make sure your Vendure Config uses this variable:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+    apiOptions: {
+        // highlight-next-line
+        port: +(process.env.PORT || 3000),
+        // ...
+    },
+    // ...
+};
+```
+
+### Database connection
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+Make sure your DB connection options uses the following environment variables:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+    // ...
+    dbConnectionOptions: {
+        // ...
+        database: process.env.DB_NAME,
+        host: process.env.DB_HOST,
+        port: +process.env.DB_PORT,
+        username: process.env.DB_USERNAME,
+        password: process.env.DB_PASSWORD,
+    },
+};
+```
+### Asset storage
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+In this guide we will use the AssetServerPlugin's default local disk storage strategy. Make sure you use the
+`ASSET_UPLOAD_DIR` environment variable to set the path to the directory where the uploaded assets will be stored.
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AssetServerPlugin } from '@vendure/asset-server-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AssetServerPlugin.init({
+            route: 'assets',
+            // highlight-next-line
+            assetUploadDir: process.env.ASSET_UPLOAD_DIR || path.join(__dirname, '../static/assets'),
+        }),
+    ],
+    // ...
+};
+```
+
+## Create a new Railway project
+
+From the Railway dashboard, click "New Project" and select "Empty Project". You'll be taken to a screen where you can
+add the first service to your project. 
+
+## Create the database
+
+Click the "Add a Service" button and select "database". Choose a database that matches the one you are using in your
+Vendure project. If you are following along using the one-click-deploy repo, then choose "Postgres".
+
+## Create the Vendure server
+
+Click the "new" button to create a new service, and select "GitHub repo". Select the repository which contains your
+Vendure project. You may need to configure access to this repo if you haven't already done so.
+
+![Create new service](./01-new-service.webp)
+
+### Configure the server service
+
+You should then see a card representing this service in the main area of the dashboard. Click the card and go to the
+"settings" tab.
+
+* Scroll to the "Service" section and rename the service to "vendure-server".
+* Check the "Build" section and make sure the build settings make sense for your repo. If you are using
+the one-click-deploy repo, then it should detect the Dockerfile.
+* In the "Deploy" section, set the "Custom start command" to `node ./dist/index.js`.
+* Finally, scroll up to the "Networking" section and click "Generate domain" to set up a temporary domain for your
+Vendure server.
+
+### Create a Volume
+
+In order to persist the uploaded product images, we need to create a volume. Click the "new" button and select "Volume".
+Attach it to the "vendure-server" service and set the mount path to `/vendure-assets`.
+
+### Configure server env vars
+
+Click on the "vendure-server" service and go to the "Variables" tab. This is where we will set up the environment
+variables which are used in our Vendure Config. You can use the raw editor to add the following variables, making
+sure to replace the highlighted values with your own:
+
+```bash
+DB_NAME=${{Postgres.PGDATABASE}}
+DB_USERNAME=${{Postgres.PGUSER}}
+DB_PASSWORD=${{Postgres.PGPASSWORD}}
+DB_HOST=${{Postgres.PGHOST}}
+DB_PORT=${{Postgres.PGPORT}}
+ASSET_UPLOAD_DIR=/vendure-assets
+// highlight-next-line
+COOKIE_SECRET=<add some random characters>
+SUPERADMIN_USERNAME=superadmin
+// highlight-next-line
+SUPERADMIN_PASSWORD=<create some strong password>
+```
+
+![Setting env vars](./02-env-vars.webp) 
+
+:::note
+The variables starting with `${{Postgres...}}` assume that your database service is called "Postgres". If you have
+named it differently, then you'll need to change these variables accordingly.
+:::
+
+## Create the Vendure worker
+
+Finally, we need to define the worker process which will run the background tasks. Click the "new" button and select
+"GitHub repo". Select again the repository which contains your Vendure project. 
+
+### Configure the worker service
+
+You should then see a card representing this service in the main area of the dashboard. Click the card and go to the
+"settings" tab.
+
+* Scroll to the "Service" section and rename the service to "vendure-worker".
+* Check the "Build" section and make sure the build settings make sense for your repo. If you are using
+  the one-click-deploy repo, then it should detect the Dockerfile.
+* In the "Deploy" section, set the "Custom start command" to `node ./dist/index-worker.js`.
+
+### Configure worker env vars
+
+The worker will need to know how to connect to the database, so add the following variables to the "Variables" tab:
+
+```bash
+DB_NAME=${{Postgres.PGDATABASE}}
+DB_USERNAME=${{Postgres.PGUSER}}
+DB_PASSWORD=${{Postgres.PGPASSWORD}}
+DB_HOST=${{Postgres.PGHOST}}
+DB_PORT=${{Postgres.PGPORT}}
+```
+
+## Test your Vendure server
+
+To test that everything is working, click the "vendure-server" card and then the link to the temporary domain.
+
+![Test server](./03-test-server.webp)
+
+## Next Steps
+
+This setup gives you a basic Vendure server to get started with. When moving to a more production-ready setup, you'll
+want to consider the following:
+
+- Use MinIO for asset storage. This is a more robust and scalable solution than the local disk storage used here. 
+  - [MinIO template for Railway](https://railway.app/template/SMKOEA), 
+  - [Configuring the AssetServerPlugin for MinIO](/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy/#usage-with-minio)
+- Use Redis to power the job queue and session cache. This is not only more performant, but will enable horizontal scaling of your
+server and worker instances.
+  - [Railway Redis docs](https://docs.railway.app/guides/redis)
+  - [Vendure horizontal scaling docs](/guides/deployment/horizontal-scaling)
+  

+ 221 - 0
docs/docs/guides/deployment/deploy-to-render/index.mdx

@@ -0,0 +1,221 @@
+---
+title: "Deploying to Render"
+---
+
+![Deploy to Render](./deploy-to-render.webp)
+
+[Render](https://render.com/) is a managed hosting platform which allows you to deploy and scale your Vendure server and infrastructure with ease.
+
+:::note
+The configuration in this guide will cost from around $12 per month to run.
+:::
+
+## Prerequisites
+
+First of all you'll need to [create a new Render account](https://dashboard.render.com/register) if you
+don't already have one.
+
+For this guide you'll need to have your Vendure project in a git repo on either GitHub or GitLab.
+
+:::info
+If you'd like to quickly get started with a ready-made Vendure project which includes sample data, you can use our
+[Vendure one-click-deploy repo](https://github.com/vendurehq/one-click-deploy), which means you won't have
+to set up your own git repo.
+:::
+
+## Configuration
+
+### Port
+
+Render defines the port via the `PORT` environment variable and [defaults to `10000`](https://docs.render.com/web-services#host-and-port-configuration), so make sure your Vendure Config uses this variable:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+    apiOptions: {
+        // highlight-next-line
+        port: +(process.env.PORT || 3000),
+        // ...
+    },
+    // ...
+};
+```
+
+### Database connection
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+Make sure your DB connection options uses the following environment variables:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+    // ...
+    dbConnectionOptions: {
+        // ...
+        database: process.env.DB_NAME,
+        host: process.env.DB_HOST,
+        port: +process.env.DB_PORT,
+        username: process.env.DB_USERNAME,
+        password: process.env.DB_PASSWORD,
+    },
+};
+```
+### Asset storage
+
+:::info
+The following is already pre-configured if you are using the one-click-deploy repo.
+:::
+
+In this guide we will use the AssetServerPlugin's default local disk storage strategy. Make sure you use the
+`ASSET_UPLOAD_DIR` environment variable to set the path to the directory where the uploaded assets will be stored.
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AssetServerPlugin } from '@vendure/asset-server-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AssetServerPlugin.init({
+            route: 'assets',
+            // highlight-next-line
+            assetUploadDir: process.env.ASSET_UPLOAD_DIR || path.join(__dirname, '../static/assets'),
+        }),
+    ],
+    // ...
+};
+```
+
+## Create a database
+
+From the Render dashboard, click the "New" button and select "PostgreSQL" from the list of services:
+
+![Create a new PostgreSQL database](./01-create-db.webp)
+
+Give the database a name (e.g. "postgres"), select a region close to you, select an appropriate plan
+and click "Create Database".
+
+## Create the Vendure server
+
+Click the "New" button again and select "Web Service" from the list of services. Choose the "Build and deploy from a Git repository" option.
+
+In the next step you will be prompted to connect to either GitHub or GitLab. Select the appropriate option and follow the instructions
+to connect your account and grant access to the repository containing your Vendure project.
+
+:::info
+If you are using the one-click-deploy repo, you should instead use the "Public Git repository" option and enter the URL of the repo:
+
+```
+https://github.com/vendurehq/one-click-deploy
+```
+:::
+
+### Configure the server service
+
+In the next step you will configure the server:
+
+- **Name**: "vendure-server"
+- **Region**: Select a region close to you
+- **Branch**: Select the branch you want to deploy, usually "main" or "master"
+- **Runtime**: If you have a Dockerfile then it should be auto-detected. If not you should select "Node" and enter the appropriate build and start commands. For a
+typical Vendure project these would be:
+  - **Build Command**: `yarn; yarn build` or `npm install; npm run build`
+  - **Start Command**: `node ./dist/index.js`
+- **Instance Type**: Select the appropriate instance type. Since we want to use a persistent volume to store our assets, we need to
+use at least the "Starter" instance type or higher.
+
+Click the "Advanced" button to expand the advanced options:
+
+- Click "Add Disk" to set up a persistent volume for the assets and use the following settings:
+  - **Name**: "vendure-assets"
+  - **Mount Path**: `/vendure-assets`
+  - **Size**: As appropriate. For testing purposes you can use the smallest size (1GB)
+- **Health Check Path**: `/health`
+- **Docker Command**: `node ./dist/index.js` (if you are _not_ using a Dockerfile this option will not be available)
+
+Click "Create Web Service" to create the service.
+
+:::note
+If you have not already set up payment, you will be prompted to enter credit card details at this point.
+:::
+
+## Configure environment variables
+
+Next we need to set up the environment variables which will be used by both the server and worker. Click the "Env Groups" tab
+and then click the "New Environment Group" button.
+
+Name the group "vendure configuration" and add the following variables. The database variables can be found by navigating
+to the database service, clicking the "Info" tab and scrolling to the "Connections" section:
+
+![Database connection settings](./03-db-connection.webp)
+
+```bash
+DB_NAME=<database "Database">
+DB_USERNAME=<database "Username">
+DB_PASSWORD=<database "Password">
+DB_HOST=<database "Hostname">
+DB_PORT=<database "Port">
+ASSET_UPLOAD_DIR=/vendure-assets
+// highlight-next-line
+COOKIE_SECRET=<add some random characters>
+SUPERADMIN_USERNAME=superadmin
+// highlight-next-line
+SUPERADMIN_PASSWORD=<create some strong password>
+```
+Once the correct values have been entered, click "Create Environment Group".
+
+Next, click the "vendure-server" service and go to the "Environment" tab to link the environment group to the service:
+
+![Link environment group](./04-link-env-group.webp)
+
+## Create the Vendure worker
+
+Finally, we need to define the worker process which will run the background tasks. Click the "New" button and select
+"Background Worker".
+
+Select the same git repo as before, and in the next step configure the worker:
+
+- **Name**: "vendure-worker"
+- **Region**: Same as the server
+- **Branch**: Select the branch you want to deploy, usually "main" or "master"
+- **Runtime**: If you have a Dockerfile then it should be auto-detected. If not you should select "Node" and enter the appropriate build and start commands. For a
+  typical Vendure project these would be:
+  - **Build Command**: `yarn; yarn build` or `npm install; npm run build`
+  - **Start Command**: `node ./dist/index-worker.js`
+- **Instance Type**: Select the appropriate instance type. The Starter size is fine to get started.
+
+Click the "Advanced" button to expand the advanced options:
+
+- **Docker Command**: `node ./dist/index-worker.js` (if you are _not_ using a Dockerfile this option will not be available)
+
+Click "Create Background Worker" to create the worker.
+
+Finally, click the "Environment" tab and link the "vendure configuration" environment group to the worker.
+
+## Test your Vendure server
+
+Navigate back to the dashboard, click the "vendure-server" service, and you should see a link to the temporary domain:
+
+![Test server](./05-server-url.webp)
+
+Click the link and append `/admin` to the URL to open the Admin UI. Log in with the username and password you set in the
+environment variables.
+
+## Next Steps
+
+This setup gives you a basic Vendure server to get started with. When moving to a more production-ready setup, you'll
+want to consider the following:
+
+- Use MinIO for asset storage. This is a more robust and scalable solution than the local disk storage used here.
+  - [Deploying MinIO to Render](https://docs.render.com/deploy-minio),
+  - [Configuring the AssetServerPlugin for MinIO](/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy/#usage-with-minio)
+- Use Redis to power the job queue and session cache. This is not only more performant, but will enable horizontal scaling of your
+  server and worker instances.
+  - [Render Redis docs](https://docs.render.com/redis#creating-a-redis-instance)
+  - [Vendure horizontal scaling docs](/guides/deployment/horizontal-scaling)
+  

+ 2 - 2
docs/docs/guides/deployment/horizontal-scaling.mdx

@@ -51,13 +51,13 @@ For a more complete guide, see the [Using Docker guide](/guides/deployment/using
 
 PM2 must be installed on your server:
 
-```sh
+```bash
 npm install pm2@latest -g
 ```
 
 Your processes can then be run in [cluster mode](https://pm2.keymetrics.io/docs/usage/cluster-mode/) with the following command:
 
-```sh
+```bash
 pm2 start ./dist/index.js -i 4
 ```
 

+ 3 - 3
docs/docs/guides/deployment/production-configuration/index.mdx

@@ -47,7 +47,7 @@ It is recommended that you install and configure the [HardenPlugin](/reference/c
 
 Install the plugin: 
 
-```sh
+```bash
 npm install @vendure/harden-plugin
 
 # or
@@ -104,13 +104,13 @@ Vendure internally treats all dates & times as UTC. However, you may sometimes r
 
 You can check the timezone in **MySQL/MariaDB** by executing:
 
-```SQL
+```sql
 SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP);
 ```
 and you should expect to see `00:00:00`.
 
 In **Postgres**, you can execute:
-```SQL
+```sql
 show timezone;
 ```
 and you should expect to see `UTC` or `Etc/UTC`.

+ 2 - 2
docs/docs/guides/deployment/using-docker.mdx

@@ -21,13 +21,13 @@ RUN npm run build
 
 This Dockerfile can then be built into an "image" using:
 
-```sh
+```bash
 docker build -t vendure .
 ```
 
 This same image can be used to run both the Vendure server and the worker:
 
-```sh
+```bash
 # Run the server
 docker run -dp 3000:3000 --name vendure-server vendure npm run start:server
 

+ 137 - 0
docs/docs/guides/developer-guide/dataloaders/index.mdx

@@ -0,0 +1,137 @@
+---
+title: "GraphQL Dataloaders"
+showtoc: true
+---
+
+[Dataloaders](https://github.com/graphql/dataloader) are used in GraphQL to solve the so called N+1 problem. This is an advanced performance optimization technique you may
+want to use in your application if you find certain custom queries are slow or inefficient.
+
+## N+1 problem
+
+Imagine a cart with 20 items. Your implementation requires you to perform an `async` calculation `isSubscription` for each cart item which executes one or more queries each time it is called, and it takes pretty long on each execution. It works fine for a cart with 1 or 2 items. But with more than 15 items, suddenly the cart takes a **lot** longer to load. Especially when the site is busy.
+
+The reason: the N+1 problem. Your cart is firing of 20 or more queries almost at the same time, adding **significantly** to the GraphQL request. It's like going to the McDonald's drive-in to get 10 hamburgers and getting in line 10 times to get 1 hamburger at a time. It's not efficient.
+
+## The solution: dataloaders
+
+Dataloaders allow you to say: instead of loading each field in the `grapqhl` tree one at a time, aggregate all the `ids` you want to execute the `async` calculation for, and then execute this for all the `ids` in one efficient `request`.
+
+Dataloaders are generally used on `fieldResolver`s. Often, you will need a specific dataloader for each field resolver.
+
+A Dataloader can return anything: `boolean`, `ProductVariant`, `string`, etc.
+
+## Performance implications
+
+Dataloaders can have a huge impact on performance. If your `fieldResolver` executes queries, and you log these queries, you should see a cascade of queries before the implementation of the dataloader, change to a single query using multiple `ids` after you implement it.
+
+## Do I need this for `CustomField` relations? 
+
+No, not normally. `CustomField` relations are automatically added to the root query for the `entity` that they are part of. So, they are loaded as part of the query that loads that entity.
+
+## Example
+
+We will provide a complete example here for you to use as a starting point. The skeleton created can handle multiple dataloaders across multiple channels. We will implement a `fieldResolver` called `isSubscription` for an `OrderLine` that will return a `true/false` for each incoming `orderLine`, to indicate whether the `orderLine` represents a subscription.
+
+
+```ts title="src/plugins/my-plugin/api/api-extensions.ts"
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    extend type OrderLine {
+        isSubscription: Boolean!
+    }
+`
+```
+
+This next part import the `dataloader` package, which you can install with
+
+```bash
+npm install dataloader
+```
+
+**Dataloader skeleton**
+
+```ts title="src/plugins/my-plugin/api/dataloader.ts"
+import DataLoader from 'dataloader'
+
+const LoggerCtx = 'SubscriptionDataloaderService'
+
+@Injectable({ scope: Scope.REQUEST }) // Important! Dataloaders live at the request level
+export class DataloaderService {
+
+  /**
+   * first level is channel identifier, second level is dataloader key
+   */
+  private loaders = new Map<string, Map<string, DataLoader<ID, any>>>()
+
+  constructor(private service: SubscriptionExtensionService) {}
+
+  getLoader(ctx: RequestContext, dataloaderKey: string) {
+    const token = ctx.channel?.code ?? `${ctx.channelId}`
+    
+    Logger.debug(`Dataloader retrieval: ${token}, ${dataloaderKey}`, LoggerCtx)
+
+    if (!this.loaders.has(token)) {
+      this.loaders.set(token, new Map<string, DataLoader<ID, any>>())
+    }
+
+    const channelLoaders = this.loaders.get(token)!
+    if (!channelLoaders.get(dataloaderKey)) {
+      let loader: DataLoader<ID, any>
+
+      switch (dataloaderKey) {
+        case 'is-subscription':
+          loader = new DataLoader<ID, any>((ids) =>
+            this.batchLoadIsSubscription(ctx, ids as ID[]),
+          )
+          break
+        // Implement cases for your other dataloaders here
+        default:
+          throw new Error(`Unknown dataloader key ${dataloaderKey}`)
+      }
+
+      channelLoaders.set(dataloaderKey, loader)
+    }
+    return channelLoaders.get(dataloaderKey)!
+  }
+
+  private async batchLoadIsSubscription(
+    ctx: RequestContext,
+    ids: ID[],
+  ): Promise<Boolean[]> {
+    // Returns an array of ids that represent those input ids that are subscriptions
+    // Remember: this array can be smaller than the input array
+    const subscriptionIds = await this.service.whichSubscriptions(ctx, ids)
+
+    Logger.debug(`Dataloader is-subscription: ${ids}: ${subscriptionIds}`, LoggerCtx)
+
+    return ids.map((id) => subscriptionIds.includes(id)) // Important! preserve order and size of input ids array
+  }
+}
+```
+
+
+```ts title="src/plugins/my-plugin/api/entity-resolver.ts"
+@Resolver(() => OrderLine)
+export class MyPluginOrderLineEntityResolver {
+  constructor(
+    private dataloaderService: DataloaderService,
+  ) {}
+
+  @ResolveField()
+  isSubscription(@Ctx() ctx: RequestContext, @Parent() parent: OrderLine) {
+    const loader = this.dataloaderService.getLoader(ctx, 'is-subscription')
+    return loader.load(parent.id)
+  }
+}
+```
+
+To make it all work, ensure that the `DataLoaderService` is loaded in your `plugin` as a provider.
+
+:::tip
+Dataloaders map the result in the same order as the `ids` you send to the dataloader. 
+Dataloaders expect the same order and array size in the return result. 
+
+In other words: ensure that the order of your returned result is the same as the incoming `ids` and don't omit values!
+:::
+

+ 264 - 0
docs/docs/guides/developer-guide/security/index.mdx

@@ -0,0 +1,264 @@
+---
+title: "Security"
+---
+
+Security of your Vendure application includes considering how to prevent and protect against common security threats such as:
+
+- Data breaches
+- Unauthorized access
+- Attacks aimed at disrupting the service
+
+Vendure itself is designed with security in mind, but you must also consider the security of your own application code, the server environment, and the network architecture.
+
+## Basics
+
+Here are some basic measures you should use to secure your Vendure application. These are not exhaustive, but they are a good starting point.
+
+### Change the default credentials
+
+Do not deploy any public Vendure instance with the default superadmin credentials (`superadmin:superadmin`). Use your hosting platform's environment variables to set a **strong** password for the Superadmin account.
+
+```ts
+import { VendureConfig } from '@vendure/core';
+
+export const config: VendureConfig = {
+  authOptions: {
+    tokenMethod: ['bearer', 'cookie'],
+    superadminCredentials: {
+      identifier: process.env.SUPERADMIN_USERNAME,
+      password: process.env.SUPERADMIN_PASSWORD,
+    },
+  },
+  // ...
+};
+```
+
+### Use the HardenPlugin
+
+It is recommended that you install and configure the [HardenPlugin](/reference/core-plugins/harden-plugin/) for all production deployments. This plugin locks down your schema 
+(disabling introspection and field suggestions) and protects your Shop API against malicious queries that could otherwise overwhelm your server.
+
+Install the plugin:
+
+```bash
+npm install @vendure/harden-plugin
+
+# or
+
+yarn add @vendure/harden-plugin
+```
+
+Then add it to your VendureConfig:
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { HardenPlugin } from '@vendure/harden-plugin';
+
+const IS_DEV = process.env.APP_ENV === 'dev';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    HardenPlugin.init({
+      maxQueryComplexity: 500,
+      apiMode: IS_DEV ? 'dev' : 'prod',
+    }),
+    // ...
+  ]
+};
+```
+
+:::info
+For a detailed explanation of how to best configure this plugin, see the [HardenPlugin docs](/reference/core-plugins/harden-plugin/).
+:::
+
+### Harden the AssetServerPlugin
+
+If you are using the [AssetServerPlugin](/reference/core-plugins/asset-server-plugin/), it is possible by default to use the dynamic
+image transform feature to overload the server with requests for new image sizes & formats. To prevent this, you can
+configure the plugin to only allow transformations for the preset sizes, and limited quality levels and formats.
+Since v3.1 we ship the [PresetOnlyStrategy](/reference/core-plugins/asset-server-plugin/preset-only-strategy/) for this purpose, and
+you can also create your own strategies.
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { AssetServerPlugin, PresetOnlyStrategy } from '@vendure/asset-server-plugin';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    AssetServerPlugin.init({
+      // ...
+      // highlight-start  
+      imageTransformStrategy: new PresetOnlyStrategy({
+        defaultPreset: 'large',
+        permittedQuality: [0, 50, 75, 85, 95],
+        permittedFormats: ['jpg', 'webp', 'avif'],
+        allowFocalPoint: false,
+      }),
+      // highlight-end
+    }),
+  ]
+};
+```
+
+## OWASP Top Ten Security Assessment
+
+The Open Worldwide Application Security Project (OWASP) is a nonprofit foundation that works to improve the security of software.
+
+It publishes a top 10 list of common web application vulnerabilities: https://owasp.org/Top10
+
+This section assesses Vendure against this list, stating what is covered **out of the box** (built in to the framework or easily configurable) and what needs to be **additionally considered.**
+
+### 1. Broken Access Control
+
+Reference: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
+
+Out of the box:
+
+- Vendure uses role-based access control
+- We deny by default for non-public API requests
+- Built-in CORS controls for session cookies
+- Directory listing is not possible via default configuration (e.g. exposing web root dir contents)
+- Stateful session identifiers should be invalidated on the server after logout. On logout we delete all session records from the DB & session cache.
+
+To consider:
+
+- Rate limit API and controller access to minimize the harm from automated attack tooling.
+
+### 2. Cryptographic Failures
+
+Reference: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
+
+Out of the box:
+
+- Vendure defaults to bcrypt with 12 salt rounds for storing passwords. This strategy is configurable if security requirements mandate alternative algorithms.
+- No deprecated hash functions (SHA1, MD5) are used in security-related contexts (only for things like creating cache keys).
+- Payment information is not stored in Vendure by default. Payment integrations rely on the payment provider to store all sensitive data.
+
+To consider:
+
+- The Vendure server will not use TLS be default. The usual configuration is to handle this at the gateway level on your production platform.
+- If a network caching layer is used (e.g. Stellate), ensure it is configured to not cache user-related data (customer details, active order etc)
+
+### 3. Injection
+
+Reference: https://owasp.org/Top10/A03_2021-Injection/
+
+Out of the box:
+
+- GraphQL has built-in validation of incoming data
+- All database operations are parameterized - no string concatenation using user-supplied data.
+- List queries apply default limits to prevent mass disclosure of records.
+
+To consider:
+
+- If using custom fields, you should consider defining a validation function to prevent bad data from getting into the database.
+
+### 4. Insecure Design
+
+Reference: https://owasp.org/Top10/A04_2021-Insecure_Design/
+
+Out of the box:
+
+- Use of established libraries for the critical underlying components: NestJS, TypeORM, Angular.
+- End-to-end tests of security-related flows such as authentication, verification, and RBAC permissions controls.
+- Harden plugin provides pre-configured protections against common attack vectors targeting GraphQL APIs.
+
+To consider:
+
+- Tiered exposure such as an API gateway which prevents exposure of the Admin API to the public internet.
+- Limit resource usage of Vendure server & worker instances via containerization.
+- Rate limiting & other network-level protections (such as Cloudflare) should be considered.
+
+### 5. Security Misconfiguration
+
+Reference: https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
+
+Out of the box:
+
+- Single point of configuration for the entire application, reducing the chance of misconfiguration.
+- A default setup only requires a database, which means there are few components to configure and harden.
+- Stack traces are not leaked in API errors
+
+To consider:
+
+- Ensure the default superadmin credentials are not used in production
+- Use environment variables to turn off development features such as the GraphQL playground
+- Use the HardenPlugin in production to automatically turn of development features and restrict system information leaking via API.
+- Use fine-grained permissions and roles for your administrator accounts to reduce the attack surface if an account is compromised.
+
+### 6. Vulnerable and Outdated Components
+
+Reference: https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/
+
+Out of the box:
+
+- All dependencies are updated to current versions with each minor release
+- Modular design limits the number of dependencies for core packages.
+- Automated code & dependency scanning is used in the Vendure repo
+
+To consider:
+
+- Run your own audits on your code base.
+- Use version override mechanisms if needed to patch and critical Vendure dependencies that did not yet get updated.
+
+### 7. Identification and Authentication Failures
+
+Reference: https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/
+
+Out of the box:
+
+- Valid usernames are not leaked via mechanisms such as account reset
+- Does not permit "knowlege-based" account recovery
+- Uses strong password hashing (bcrypt with 12 salt rounds)
+- Session identifiers are not exposed in API urls (instead we use headers/cookies)
+- New session tokens always regenerated after successful login
+- Sessions deleted during logout
+- Cryptographically-strong, high-entropy session tokens are used (crypto.randomBytes API)
+
+To consider:
+
+- Implementing a multi-factor authentication flow
+- Do not use default superadmin credentials in production
+- Implementing a custom PasswordValidationStrategy to disallow weak/common passwords
+- Subscribe to AttemptedLoginEvent to implement detection of brute-force attacks
+
+### 8. Software and Data Integrity Failures
+
+Reference: https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/
+
+To consider:
+
+- Exercise caution when introducing new dependencies to your project.
+- Do not use untrusted Vendure plugins. Where possible review the code prior to use.
+- Exercise caution if using auto-updating mechanisms for dependencies.
+- If storing serialized data in custom fields, implement validation to prevent untrusted data getting into the database.
+- Evaluate your CI/CD pipeline against the OWASP recommendations for this point
+
+### 9. Security Logging and Monitoring Failures
+
+Reference: https://owasp.org/Top10/A09_2021-Security_Logging_and_Monitoring_Failures/
+
+Out of the box:
+
+- APIs for integrating logging & monitoring tools & services, e.g. configurable Logger interface & ErrorHandlerStrategy
+- Official Sentry integration for application performance monitoring
+
+To consider:
+
+- Integrate with dedicated logging tools for improved log management
+- Integrate with monitoring tools such as Sentry
+- Use the EventBus to monitor events such as repeated failed login attempts and high-value orders
+
+### 10. Server-Side Request Forgery (SSRF)
+
+Reference: [https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_(SSRF)/](https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/)
+
+Out of the box:
+
+- By default Vendure does not rely on requests to remote servers for core functionality
+
+To consider:
+
+- Review the OWASP recommendations against your network architecture

+ 2 - 2
docs/docs/guides/developer-guide/stand-alone-scripts/index.mdx

@@ -54,7 +54,7 @@ async function getProductCount() {
 
 This script can then be run from the command-line:
 
-```shell
+```bash
 npx ts-node src/get-product-count.ts
 
 # or
@@ -64,7 +64,7 @@ yarn ts-node src/get-product-count.ts
 
 resulting in the following output:
 
-```shell
+```bash
 info 01/08/23, 11:50 - [Vendure Worker] Bootstrapping Vendure Worker (pid: 4428)...
 info 01/08/23, 11:50 - [Vendure Worker] Vendure Worker is ready
 info 01/08/23, 11:50 - [Vendure Worker]

+ 1 - 1
docs/docs/guides/developer-guide/testing/index.mdx

@@ -24,7 +24,7 @@ For a working example of a Vendure plugin with e2e testing, see the [real-world-
   - `@swc/core`
   - `unplugin-swc`
 
-```sh
+```bash
 npm install --save-dev @vendure/testing vitest graphql-tag @swc/core unplugin-swc
 ```
 

+ 113 - 0
docs/docs/guides/extending-the-admin-ui/adding-ui-translations/index.mdx

@@ -0,0 +1,113 @@
+---
+title: 'Adding UI Translations'
+---
+
+The Vendure Admin UI is fully localizable, allowing you to:
+
+* create custom translations for your UI extensions
+* override existing translations
+* add complete translations for whole new languages
+
+![The UI language is set from the User menu](./ui-translations-01.webp)
+
+## Translation format
+
+The Admin UI uses the [Messageformat](https://messageformat.github.io/messageformat/) specification to convert i18n tokens to localized strings in the browser. Each language should have a corresponding JSON file containing the translations for that language.
+
+Here is an excerpt from the `en.json` file that ships with the Admin UI:
+
+```json title="en.json"
+{
+  "admin": {
+    "create-new-administrator": "Create new administrator"
+  },
+  "asset": {
+    "add-asset": "Add asset",
+    "add-asset-with-count": "Add {count, plural, 0 {assets} one {1 asset} other {{count} assets}}",
+    "assets-selected-count": "{ count } assets selected",
+    "dimensions": "Dimensions"
+  }
+}
+```
+
+The translation tokens are grouped into a single-level deep nested structure. In the Angular code, these are referenced like this: 
+
+```html
+<label>{{ 'asset.assets-selected-count' | translate:{ count } }}</label>
+```
+
+That is, the `{ ... }` represent variables that are passed from the application code and interpolated into the final localized string.
+
+## Adding a new language
+
+The Admin UI ships with built-in support for many languages, but allows you to add support for any other language without the need to modify the package internals.
+
+1. **Create your translation file**
+
+     Start by copying the contents of the [English language file](https://github.com/vendurehq/vendure/blob/master/packages/admin-ui/src/lib/static/i18n-messages/en.json) into a new file, `<languageCode>.json`, where `languageCode` is the 2-character [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for the language. Replace the strings with the translation for the new language.
+2. **Install `@vendure/ui-devkit`**
+
+    If not already installed, install the `@vendure/ui-devkit` package, which allows you to create custom builds of the Admin UI.
+3. **Register the translation file**
+  
+    Here's a minimal directory structure and sample code to add your new translation:
+    
+    ```text
+    /src
+    ├─ vendure-config.ts
+    └─ translations/
+        └─ ms.json
+    ```
+    
+    And the config code to register the translation file:
+    
+    ```ts title="src.vendure-config.ts"
+    import path from 'path';
+    import { VendureConfig } from '@vendure/core';
+    import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+    import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+    
+    export const config: VendureConfig = {
+        // ...
+        plugins: [
+            AdminUiPlugin.init({
+                port: 3002,
+                app: compileUiExtensions({
+                    outputPath: path.join(__dirname, '../admin-ui'),
+                    extensions: [{
+                        translations: {
+                            ms: path.join(__dirname, 'translations/ms.json'),
+                        }
+                    }],
+                }),
+                adminUiConfig:{
+                    defaultLanguage: LanguageCode.ms,
+                    availableLanguages: [LanguageCode.ms, LanguageCode.en],
+                }
+            }),
+        ],
+    };
+    ```
+
+## Translating UI Extensions
+
+You can also create translations for your own UI extensions, in much the same way as outlined above in "Adding a new language". Your translations can be split over several files, since the `translations` config object can take a glob, e.g.:
+
+```ts
+translations: {
+  de: path.join(__dirname, 'ui-extensions/my-extension/**/*.de.json'),
+}
+```
+
+This allows you, if you wish, to co-locate your translation files with your components.
+
+Care should be taken to uniquely namespace your translation tokens, as conflicts with the base translation file will cause your translations to overwrite the defaults. This can be solved by using a unique section name, e.g.:
+
+```json
+{
+  "my-reviews-plugin": {
+    "all-reviews": "All reviews",
+    "approve review": "Approve review"
+  }
+}
+```

+ 205 - 0
docs/docs/guides/extending-the-admin-ui/using-other-frameworks/index.mdx

@@ -0,0 +1,205 @@
+---
+title: 'Using Other Frameworks'
+weight: 1
+---
+
+From version 2.1.0, Admin UI extensions can be written in either Angular or React. Prior to v2.1.0, only Angular was natively supported. 
+
+It is, however, possible to extend the Admin UI using other frameworks such as Vue, Svelte, Solid etc. Note that the extension experience is much more limited than with Angular or React, but depending on your needs it may be sufficient.
+
+:::info
+For working examples of a UI extensions built with **Vue**, see the [real-world-vendure ui extensions](https://github.com/vendurehq/real-world-vendure/tree/master/src/ui-extensions)
+:::
+
+There is still a small amount of Angular "glue code" needed to let the compiler know how to integrate your extension, so let's take a look at how this is done.
+
+## 1. Install `@vendure/ui-devkit`
+
+To create UI extensions, you'll need to install the `@vendure/ui-devkit` package. This package contains a compiler for building your customized version of the Admin UI, as well as the Angular dependencies you'll need to create your extensions.
+
+```bash
+yarn add @vendure/ui-devkit
+
+# or
+
+npm install @vendure/ui-devkit
+```
+
+## 2. Create the folder structure
+
+In this example, we will work with the following folder structure, and use Create React App our example.
+
+```text
+src
+└─plugins
+  └─ my-plugin
+    └─ ui
+      ├─ routes.ts
+      └─ vue-app
+        └─ (directory created by `vue create`, for example)
+```
+
+## 3. Create an extension module
+
+Here's the Angular code needed to tell the compiler where to find your extension:
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { hostExternalFrame } from '@vendure/admin-ui/core';
+
+export default [
+    hostExternalFrame({
+        path: '',
+
+        // You can also use parameters which allow the app
+        // to have dynamic routing, e.g.
+        // path: ':slug'
+        // Then you can use the getActivatedRoute() function from the
+        // UiDevkitClient in order to access the value of the "slug"
+        // parameter.
+
+        breadcrumbLabel: 'Vue App',
+        // This is the URL to the compiled React app index.
+        // The next step will explain the "assets/react-app" path.
+        extensionUrl: './assets/vue-app/index.html',
+        openInNewTab: false,
+    })
+];
+```
+
+## 4. Define the AdminUiExtension config
+
+Next we will define an [AdminUiExtension](/reference/admin-ui-api/ui-devkit/admin-ui-extension/) object which is passed to the `compileUiExtensions()` function in your Vendure config:
+
+```ts title="src/vendure-config.ts"
+import path from 'path';
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AdminUiPlugin.init({
+            route: 'admin',
+            port: 3002,
+            app: compileUiExtensions({
+                outputPath: path.join(__dirname, '../admin-ui'),
+                extensions: [{
+                    // Points to the path containing our Angular "glue code" module
+                    extensionPath: path.join(__dirname, 'plugins/my-plugin/ui'),
+                    routes: [{ route: 'vue-ui', filePath: 'routes.ts' }],
+                    staticAssets: [
+                        // This is where we tell the compiler to copy the compiled Vue app
+                        // artifacts over to the Admin UI's `/static` directory. In this case we
+                        // also rename "build" to "vue-app". This is why the `extensionUrl`
+                        // in the module config points to './assets/vue-app/index.html'.
+                        {path: path.join(__dirname, 'plugins/my-plugin/ui/vue-app/dist'), rename: 'vue-app'},
+                    ],
+                }],
+                devMode: true,
+            }),
+        }),
+    ]
+};
+```
+
+## 5. Build your extension
+
+To ensure things are working we can now build our Vue app by running `yarn build` in the `vue-app` directory. This will build and output the app artifacts to the `vue-app/build` directory - the one we pointed to in the `staticAssets` array above.
+
+Once build, we can start the Vendure server.
+
+The `compileUiExtensions()` function returns a `compile()` function which will be invoked by the AdminUiPlugin upon server bootstrap. During this compilation process, a new directory will be generated at `/admin-ui` (as specified by the `outputPath` option) which will contains the un-compiled sources of your new Admin UI app.
+
+Next, these source files will be run through the Angular compiler, the output of which will be visible in the console.
+
+Now go to the Admin UI app in your browser and log in. You should now be able to manually enter the URL `http://localhost:3000/admin/extensions/vue-ui` and you should see the Vue app rendered in the Admin UI.
+
+## Integrate with the Admin UI
+
+### Styling
+The `@vendure/admin-ui` package (which will be installed alongside the ui-devkit) provides a stylesheet to allow your extension to fit visually with the rest of the Admin UI.
+
+If you have a build step, you can import it into your app like this:
+
+```ts
+import '@vendure/admin-ui/static/theme.min.css';
+```
+
+If your extension does not have a build step, you can still include the theme stylesheet as a local resource:
+
+```html
+<!-- src/ui-extension/plain-js-app/index.html -->
+<head>
+  <link rel="stylesheet" href="../../theme.min.css" />
+</head>
+```
+
+### UiDevkitClient
+
+The `@vendure/ui-devkit` package provides a number of helper methods which allow your extension to seamlessly interact with the underlying Admin UI infrastructure, collectively known as the [UiDevkitClient](/reference/admin-ui-api/ui-devkit/ui-devkit-client/). The client allows your extension to:
+
+* Make GraphQL queries & mutations, without the need for your own HTTP or GraphQL client, with full integration with the Admin UI client-side GraphQL cache.
+* Display toast notifications.
+
+#### setTargetOrigin
+
+The UiDevkitClient uses the browser's [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to communicate between the Admin UI app and your extension. For security reasons this communication channel is restricted to a specific domain (where your extension app will be running from). To configure this, use the [setTargetOrigin](/reference/admin-ui-api/ui-devkit/ui-devkit-client/#settargetorigin) function:
+
+```ts
+import { setTargetOrigin } from '@vendure/ui-devkit';
+
+setTargetOrigin('http://my-domain.com');
+
+```
+
+If this is mis-configured you will see an error along the lines of "Failed to execute 'postMessage' on 'DOMWindow'".
+
+For apps with a build step, you can use these functions like this:
+
+```ts
+import { graphQlMutation, notify } from '@vendure/ui-devkit';
+
+// somewhere in your component
+const disableProduct = (id: string) => {
+  graphQlMutation(`
+    mutation DisableProduct($id: ID!) {
+      updateProduct(input: { id: $id, enabled: false }) {
+        id
+        enabled
+      }
+    }`, { id }).then(result => {
+     notify({
+       message: 'Updated Product',
+     });
+  })
+}
+```
+
+If your extension does not have a build step, you can still include the UiDevkitClient as a local resource, which will expose a `VendureUiClient` global object:
+
+```html
+<!-- src/ui-extension/plain-js-app/index.html -->
+<head>
+  <script src="../devkit/ui-devkit.js"></script>
+</head>
+<script>
+  const disableProduct = id => {
+    VendureUiClient.graphQlMutation(`
+      mutation DisableProduct($id: ID!) {
+        updateProduct(input: { id: $id, enabled: false }) {
+          id
+          enabled
+        }
+      }`, { id }).then(result => {
+       VendureUiClient.notify({
+         message: 'Updated Product',
+       });
+    })
+  }
+</script>
+```
+
+## Next Steps
+
+Now you have created your extension, you need a way for your admin to access it. See [Adding Navigation Items](/guides/extending-the-admin-ui/nav-menu/)

+ 94 - 0
docs/docs/guides/extending-the-dashboard/localization/index.mdx

@@ -0,0 +1,94 @@
+---
+title: 'Localization'
+---
+
+:::note
+Support for localization of Dashboard extensions was added in v3.5.1
+:::
+
+The Dashboard uses [Lingui](https://lingui.dev/), which provides a powerful i18n solution for React:
+
+- ICU MessageFormat support
+- Automatic message extraction
+- TypeScript integration
+- Pluralization support
+- Compile-time optimization
+
+## Wrap your strings
+
+First you'll need to wrap any strings that need to be localized:
+
+```tsx
+import { Trans, useLingui } from '@lingui/react/macro';
+
+function MyComponent() {
+    const { t } = useLingui();
+
+    return (
+        <div>
+            <h1>
+                // highlight-next-line
+                <Trans>Welcome to Dashboard</Trans>
+            </h1>
+            // highlight-next-line
+            <p>{t`Click here to continue`}</p>
+        </div>
+    );
+}
+```
+
+You will mainly make use of the [Trans component](https://lingui.dev/ref/react#trans)
+and the [useLingui hook](https://lingui.dev/ref/react#uselingui).
+
+## Extract translations
+
+Create a `lingui.config.js` file in your project root, with references to any plugins that need to be localized:
+
+```js title="lingui.config.js
+import { defineConfig } from '@lingui/cli';
+
+export default defineConfig({
+    sourceLocale: 'en',
+    // Add any locales you wish to support
+    locales: ['en', 'de'],
+    catalogs: [
+        // For each plugin you want to localize, add a catalog entry
+        {
+            // This is the output location of the generated .po files
+            path: '<rootDir>/src/plugins/reviews/dashboard/i18n/{locale}',
+            // This is the pattern that tells Lingui which files to scan
+            // to extract translation strings
+            include: ['<rootDir>/src/plugins/reviews/dashboard/**'],
+        },
+    ],
+});
+```
+
+Then extract the translations:
+
+```bash
+npx lingui extract
+```
+
+This will output the given locale files in the directories specified in the config file above.
+In this case:
+
+```
+src/
+└── plugins/
+    └── reviews/
+        └── dashboard/
+            └── i18n/
+                ├── en.po
+                └── de.po
+```
+
+Since we set the "sourceLocale" to be "en", the `en.po` file will already be complete. You'll then need to
+open up the `de.po` file and add German translations for each of the strings, by filling out the empty `msgstr` values:
+
+```text title="de.po"
+#: test-plugins/reviews/dashboard/review-list.tsx:51
+msgid "Welcome to Dashboard"
+// highlight-next-line
+msgstr "Willkommen zum Dashboard"
+```

+ 231 - 0
docs/docs/guides/getting-started/installation/index.mdx

@@ -0,0 +1,231 @@
+---
+title: 'Installation'
+sidebar_position: 1
+---
+
+## Requirements
+
+- [Node.js](https://nodejs.org/en/) **v20**, **v22** and **v24** - these versions are tested and supported. (Odd-numbered versions above v20 should still work but are not officially supported.)
+
+### Optional
+
+- [Docker Desktop](https://www.docker.com/products/docker-desktop/): If you want to use the quick start with Postgres, you must have Docker Desktop installed. If you do not have Docker Desktop installed, then SQLite will be used for your database.
+- If you want to use an existing MySQL, MariaDB, or Postgres server as your data store, then you'll need an instance available locally. However, **if you are just testing out Vendure, we recommend the quick start option, which handles the database for you**.
+
+## @vendure/create
+
+The recommended way to get started with Vendure is by using the [@vendure/create](https://github.com/vendurehq/vendure/tree/master/packages/create) tool. This is a command-line tool which will scaffold and configure your new Vendure project and install all dependencies.
+
+### Quick Start
+
+First, run the following command in your terminal, replacing `my-shop` with the name of your project:
+
+```bash
+npx @vendure/create my-shop
+```
+
+Next, choose the "Quick Start" option. This is the fastest way to get a Vendure server up and running and will handle all the configuration for you.
+If you have Docker Desktop installed, it will create and configure a Postgres database for you. If not, it will use SQLite.
+
+```text
+┌  Let's create a Vendure App ✨
+│
+◆  How should we proceed?
+// highlight-next-line
+│  ● Quick Start (Get up and running in a single step)
+│  ○ Manual Configuration
+└
+```
+
+Next you'll be prompted to include our official Next.js storefront starter in your project. This is optional, but recommended if you want to
+quickly see a working storefront connected to your Vendure server.
+
+```text
+┌  Let's create a Vendure App ✨
+│
+◇  How should we proceed?
+│  Quick Start
+│
+◇  Using port 3000
+│
+◇  Docker is running
+│
+// highlight-start
+◆  Would you like to include the Next.js storefront?
+│  ○ No
+│  ● Yes
+└
+// highlight-end
+```
+
+And that's it! After a minute or two, you'll have a **fully-functional Vendure server** installed locally.
+
+Once the installation is done, your terminal will output a message indicating a successful installation with:
+
+- The URL to access the **Dashboard**
+- Your admin log-in credentials
+- The project file path
+
+Proceed to the [Start the server](#start-the-server) section below to run your Vendure server.
+
+### Manual Configuration
+
+If you'd rather have more control over the configuration, you can choose the "Manual Configuration" option. This will prompt you to select a database and whether to populate the database with sample data.
+
+#### 1. Select a database
+
+Vendure supports a number of different databases. The `@vendure/create` tool will prompt you to select one.
+
+**To quickly test out Vendure, we recommend using SQLite**, which requires no external dependencies. You can always switch to a different database later [by changing your configuration file](/guides/developer-guide/configuration/#connecting-to-the-database).
+
+```text
+┌  Let's create a Vendure App ✨
+│
+◇  How should we proceed?
+│  Manual Configuration
+│
+◇  Using port 3000
+│
+◆  Which database are you using?
+│  ○ MySQL
+│  ○ MariaDB
+│  ○ Postgres
+// highlight-next-line
+│  ● SQLite
+└
+```
+
+:::tip
+If you select MySQL, MariaDB, or Postgres, you need to make sure you:
+
+1. **Have the database server running**: You can either install the database locally on your machine, use a cloud provider, or run it via Docker. For local development with Docker, you can use the provided `docker-compose.yml` file in your project.
+
+2. **Have created a database**: Use your database client to create an empty database (e.g., `CREATE DATABASE vendure;` in most SQL databases).
+
+3. **Have database credentials**: You need the username and password for a database user that has full permissions (CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, SELECT) on the database you created.
+
+For detailed database configuration examples, see the [Configuration guide](/guides/developer-guide/configuration/#connecting-to-the-database).
+
+:::
+
+#### 2. Populate with data
+
+The final prompt will ask whether to populate your new Vendure server with some sample product data.
+
+**We recommend you do so**, as it will give you a good starting point for exploring the APIs, which we will cover in the [Try the API section](/guides/getting-started/try-the-api/), as well as providing some data to use when building your own storefront.
+
+```text
+┌  Let's create a Vendure App ✨
+│
+◇  How should we proceed?
+│  Manual Configuration
+│
+// ...
+│
+◆  Populate with some sample product data?
+// highlight-next-line
+│  ● yes
+│  ○ no
+└
+```
+
+#### 3. Optional storefront setup
+
+From v3.5.2 onwards, you can choose to include an official Next.js Storefront Starter as part of your new Vendure project.
+
+#### 4. Complete setup
+
+Next, a project scaffold will be created and dependencies installed. This may take a few minutes.
+
+Once complete, you'll see a message like this:
+
+```text
+◇  Setup complete! ─────────────────────────────────────╮
+│                                                       │
+│  Your new Vendure project was created!                │
+│  /Users/username/path/my-shop                         │
+│                                                       │
+│                                                       │
+│  This is a monorepo with the following apps:          │
+│    apps/server     - Vendure backend                  │
+│    apps/storefront - Next.js frontend                 │
+│                                                       │
+│                                                       │
+│  Next, run:                                           │
+│  $ cd my-shop                                         │
+│  $ npm run dev                                        │
+│                                                       │
+│                                                       │
+│  This will start both the server and storefront.      │
+│                                                       │
+│                                                       │
+│  Access points:                                       │
+│    Dashboard:  http://localhost:3000/dashboard        │
+│    Storefront: http://localhost:3001                  │
+│                                                       │
+│                                                       │
+│  Use the following credentials to log in:             │
+│  Username: superadmin                                 │
+│  Password: superadmin                                 │
+│                                                       │
+│                                                       │
+│  ➡️ Docs: https://docs.vendure.io                     │
+│  ➡️ Discord community: https://vendure.io/community   │
+│  ➡️ Star us on GitHub:                                │
+│     https://github.com/vendurehq/vendure      │
+│                                                       │
+├───────────────────────────────────────────────────────╯
+│
+└  Happy hacking!
+```
+
+### Start the server
+
+Follow the instructions to move into the new directory created for your project, and start the server:
+
+```bash
+cd my-shop
+
+npm run dev
+```
+
+You should now be able to access:
+
+- The Vendure Admin GraphQL API: [http://localhost:3000/admin-api](http://localhost:3000/admin-api)
+- The Vendure Shop GraphQL API: [http://localhost:3000/shop-api](http://localhost:3000/shop-api)
+- The Vendure Dashboard: [http://localhost:3000/dashboard](http://localhost:3000/dashboard/)
+
+If you included the Next.js Storefront Starter, you can also access:
+
+- The Next.js Storefront: [http://localhost:3001](http://localhost:3001/)
+
+![Running apps](./app-screens.webp)
+
+Congratulations! 🥳 You now have a fully functional Vendure server running locally.
+
+Now you can explore Vendure by following our [Try the API guide](/guides/getting-started/try-the-api/) to learn how to interact with the server.
+
+If you are new to GraphQL, you should also check out our [Introducing GraphQL guide](/guides/getting-started/graphql-intro/).
+
+:::tip
+Open the Dashboard at [http://localhost:3000/dashboard](http://localhost:3000/dashboard) in your browser and log in with the superadmin credentials you specified, which default to:
+
+- **username**: superadmin
+- **password**: superadmin
+
+:::
+
+:::cli
+Use `npx vendure add` to start adding plugins & custom functionality to your Vendure server.
+:::
+
+### Troubleshooting
+
+- If you encounter any issues during installation, you can get a more detailed output by setting the log level to `verbose`:
+
+    ```bash
+    npx @vendure/create my-shop --log-level verbose
+    ```
+
+- The [supported TypeScript version](https://github.com/vendurehq/vendure/blob/master/packages/create/src/constants.ts#L7) is set upon installation. Upgrading to a newer version of TypeScript might result in compilation errors because TypeScript sometimes introduces stricter checks in newer versions.
+- If you want to use **Yarn**, from Vendure v2.2.0+, you'll need to use **Yarn 2** (Berry) or above.

+ 63 - 0
docs/docs/reference/admin-ui-api/components/chip-component.mdx

@@ -0,0 +1,63 @@
+---
+title: "ChipComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ChipComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/chip/chip.component.ts" sourceLine="16" packageName="@vendure/admin-ui" />
+
+A chip component for displaying a label with an optional action icon.
+
+*Example*
+
+```html
+<vdr-chip [colorFrom]="item.value"
+          icon="close"
+          (iconClick)="clear(item)">
+{{ item.value }}</vdr-chip>
+```
+
+```ts title="Signature"
+class ChipComponent {
+    @Input() icon: string;
+    @Input() invert = false;
+    @Input() colorFrom = '';
+    @Input() colorType: 'error' | 'success' | 'warning';
+    @Output() iconClick = new EventEmitter<MouseEvent>();
+}
+```
+
+<div className="members-wrapper">
+
+### icon
+
+<MemberInfo kind="property" type={`string`}   />
+
+The icon should be the name of one of the available Clarity icons: https://clarity.design/foundation/icons/shapes/
+### invert
+
+<MemberInfo kind="property" type={``}   />
+
+
+### colorFrom
+
+<MemberInfo kind="property" type={``}   />
+
+If set, the chip will have an auto-generated background
+color based on the string value passed in.
+### colorType
+
+<MemberInfo kind="property" type={`'error' | 'success' | 'warning'`}   />
+
+The color of the chip can also be one of the standard status colors.
+### iconClick
+
+<MemberInfo kind="property" type={``}   />
+
+
+
+
+</div>

+ 174 - 0
docs/docs/reference/admin-ui-api/components/currency-input-component.mdx

@@ -0,0 +1,174 @@
+---
+title: "CurrencyInputComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## CurrencyInputComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts" sourceLine="33" packageName="@vendure/admin-ui" />
+
+A form input control which displays currency in decimal format, whilst working
+with the integer cent value in the background.
+
+*Example*
+
+```html
+<vdr-currency-input
+    [(ngModel)]="entityPrice"
+    [currencyCode]="currencyCode"
+></vdr-currency-input>
+```
+
+```ts title="Signature"
+class CurrencyInputComponent implements ControlValueAccessor, OnInit, OnChanges, OnDestroy {
+    @Input() disabled = false;
+    @Input() readonly = false;
+    @Input() value: number;
+    @Input() currencyCode = '';
+    @Output() valueChange = new EventEmitter();
+    prefix$: Observable<string>;
+    suffix$: Observable<string>;
+    hasFractionPart = true;
+    onChange: (val: any) => void;
+    onTouch: () => void;
+    _inputValue: string;
+    readonly precision: number;
+    readonly precisionFactor: number;
+    constructor(dataService: DataService, currencyService: CurrencyService)
+    ngOnInit() => ;
+    ngOnChanges(changes: SimpleChanges) => ;
+    ngOnDestroy() => ;
+    registerOnChange(fn: any) => ;
+    registerOnTouched(fn: any) => ;
+    setDisabledState(isDisabled: boolean) => ;
+    onInput(value: string) => ;
+    onFocus() => ;
+    writeValue(value: any) => void;
+}
+```
+* Implements: <code>ControlValueAccessor</code>, <code>OnInit</code>, <code>OnChanges</code>, <code>OnDestroy</code>
+
+
+
+<div className="members-wrapper">
+
+### disabled
+
+<MemberInfo kind="property" type={``}   />
+
+
+### readonly
+
+<MemberInfo kind="property" type={``}   />
+
+
+### value
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### currencyCode
+
+<MemberInfo kind="property" type={``}   />
+
+
+### valueChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### prefix$
+
+<MemberInfo kind="property" type={`Observable&#60;string&#62;`}   />
+
+
+### suffix$
+
+<MemberInfo kind="property" type={`Observable&#60;string&#62;`}   />
+
+
+### hasFractionPart
+
+<MemberInfo kind="property" type={``}   />
+
+
+### onChange
+
+<MemberInfo kind="property" type={`(val: any) =&#62; void`}   />
+
+
+### onTouch
+
+<MemberInfo kind="property" type={`() =&#62; void`}   />
+
+
+### _inputValue
+
+<MemberInfo kind="property" type={`string`}   />
+
+
+### precision
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### precisionFactor
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, currencyService: CurrencyService) => CurrencyInputComponent`}   />
+
+
+### ngOnInit
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### ngOnChanges
+
+<MemberInfo kind="method" type={`(changes: SimpleChanges) => `}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### registerOnChange
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### registerOnTouched
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### setDisabledState
+
+<MemberInfo kind="method" type={`(isDisabled: boolean) => `}   />
+
+
+### onInput
+
+<MemberInfo kind="method" type={`(value: string) => `}   />
+
+
+### onFocus
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### writeValue
+
+<MemberInfo kind="method" type={`(value: any) => void`}   />
+
+
+
+
+</div>

+ 238 - 0
docs/docs/reference/admin-ui-api/components/data-table-component.mdx

@@ -0,0 +1,238 @@
+---
+title: "DataTableComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## DataTableComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.ts" sourceLine="86" packageName="@vendure/admin-ui" />
+
+A table for displaying PaginatedList results. It is designed to be used inside components which
+extend the <a href='/reference/admin-ui-api/list-detail-views/base-list-component#baselistcomponent'>BaseListComponent</a> class.
+
+**Deprecated** This component is deprecated. Use the <a href='/reference/admin-ui-api/components/data-table2component#datatable2component'>DataTable2Component</a> instead.
+
+*Example*
+
+```html
+<vdr-data-table
+  [items]="items$ | async"
+  [itemsPerPage]="itemsPerPage$ | async"
+  [totalItems]="totalItems$ | async"
+  [currentPage]="currentPage$ | async"
+  (pageChange)="setPageNumber($event)"
+  (itemsPerPageChange)="setItemsPerPage($event)"
+>
+  <!-- The header columns are defined first -->
+  <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
+  <vdr-dt-column></vdr-dt-column>
+  <vdr-dt-column></vdr-dt-column>
+
+  <!-- Then we define how a row is rendered -->
+  <ng-template let-taxRate="item">
+    <td class="left align-middle">{{ taxRate.name }}</td>
+    <td class="left align-middle">{{ taxRate.category.name }}</td>
+    <td class="left align-middle">{{ taxRate.zone.name }}</td>
+    <td class="left align-middle">{{ taxRate.value }}%</td>
+    <td class="right align-middle">
+      <vdr-table-row-action
+        iconShape="edit"
+        [label]="'common.edit' | translate"
+        [linkTo]="['./', taxRate.id]"
+      ></vdr-table-row-action>
+    </td>
+    <td class="right align-middle">
+      <vdr-dropdown>
+        <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
+          {{ 'common.actions' | translate }}
+          <clr-icon shape="caret down"></clr-icon>
+        </button>
+        <vdr-dropdown-menu vdrPosition="bottom-right">
+          <button
+              type="button"
+              class="delete-button"
+              (click)="deleteTaxRate(taxRate)"
+              [disabled]="!(['DeleteSettings', 'DeleteTaxRate'] | hasPermission)"
+              vdrDropdownItem
+          >
+              <clr-icon shape="trash" class="is-danger"></clr-icon>
+              {{ 'common.delete' | translate }}
+          </button>
+        </vdr-dropdown-menu>
+      </vdr-dropdown>
+    </td>
+  </ng-template>
+</vdr-data-table>
+```
+
+```ts title="Signature"
+class DataTableComponent<T> implements AfterContentInit, OnChanges, OnInit, OnDestroy {
+    @Input() items: T[];
+    @Input() itemsPerPage: number;
+    @Input() currentPage: number;
+    @Input() totalItems: number;
+    @Input() emptyStateLabel: string;
+    @Input() selectionManager?: SelectionManager<T>;
+    @Output() pageChange = new EventEmitter<number>();
+    @Output() itemsPerPageChange = new EventEmitter<number>();
+    @Input() allSelected: boolean;
+    @Input() isRowSelectedFn: ((item: T) => boolean) | undefined;
+    @Output() allSelectChange = new EventEmitter<void>();
+    @Output() rowSelectChange = new EventEmitter<{ event: MouseEvent; item: T }>();
+    @ContentChildren(DataTableColumnComponent) columns: QueryList<DataTableColumnComponent>;
+    @ContentChildren(TemplateRef) templateRefs: QueryList<TemplateRef<any>>;
+    rowTemplate: TemplateRef<any>;
+    currentStart: number;
+    currentEnd: number;
+    disableSelect = false;
+    constructor(changeDetectorRef: ChangeDetectorRef)
+    ngOnInit() => ;
+    ngOnChanges(changes: SimpleChanges) => ;
+    ngOnDestroy() => ;
+    ngAfterContentInit() => void;
+    trackByFn(index: number, item: any) => ;
+    onToggleAllClick() => ;
+    onRowClick(item: T, event: MouseEvent) => ;
+}
+```
+* Implements: <code>AfterContentInit</code>, <code>OnChanges</code>, <code>OnInit</code>, <code>OnDestroy</code>
+
+
+
+<div className="members-wrapper">
+
+### items
+
+<MemberInfo kind="property" type={`T[]`}   />
+
+
+### itemsPerPage
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### currentPage
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### totalItems
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### emptyStateLabel
+
+<MemberInfo kind="property" type={`string`}   />
+
+
+### selectionManager
+
+<MemberInfo kind="property" type={`SelectionManager&#60;T&#62;`}   />
+
+
+### pageChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### itemsPerPageChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### allSelected
+
+<MemberInfo kind="property" type={`boolean`}   />
+
+
+### isRowSelectedFn
+
+<MemberInfo kind="property" type={`((item: T) =&#62; boolean) | undefined`}   />
+
+
+### allSelectChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### rowSelectChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### columns
+
+<MemberInfo kind="property" type={`QueryList&#60;DataTableColumnComponent&#62;`}   />
+
+
+### templateRefs
+
+<MemberInfo kind="property" type={`QueryList&#60;TemplateRef&#60;any&#62;&#62;`}   />
+
+
+### rowTemplate
+
+<MemberInfo kind="property" type={`TemplateRef&#60;any&#62;`}   />
+
+
+### currentStart
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### currentEnd
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### disableSelect
+
+<MemberInfo kind="property" type={``}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(changeDetectorRef: ChangeDetectorRef) => DataTableComponent`}   />
+
+
+### ngOnInit
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### ngOnChanges
+
+<MemberInfo kind="method" type={`(changes: SimpleChanges) => `}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### ngAfterContentInit
+
+<MemberInfo kind="method" type={`() => void`}   />
+
+
+### trackByFn
+
+<MemberInfo kind="method" type={`(index: number, item: any) => `}   />
+
+
+### onToggleAllClick
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### onRowClick
+
+<MemberInfo kind="method" type={`(item: T, event: MouseEvent) => `}   />
+
+
+
+
+</div>

+ 262 - 0
docs/docs/reference/admin-ui-api/components/datetime-picker-component.mdx

@@ -0,0 +1,262 @@
+---
+title: "DatetimePickerComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## DatetimePickerComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.ts" sourceLine="39" packageName="@vendure/admin-ui" />
+
+A form input for selecting datetime values.
+
+*Example*
+
+```html
+<vdr-datetime-picker [(ngModel)]="startDate"></vdr-datetime-picker>
+```
+
+```ts title="Signature"
+class DatetimePickerComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
+    @Input() yearRange;
+    @Input() weekStartDay: DayOfWeek = 'mon';
+    @Input() timeGranularityInterval = 5;
+    @Input() min: string | null = null;
+    @Input() max: string | null = null;
+    @Input() readonly = false;
+    @ViewChild('dropdownComponent', { static: true }) dropdownComponent: DropdownComponent;
+    @ViewChild('datetimeInput', { static: true }) datetimeInput: ElementRef<HTMLInputElement>;
+    @ViewChild('calendarTable') calendarTable: ElementRef<HTMLTableElement>;
+    disabled = false;
+    calendarView$: Observable<CalendarView>;
+    current$: Observable<CurrentView>;
+    selected$: Observable<Date | null>;
+    selectedHours$: Observable<number | null>;
+    selectedMinutes$: Observable<number | null>;
+    years: number[];
+    weekdays: string[] = [];
+    hours: number[];
+    minutes: number[];
+    constructor(changeDetectorRef: ChangeDetectorRef, datetimePickerService: DatetimePickerService)
+    ngOnInit() => ;
+    ngAfterViewInit() => void;
+    ngOnDestroy() => void;
+    registerOnChange(fn: any) => ;
+    registerOnTouched(fn: any) => ;
+    setDisabledState(isDisabled: boolean) => ;
+    writeValue(value: string | null) => ;
+    prevMonth() => ;
+    nextMonth() => ;
+    selectToday() => ;
+    setYear(event: Event) => ;
+    setMonth(event: Event) => ;
+    selectDay(day: DayCell) => ;
+    clearValue() => ;
+    handleCalendarKeydown(event: KeyboardEvent) => ;
+    setHour(event: Event) => ;
+    setMinute(event: Event) => ;
+    closeDatepicker() => ;
+}
+```
+* Implements: <code>ControlValueAccessor</code>, <code>AfterViewInit</code>, <code>OnInit</code>, <code>OnDestroy</code>
+
+
+
+<div className="members-wrapper">
+
+### yearRange
+
+<MemberInfo kind="property" type={``}   />
+
+The range above and below the current year which is selectable from
+the year select control. If a min or max value is set, these will
+override the yearRange.
+### weekStartDay
+
+<MemberInfo kind="property" type={`DayOfWeek`}   />
+
+The day that the week should start with in the calendar view.
+### timeGranularityInterval
+
+<MemberInfo kind="property" type={``}   />
+
+The granularity of the minutes time picker
+### min
+
+<MemberInfo kind="property" type={`string | null`}   />
+
+The minimum date as an ISO string
+### max
+
+<MemberInfo kind="property" type={`string | null`}   />
+
+The maximum date as an ISO string
+### readonly
+
+<MemberInfo kind="property" type={``}   />
+
+Sets the readonly state
+### dropdownComponent
+
+<MemberInfo kind="property" type={`<a href='/reference/admin-ui-api/components/dropdown-component#dropdowncomponent'>DropdownComponent</a>`}   />
+
+
+### datetimeInput
+
+<MemberInfo kind="property" type={`ElementRef&#60;HTMLInputElement&#62;`}   />
+
+
+### calendarTable
+
+<MemberInfo kind="property" type={`ElementRef&#60;HTMLTableElement&#62;`}   />
+
+
+### disabled
+
+<MemberInfo kind="property" type={``}   />
+
+
+### calendarView$
+
+<MemberInfo kind="property" type={`Observable&#60;CalendarView&#62;`}   />
+
+
+### current$
+
+<MemberInfo kind="property" type={`Observable&#60;CurrentView&#62;`}   />
+
+
+### selected$
+
+<MemberInfo kind="property" type={`Observable&#60;Date | null&#62;`}   />
+
+
+### selectedHours$
+
+<MemberInfo kind="property" type={`Observable&#60;number | null&#62;`}   />
+
+
+### selectedMinutes$
+
+<MemberInfo kind="property" type={`Observable&#60;number | null&#62;`}   />
+
+
+### years
+
+<MemberInfo kind="property" type={`number[]`}   />
+
+
+### weekdays
+
+<MemberInfo kind="property" type={`string[]`}   />
+
+
+### hours
+
+<MemberInfo kind="property" type={`number[]`}   />
+
+
+### minutes
+
+<MemberInfo kind="property" type={`number[]`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(changeDetectorRef: ChangeDetectorRef, datetimePickerService: DatetimePickerService) => DatetimePickerComponent`}   />
+
+
+### ngOnInit
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### ngAfterViewInit
+
+<MemberInfo kind="method" type={`() => void`}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => void`}   />
+
+
+### registerOnChange
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### registerOnTouched
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### setDisabledState
+
+<MemberInfo kind="method" type={`(isDisabled: boolean) => `}   />
+
+
+### writeValue
+
+<MemberInfo kind="method" type={`(value: string | null) => `}   />
+
+
+### prevMonth
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### nextMonth
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### selectToday
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### setYear
+
+<MemberInfo kind="method" type={`(event: Event) => `}   />
+
+
+### setMonth
+
+<MemberInfo kind="method" type={`(event: Event) => `}   />
+
+
+### selectDay
+
+<MemberInfo kind="method" type={`(day: DayCell) => `}   />
+
+
+### clearValue
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### handleCalendarKeydown
+
+<MemberInfo kind="method" type={`(event: KeyboardEvent) => `}   />
+
+
+### setHour
+
+<MemberInfo kind="method" type={`(event: Event) => `}   />
+
+
+### setMinute
+
+<MemberInfo kind="method" type={`(event: Event) => `}   />
+
+
+### closeDatepicker
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+
+
+</div>

+ 86 - 0
docs/docs/reference/admin-ui-api/components/dropdown-component.mdx

@@ -0,0 +1,86 @@
+---
+title: "DropdownComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## DropdownComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown.component.ts" sourceLine="28" packageName="@vendure/admin-ui" />
+
+Used for building dropdown menus.
+
+*Example*
+
+```html
+<vdr-dropdown>
+  <button class="btn btn-outline" vdrDropdownTrigger>
+      <clr-icon shape="plus"></clr-icon>
+      Select type
+  </button>
+  <vdr-dropdown-menu vdrPosition="bottom-left">
+    <button
+      *ngFor="let typeName of allTypes"
+      type="button"
+      vdrDropdownItem
+      (click)="selectType(typeName)"
+    >
+      typeName
+    </button>
+  </vdr-dropdown-menu>
+</vdr-dropdown>
+```
+
+```ts title="Signature"
+class DropdownComponent {
+    isOpen = false;
+    public trigger: ElementRef;
+    @Input() manualToggle = false;
+    onClick() => ;
+    toggleOpen() => ;
+    onOpenChange(callback: (isOpen: boolean) => void) => ;
+    setTriggerElement(elementRef: ElementRef) => ;
+}
+```
+
+<div className="members-wrapper">
+
+### isOpen
+
+<MemberInfo kind="property" type={``}   />
+
+
+### trigger
+
+<MemberInfo kind="property" type={`ElementRef`}   />
+
+
+### manualToggle
+
+<MemberInfo kind="property" type={``}   />
+
+
+### onClick
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### toggleOpen
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### onOpenChange
+
+<MemberInfo kind="method" type={`(callback: (isOpen: boolean) =&#62; void) => `}   />
+
+
+### setTriggerElement
+
+<MemberInfo kind="method" type={`(elementRef: ElementRef) => `}   />
+
+
+
+
+</div>

+ 155 - 0
docs/docs/reference/admin-ui-api/components/facet-value-selector-component.mdx

@@ -0,0 +1,155 @@
+---
+title: "FacetValueSelectorComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## FacetValueSelectorComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.ts" sourceLine="34" packageName="@vendure/admin-ui" />
+
+A form control for selecting facet values.
+
+*Example*
+
+```html
+<vdr-facet-value-selector
+  (selectedValuesChange)="selectedValues = $event"
+></vdr-facet-value-selector>
+```
+The `selectedValuesChange` event will emit an array of `FacetValue` objects.
+
+```ts title="Signature"
+class FacetValueSelectorComponent implements OnInit, OnDestroy, ControlValueAccessor {
+    @Output() selectedValuesChange = new EventEmitter<FacetValueFragment[]>();
+    @Input() readonly = false;
+    @Input() transformControlValueAccessorValue: (value: FacetValueFragment[]) => any[] = value => value;
+    searchInput$ = new Subject<string>();
+    searchLoading = false;
+    searchResults$: Observable<FacetValueFragment[]>;
+    selectedIds$ = new Subject<string[]>();
+    onChangeFn: (val: any) => void;
+    onTouchFn: () => void;
+    disabled = false;
+    value: Array<string | FacetValueFragment>;
+    constructor(dataService: DataService, changeDetectorRef: ChangeDetectorRef)
+    ngOnInit() => void;
+    ngOnDestroy() => ;
+    onChange(selected: FacetValueFragment[]) => ;
+    registerOnChange(fn: any) => ;
+    registerOnTouched(fn: any) => ;
+    setDisabledState(isDisabled: boolean) => void;
+    focus() => ;
+    writeValue(obj: string | FacetValueFragment[] | Array<string | number> | null) => void;
+}
+```
+* Implements: <code>OnInit</code>, <code>OnDestroy</code>, <code>ControlValueAccessor</code>
+
+
+
+<div className="members-wrapper">
+
+### selectedValuesChange
+
+<MemberInfo kind="property" type={``}   />
+
+
+### readonly
+
+<MemberInfo kind="property" type={``}   />
+
+
+### transformControlValueAccessorValue
+
+<MemberInfo kind="property" type={`(value: FacetValueFragment[]) =&#62; any[]`}   />
+
+
+### searchInput$
+
+<MemberInfo kind="property" type={``}   />
+
+
+### searchLoading
+
+<MemberInfo kind="property" type={``}   />
+
+
+### searchResults$
+
+<MemberInfo kind="property" type={`Observable&#60;FacetValueFragment[]&#62;`}   />
+
+
+### selectedIds$
+
+<MemberInfo kind="property" type={``}   />
+
+
+### onChangeFn
+
+<MemberInfo kind="property" type={`(val: any) =&#62; void`}   />
+
+
+### onTouchFn
+
+<MemberInfo kind="property" type={`() =&#62; void`}   />
+
+
+### disabled
+
+<MemberInfo kind="property" type={``}   />
+
+
+### value
+
+<MemberInfo kind="property" type={`Array&#60;string | FacetValueFragment&#62;`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef: ChangeDetectorRef) => FacetValueSelectorComponent`}   />
+
+
+### ngOnInit
+
+<MemberInfo kind="method" type={`() => void`}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### onChange
+
+<MemberInfo kind="method" type={`(selected: FacetValueFragment[]) => `}   />
+
+
+### registerOnChange
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### registerOnTouched
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### setDisabledState
+
+<MemberInfo kind="method" type={`(isDisabled: boolean) => void`}   />
+
+
+### focus
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### writeValue
+
+<MemberInfo kind="method" type={`(obj: string | FacetValueFragment[] | Array&#60;string | number&#62; | null) => void`}   />
+
+
+
+
+</div>

+ 86 - 0
docs/docs/reference/admin-ui-api/components/object-tree-component.mdx

@@ -0,0 +1,86 @@
+---
+title: "ObjectTreeComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ObjectTreeComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/object-tree/object-tree.component.ts" sourceLine="22" packageName="@vendure/admin-ui" />
+
+This component displays a plain JavaScript object as an expandable tree.
+
+*Example*
+
+```html
+<vdr-object-tree [value]="payment.metadata"></vdr-object-tree>
+```
+
+```ts title="Signature"
+class ObjectTreeComponent implements OnChanges {
+    @Input() value: { [key: string]: any } | string;
+    @Input() isArrayItem = false;
+    depth: number;
+    expanded: boolean;
+    valueIsArray: boolean;
+    entries: Array<{ key: string; value: any }>;
+    constructor(parent: ObjectTreeComponent)
+    ngOnChanges() => ;
+    isObject(value: any) => boolean;
+}
+```
+* Implements: <code>OnChanges</code>
+
+
+
+<div className="members-wrapper">
+
+### value
+
+<MemberInfo kind="property" type={`{ [key: string]: any } | string`}   />
+
+
+### isArrayItem
+
+<MemberInfo kind="property" type={``}   />
+
+
+### depth
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### expanded
+
+<MemberInfo kind="property" type={`boolean`}   />
+
+
+### valueIsArray
+
+<MemberInfo kind="property" type={`boolean`}   />
+
+
+### entries
+
+<MemberInfo kind="property" type={`Array&#60;{ key: string; value: any }&#62;`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(parent: <a href='/reference/admin-ui-api/components/object-tree-component#objecttreecomponent'>ObjectTreeComponent</a>) => ObjectTreeComponent`}   />
+
+
+### ngOnChanges
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### isObject
+
+<MemberInfo kind="method" type={`(value: any) => boolean`}   />
+
+
+
+
+</div>

+ 41 - 0
docs/docs/reference/admin-ui-api/components/order-state-label-component.mdx

@@ -0,0 +1,41 @@
+---
+title: "OrderStateLabelComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## OrderStateLabelComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.ts" sourceLine="13" packageName="@vendure/admin-ui" />
+
+Displays the state of an order in a colored chip.
+
+*Example*
+
+```html
+<vdr-order-state-label [state]="order.state"></vdr-order-state-label>
+```
+
+```ts title="Signature"
+class OrderStateLabelComponent {
+    @Input() state: string;
+    chipColorType: void
+}
+```
+
+<div className="members-wrapper">
+
+### state
+
+<MemberInfo kind="property" type={`string`}   />
+
+
+### chipColorType
+
+<MemberInfo kind="property" type={``}   />
+
+
+
+
+</div>

+ 75 - 0
docs/docs/reference/admin-ui-api/components/product-variant-selector-component.mdx

@@ -0,0 +1,75 @@
+---
+title: "ProductVariantSelectorComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ProductVariantSelectorComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/product-variant-selector/product-variant-selector.component.ts" sourceLine="21" packageName="@vendure/admin-ui" />
+
+A component for selecting product variants via an autocomplete-style select input.
+
+*Example*
+
+```html
+<vdr-product-variant-selector
+  (productSelected)="selectResult($event)"></vdr-product-variant-selector>
+```
+
+```ts title="Signature"
+class ProductVariantSelectorComponent implements OnInit {
+    searchInput$ = new Subject<string>();
+    searchLoading = false;
+    searchResults$: Observable<ProductSelectorSearchQuery['search']['items']>;
+    @Output() productSelected = new EventEmitter<ProductSelectorSearchQuery['search']['items'][number]>();
+    constructor(dataService: DataService)
+    ngOnInit() => void;
+    selectResult(product?: ProductSelectorSearchQuery['search']['items'][number]) => ;
+}
+```
+* Implements: <code>OnInit</code>
+
+
+
+<div className="members-wrapper">
+
+### searchInput$
+
+<MemberInfo kind="property" type={``}   />
+
+
+### searchLoading
+
+<MemberInfo kind="property" type={``}   />
+
+
+### searchResults$
+
+<MemberInfo kind="property" type={`Observable&#60;ProductSelectorSearchQuery['search']['items']&#62;`}   />
+
+
+### productSelected
+
+<MemberInfo kind="property" type={``}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>) => ProductVariantSelectorComponent`}   />
+
+
+### ngOnInit
+
+<MemberInfo kind="method" type={`() => void`}   />
+
+
+### selectResult
+
+<MemberInfo kind="method" type={`(product?: ProductSelectorSearchQuery['search']['items'][number]) => `}   />
+
+
+
+
+</div>

+ 108 - 0
docs/docs/reference/admin-ui-api/components/rich-text-editor-component.mdx

@@ -0,0 +1,108 @@
+---
+title: "RichTextEditorComponent"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## RichTextEditorComponent
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/rich-text-editor.component.ts" sourceLine="32" packageName="@vendure/admin-ui" />
+
+A rich text (HTML) editor based on Prosemirror (https://prosemirror.net/)
+
+*Example*
+
+```html
+<vdr-rich-text-editor
+    [(ngModel)]="description"
+    label="Description"
+></vdr-rich-text-editor>
+```
+
+```ts title="Signature"
+class RichTextEditorComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
+    @Input() label: string;
+    @HostBinding('class.readonly')
+    _readonly = false;
+    onChange: (val: any) => void;
+    onTouch: () => void;
+    constructor(changeDetector: ChangeDetectorRef, prosemirrorService: ProsemirrorService, viewContainerRef: ViewContainerRef, contextMenuService: ContextMenuService)
+    menuElement: HTMLDivElement | null
+    ngAfterViewInit() => ;
+    ngOnDestroy() => ;
+    registerOnChange(fn: any) => ;
+    registerOnTouched(fn: any) => ;
+    setDisabledState(isDisabled: boolean) => ;
+    writeValue(value: any) => ;
+}
+```
+* Implements: <code>ControlValueAccessor</code>, <code>AfterViewInit</code>, <code>OnDestroy</code>
+
+
+
+<div className="members-wrapper">
+
+### label
+
+<MemberInfo kind="property" type={`string`}   />
+
+
+### _readonly
+
+<MemberInfo kind="property" type={``}   />
+
+
+### onChange
+
+<MemberInfo kind="property" type={`(val: any) =&#62; void`}   />
+
+
+### onTouch
+
+<MemberInfo kind="property" type={`() =&#62; void`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(changeDetector: ChangeDetectorRef, prosemirrorService: ProsemirrorService, viewContainerRef: ViewContainerRef, contextMenuService: ContextMenuService) => RichTextEditorComponent`}   />
+
+
+### menuElement
+
+<MemberInfo kind="property" type={`HTMLDivElement | null`}   />
+
+
+### ngAfterViewInit
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### registerOnChange
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### registerOnTouched
+
+<MemberInfo kind="method" type={`(fn: any) => `}   />
+
+
+### setDisabledState
+
+<MemberInfo kind="method" type={`(isDisabled: boolean) => `}   />
+
+
+### writeValue
+
+<MemberInfo kind="method" type={`(value: any) => `}   />
+
+
+
+
+</div>

+ 41 - 0
docs/docs/reference/admin-ui-api/pipes/asset-preview-pipe.mdx

@@ -0,0 +1,41 @@
+---
+title: "AssetPreviewPipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## AssetPreviewPipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/asset-preview.pipe.ts" sourceLine="19" packageName="@vendure/admin-ui" />
+
+Given an Asset object (an object with `preview` and optionally `focalPoint` properties), this pipe
+returns a string with query parameters designed to work with the image resize capabilities of the
+AssetServerPlugin.
+
+*Example*
+
+```html
+<img [src]="asset | assetPreview:'tiny'" />
+<img [src]="asset | assetPreview:150" />
+```
+
+```ts title="Signature"
+class AssetPreviewPipe implements PipeTransform {
+    transform(asset?: AssetFragment, preset: string | number = 'thumb') => string;
+}
+```
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### transform
+
+<MemberInfo kind="method" type={`(asset?: AssetFragment, preset: string | number = 'thumb') => string`}   />
+
+
+
+
+</div>

+ 51 - 0
docs/docs/reference/admin-ui-api/pipes/has-permission-pipe.mdx

@@ -0,0 +1,51 @@
+---
+title: "HasPermissionPipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## HasPermissionPipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/has-permission.pipe.ts" sourceLine="16" packageName="@vendure/admin-ui" />
+
+A pipe which checks the provided permission against all the permissions of the current user.
+Returns `true` if the current user has that permission.
+
+*Example*
+
+```html
+<button [disabled]="!('UpdateCatalog' | hasPermission)">Save Changes</button>
+```
+
+```ts title="Signature"
+class HasPermissionPipe implements PipeTransform, OnDestroy {
+    constructor(permissionsService: PermissionsService, changeDetectorRef: ChangeDetectorRef)
+    transform(input: string | string[]) => any;
+    ngOnDestroy() => ;
+}
+```
+* Implements: <code>PipeTransform</code>, <code>OnDestroy</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(permissionsService: PermissionsService, changeDetectorRef: ChangeDetectorRef) => HasPermissionPipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(input: string | string[]) => any`}   />
+
+
+### ngOnDestroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+
+
+</div>

+ 47 - 0
docs/docs/reference/admin-ui-api/pipes/locale-currency-name-pipe.mdx

@@ -0,0 +1,47 @@
+---
+title: "LocaleCurrencyNamePipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## LocaleCurrencyNamePipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency-name.pipe.ts" sourceLine="18" packageName="@vendure/admin-ui" />
+
+Displays a human-readable name for a given ISO 4217 currency code.
+
+*Example*
+
+```html
+{{ order.currencyCode | localeCurrencyName }}
+```
+
+```ts title="Signature"
+class LocaleCurrencyNamePipe extends LocaleBasePipe implements PipeTransform {
+    constructor(dataService?: DataService, changeDetectorRef?: ChangeDetectorRef)
+    transform(value: any, display: 'full' | 'symbol' | 'name' = 'full', locale?: unknown) => any;
+}
+```
+* Extends: <code>LocaleBasePipe</code>
+
+
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService?: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef?: ChangeDetectorRef) => LocaleCurrencyNamePipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: any, display: 'full' | 'symbol' | 'name' = 'full', locale?: unknown) => any`}   />
+
+
+
+
+</div>

+ 54 - 0
docs/docs/reference/admin-ui-api/pipes/locale-currency-pipe.mdx

@@ -0,0 +1,54 @@
+---
+title: "LocaleCurrencyPipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## LocaleCurrencyPipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts" sourceLine="20" packageName="@vendure/admin-ui" />
+
+Formats a Vendure monetary value (in cents) into the correct format for the configured currency and display
+locale.
+
+*Example*
+
+```html
+{{ variant.priceWithTax | localeCurrency }}
+```
+
+```ts title="Signature"
+class LocaleCurrencyPipe extends LocaleBasePipe implements PipeTransform {
+    readonly precisionFactor: number;
+    constructor(currencyService: CurrencyService, dataService?: DataService, changeDetectorRef?: ChangeDetectorRef)
+    transform(value: unknown, args: unknown[]) => string | unknown;
+}
+```
+* Extends: <code>LocaleBasePipe</code>
+
+
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### precisionFactor
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### constructor
+
+<MemberInfo kind="method" type={`(currencyService: CurrencyService, dataService?: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef?: ChangeDetectorRef) => LocaleCurrencyPipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: unknown, args: unknown[]) => string | unknown`}   />
+
+
+
+
+</div>

+ 48 - 0
docs/docs/reference/admin-ui-api/pipes/locale-date-pipe.mdx

@@ -0,0 +1,48 @@
+---
+title: "LocaleDatePipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## LocaleDatePipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.ts" sourceLine="19" packageName="@vendure/admin-ui" />
+
+A replacement of the Angular DatePipe which makes use of the Intl API
+to format dates according to the selected UI language.
+
+*Example*
+
+```html
+{{ order.orderPlacedAt | localeDate }}
+```
+
+```ts title="Signature"
+class LocaleDatePipe extends LocaleBasePipe implements PipeTransform {
+    constructor(dataService?: DataService, changeDetectorRef?: ChangeDetectorRef)
+    transform(value: unknown, args: unknown[]) => unknown;
+}
+```
+* Extends: <code>LocaleBasePipe</code>
+
+
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService?: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef?: ChangeDetectorRef) => LocaleDatePipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: unknown, args: unknown[]) => unknown`}   />
+
+
+
+
+</div>

+ 47 - 0
docs/docs/reference/admin-ui-api/pipes/locale-language-name-pipe.mdx

@@ -0,0 +1,47 @@
+---
+title: "LocaleLanguageNamePipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## LocaleLanguageNamePipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/locale-language-name.pipe.ts" sourceLine="18" packageName="@vendure/admin-ui" />
+
+Displays a human-readable name for a given ISO 639-1 language code.
+
+*Example*
+
+```html
+{{ 'zh_Hant' | localeLanguageName }}
+```
+
+```ts title="Signature"
+class LocaleLanguageNamePipe extends LocaleBasePipe implements PipeTransform {
+    constructor(dataService?: DataService, changeDetectorRef?: ChangeDetectorRef)
+    transform(value: any, locale?: unknown) => string;
+}
+```
+* Extends: <code>LocaleBasePipe</code>
+
+
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService?: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef?: ChangeDetectorRef) => LocaleLanguageNamePipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: any, locale?: unknown) => string`}   />
+
+
+
+
+</div>

+ 47 - 0
docs/docs/reference/admin-ui-api/pipes/locale-region-name-pipe.mdx

@@ -0,0 +1,47 @@
+---
+title: "LocaleRegionNamePipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## LocaleRegionNamePipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.ts" sourceLine="18" packageName="@vendure/admin-ui" />
+
+Displays a human-readable name for a given region.
+
+*Example*
+
+```html
+{{ 'GB' | localeRegionName }}
+```
+
+```ts title="Signature"
+class LocaleRegionNamePipe extends LocaleBasePipe implements PipeTransform {
+    constructor(dataService?: DataService, changeDetectorRef?: ChangeDetectorRef)
+    transform(value: any, locale?: unknown) => string;
+}
+```
+* Extends: <code>LocaleBasePipe</code>
+
+
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(dataService?: <a href='/reference/admin-ui-api/services/data-service#dataservice'>DataService</a>, changeDetectorRef?: ChangeDetectorRef) => LocaleRegionNamePipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: any, locale?: unknown) => string`}   />
+
+
+
+
+</div>

+ 44 - 0
docs/docs/reference/admin-ui-api/pipes/time-ago-pipe.mdx

@@ -0,0 +1,44 @@
+---
+title: "TimeAgoPipe"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## TimeAgoPipe
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/pipes/time-ago.pipe.ts" sourceLine="18" packageName="@vendure/admin-ui" />
+
+Converts a date into the format "3 minutes ago", "5 hours ago" etc.
+
+*Example*
+
+```html
+{{ order.orderPlacedAt | timeAgo }}
+```
+
+```ts title="Signature"
+class TimeAgoPipe implements PipeTransform {
+    constructor(i18nService: I18nService)
+    transform(value: string | Date, nowVal?: string | Date) => string;
+}
+```
+* Implements: <code>PipeTransform</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(i18nService: <a href='/reference/typescript-api/common/i18n-service#i18nservice'>I18nService</a>) => TimeAgoPipe`}   />
+
+
+### transform
+
+<MemberInfo kind="method" type={`(value: string | Date, nowVal?: string | Date) => string`}   />
+
+
+
+
+</div>

+ 225 - 0
docs/docs/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy.mdx

@@ -0,0 +1,225 @@
+---
+title: "S3AssetStorageStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## S3AssetStorageStrategy
+
+<GenerationInfo sourceFile="packages/asset-server-plugin/src/config/s3-asset-storage-strategy.ts" sourceLine="155" packageName="@vendure/asset-server-plugin" />
+
+An <a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a> which uses [Amazon S3](https://aws.amazon.com/s3/) object storage service.
+To us this strategy you must first have access to an AWS account.
+See their [getting started guide](https://aws.amazon.com/s3/getting-started/) for how to get set up.
+
+Before using this strategy, make sure you have the `@aws-sdk/client-s3` and `@aws-sdk/lib-storage` package installed:
+
+```bash
+npm install @aws-sdk/client-s3 @aws-sdk/lib-storage
+```
+
+**Note:** Rather than instantiating this manually, use the <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#configures3assetstorage'>configureS3AssetStorage</a> function.
+
+## Use with S3-compatible services (MinIO)
+This strategy will also work with any S3-compatible object storage solutions, such as [MinIO](https://min.io/).
+See the <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#configures3assetstorage'>configureS3AssetStorage</a> for an example with MinIO.
+
+```ts title="Signature"
+class S3AssetStorageStrategy implements AssetStorageStrategy {
+    constructor(s3Config: S3Config, toAbsoluteUrl: (request: Request, identifier: string) => string)
+    init() => ;
+    destroy?: (() => void | Promise<void>) | undefined;
+    writeFileFromBuffer(fileName: string, data: Buffer) => ;
+    writeFileFromStream(fileName: string, data: Readable) => ;
+    readFileToBuffer(identifier: string) => ;
+    readFileToStream(identifier: string) => ;
+    deleteFile(identifier: string) => ;
+    fileExists(fileName: string) => ;
+}
+```
+* Implements: <code><a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a></code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(s3Config: <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#s3config'>S3Config</a>, toAbsoluteUrl: (request: Request, identifier: string) =&#62; string) => S3AssetStorageStrategy`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### destroy
+
+<MemberInfo kind="property" type={`(() =&#62; void | Promise&#60;void&#62;) | undefined`}   />
+
+
+### writeFileFromBuffer
+
+<MemberInfo kind="method" type={`(fileName: string, data: Buffer) => `}   />
+
+
+### writeFileFromStream
+
+<MemberInfo kind="method" type={`(fileName: string, data: Readable) => `}   />
+
+
+### readFileToBuffer
+
+<MemberInfo kind="method" type={`(identifier: string) => `}   />
+
+
+### readFileToStream
+
+<MemberInfo kind="method" type={`(identifier: string) => `}   />
+
+
+### deleteFile
+
+<MemberInfo kind="method" type={`(identifier: string) => `}   />
+
+
+### fileExists
+
+<MemberInfo kind="method" type={`(fileName: string) => `}   />
+
+
+
+
+</div>
+
+
+## S3Config
+
+<GenerationInfo sourceFile="packages/asset-server-plugin/src/config/s3-asset-storage-strategy.ts" sourceLine="19" packageName="@vendure/asset-server-plugin" />
+
+Configuration for connecting to AWS S3.
+
+```ts title="Signature"
+interface S3Config {
+    credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider;
+    bucket: string;
+    nativeS3Configuration?: any;
+    nativeS3UploadConfiguration?: any;
+}
+```
+
+<div className="members-wrapper">
+
+### credentials
+
+<MemberInfo kind="property" type={`AwsCredentialIdentity | AwsCredentialIdentityProvider`}   />
+
+The credentials used to access your s3 account. You can supply either the access key ID & secret, or you can make use of a
+[shared credentials file](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html)
+To use a shared credentials file, import the `fromIni()` function from the "@aws-sdk/credential-provider-ini" or "@aws-sdk/credential-providers" package and supply
+the profile name (e.g. `{ profile: 'default' }`) as its argument.
+### bucket
+
+<MemberInfo kind="property" type={`string`}   />
+
+The S3 bucket in which to store the assets. If it does not exist, it will be created on startup.
+### nativeS3Configuration
+
+<MemberInfo kind="property" type={`any`}   />
+
+Configuration object passed directly to the AWS SDK.
+S3.Types.ClientConfiguration can be used after importing aws-sdk.
+Using type `any` in order to avoid the need to include `aws-sdk` dependency in general.
+### nativeS3UploadConfiguration
+
+<MemberInfo kind="property" type={`any`}   />
+
+Configuration object passed directly to the AWS SDK.
+ManagedUpload.ManagedUploadOptions can be used after importing aws-sdk.
+Using type `any` in order to avoid the need to include `aws-sdk` dependency in general.
+
+
+</div>
+
+
+## configureS3AssetStorage
+
+<GenerationInfo sourceFile="packages/asset-server-plugin/src/config/s3-asset-storage-strategy.ts" sourceLine="119" packageName="@vendure/asset-server-plugin" />
+
+Returns a configured instance of the <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#s3assetstoragestrategy'>S3AssetStorageStrategy</a> which can then be passed to the <a href='/reference/core-plugins/asset-server-plugin/asset-server-options#assetserveroptions'>AssetServerOptions</a>`storageStrategyFactory` property.
+
+Before using this strategy, make sure you have the `@aws-sdk/client-s3` and `@aws-sdk/lib-storage` package installed:
+
+```bash
+npm install @aws-sdk/client-s3 @aws-sdk/lib-storage
+```
+
+*Example*
+
+```ts
+import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin';
+import { DefaultAssetNamingStrategy } from '@vendure/core';
+import { fromEnv } from '@aws-sdk/credential-providers';
+
+// ...
+
+plugins: [
+  AssetServerPlugin.init({
+    route: 'assets',
+    assetUploadDir: path.join(__dirname, 'assets'),
+    namingStrategy: new DefaultAssetNamingStrategy(),
+    storageStrategyFactory: configureS3AssetStorage({
+      bucket: 'my-s3-bucket',
+      credentials: fromEnv(), // or any other credential provider
+      nativeS3Configuration: {
+        region: process.env.AWS_REGION,
+      },
+    }),
+}),
+```
+
+## Usage with MinIO
+
+Reference: [How to use AWS SDK for Javascript with MinIO Server](https://docs.min.io/docs/how-to-use-aws-sdk-for-javascript-with-minio-server.html)
+
+*Example*
+
+```ts
+import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin';
+import { DefaultAssetNamingStrategy } from '@vendure/core';
+
+// ...
+
+plugins: [
+  AssetServerPlugin.init({
+    route: 'assets',
+    assetUploadDir: path.join(__dirname, 'assets'),
+    namingStrategy: new DefaultAssetNamingStrategy(),
+    storageStrategyFactory: configureS3AssetStorage({
+      bucket: 'my-minio-bucket',
+      credentials: {
+        accessKeyId: process.env.MINIO_ACCESS_KEY_ID,
+        secretAccessKey: process.env.MINIO_SECRET_ACCESS_KEY,
+      },
+      nativeS3Configuration: {
+        endpoint: process.env.MINIO_ENDPOINT ?? 'http://localhost:9000',
+        forcePathStyle: true,
+        signatureVersion: 'v4',
+        // The `region` is required by the AWS SDK even when using MinIO,
+        // so we just use a dummy value here.
+        region: 'eu-west-1',
+      },
+    }),
+}),
+```
+
+```ts title="Signature"
+function configureS3AssetStorage(s3Config: S3Config): void
+```
+Parameters
+
+### s3Config
+
+<MemberInfo kind="parameter" type={`<a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#s3config'>S3Config</a>`} />
+

+ 695 - 0
docs/docs/reference/core-plugins/elasticsearch-plugin/elasticsearch-options.mdx

@@ -0,0 +1,695 @@
+---
+title: "ElasticsearchOptions"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ElasticsearchOptions
+
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/options.ts" sourceLine="30" packageName="@vendure/elasticsearch-plugin" />
+
+Configuration options for the <a href='/reference/core-plugins/elasticsearch-plugin/#elasticsearchplugin'>ElasticsearchPlugin</a>.
+
+```ts title="Signature"
+interface ElasticsearchOptions {
+    host?: string;
+    port?: number;
+    connectionAttempts?: number;
+    connectionAttemptInterval?: number;
+    clientOptions?: ClientOptions;
+    indexPrefix?: string;
+    indexSettings?: object;
+    indexMappingProperties?: {
+        [indexName: string]: object;
+    };
+    reindexProductsChunkSize?: number;
+    reindexBulkOperationSizeLimit?: number;
+    searchConfig?: SearchConfig;
+    customProductMappings?: {
+        [fieldName: string]: CustomMapping<
+            [Product, ProductVariant[], LanguageCode, Injector, RequestContext]
+        >;
+    };
+    customProductVariantMappings?: {
+        [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode, Injector, RequestContext]>;
+    };
+    bufferUpdates?: boolean;
+    hydrateProductRelations?: Array<EntityRelationPaths<Product>>;
+    hydrateProductVariantRelations?: Array<EntityRelationPaths<ProductVariant>>;
+    extendSearchInputType?: {
+        [name: string]: PrimitiveTypeVariations<GraphQlPrimitive>;
+    };
+    extendSearchSortType?: string[];
+}
+```
+
+<div className="members-wrapper">
+
+### host
+
+<MemberInfo kind="property" type={`string`} default={`'http://localhost'`}   />
+
+The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
+### port
+
+<MemberInfo kind="property" type={`number`} default={`9200`}   />
+
+The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
+### connectionAttempts
+
+<MemberInfo kind="property" type={`number`} default={`10`}   />
+
+Maximum amount of attempts made to connect to the ElasticSearch server on startup.
+### connectionAttemptInterval
+
+<MemberInfo kind="property" type={`number`} default={`5000`}   />
+
+Interval in milliseconds between attempts to connect to the ElasticSearch server on startup.
+### clientOptions
+
+<MemberInfo kind="property" type={`ClientOptions`}   />
+
+Options to pass directly to the
+[Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
+set authentication or other more advanced options.
+Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
+### indexPrefix
+
+<MemberInfo kind="property" type={`string`} default={`'vendure-'`}   />
+
+Prefix for the indices created by the plugin.
+### indexSettings
+
+<MemberInfo kind="property" type={`object`} default={`{}`}  since="1.2.0"  />
+
+[These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
+are directly passed to index settings. To apply some settings indices will be recreated.
+
+*Example*
+
+```ts
+// Configuring an English stemmer
+indexSettings: {
+  analysis: {
+    analyzer: {
+      custom_analyzer: {
+        tokenizer: 'standard',
+        filter: [
+          'lowercase',
+          'english_stemmer'
+        ]
+      }
+    },
+    filter : {
+      english_stemmer : {
+        type : 'stemmer',
+        name : 'english'
+      }
+    }
+  }
+},
+```
+A more complete example can be found in the discussion thread
+[How to make elastic plugin to search by substring with stemming](https://github.com/vendurehq/vendure/discussions/1066).
+### indexMappingProperties
+
+<MemberInfo kind="property" type={`{         [indexName: string]: object;     }`} default={`{}`}  since="1.2.0"  />
+
+This option allow to redefine or define new properties in mapping. More about elastic
+[mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
+After changing this option indices will be recreated.
+
+*Example*
+
+```ts
+// Configuring custom analyzer for the `productName` field.
+indexMappingProperties: {
+  productName: {
+    type: 'text',
+    analyzer:'custom_analyzer',
+    fields: {
+      keyword: {
+        type: 'keyword',
+        ignore_above: 256,
+      }
+    }
+  }
+}
+```
+
+To reference a field defined by `customProductMappings` or `customProductVariantMappings`, you will
+need to prefix the name with `'product-<name>'` or `'variant-<name>'` respectively, e.g.:
+
+*Example*
+
+```ts
+customProductMappings: {
+   variantCount: {
+       graphQlType: 'Int!',
+       valueFn: (product, variants) => variants.length,
+   },
+},
+indexMappingProperties: {
+  'product-variantCount': {
+    type: 'integer',
+  }
+}
+```
+### reindexProductsChunkSize
+
+<MemberInfo kind="property" type={`number`} default={`2500`}  since="2.1.7"  />
+
+Products limit chunk size for each loop iteration when indexing products.
+### reindexBulkOperationSizeLimit
+
+<MemberInfo kind="property" type={`number`} default={`3000`}  since="2.1.7"  />
+
+Index operations are performed in bulk, with each bulk operation containing a number of individual
+index operations. This option sets the maximum number of operations in the memory buffer before a
+bulk operation is executed.
+### searchConfig
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/elasticsearch-plugin/elasticsearch-options#searchconfig'>SearchConfig</a>`}   />
+
+Configuration of the internal Elasticsearch query.
+### customProductMappings
+
+<MemberInfo kind="property" type={`{         [fieldName: string]: CustomMapping&#60;             [<a href='/reference/typescript-api/entities/product#product'>Product</a>, <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>[], <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]         &#62;;     }`}   />
+
+Custom mappings may be defined which will add the defined data to the
+Elasticsearch index and expose that data via the SearchResult GraphQL type,
+adding a new `customMappings`, `customProductMappings` & `customProductVariantMappings` fields.
+
+The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
+versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
+
+The `public` (default = `true`) property is used to reveal or hide the property in the GraphQL API schema.
+If this property is set to `false` it's not accessible in the `customMappings` field but it's still getting
+parsed to the elasticsearch index.
+
+This config option defines custom mappings which are accessible when the "groupByProduct" or "groupBySKU"
+input options is set to `true` (Do not set both to true at the same time). In addition, custom variant mappings can be accessed by using
+the `customProductVariantMappings` field, which is always available.
+
+*Example*
+
+```ts
+customProductMappings: {
+   variantCount: {
+       graphQlType: 'Int!',
+       valueFn: (product, variants) => variants.length,
+   },
+   reviewRating: {
+       graphQlType: 'Float',
+       public: true,
+       valueFn: product => (product.customFields as any).reviewRating,
+   },
+   priority: {
+       graphQlType: 'Int!',
+       public: false,
+       valueFn: product => (product.customFields as any).priority,
+   },
+}
+```
+
+*Example*
+
+```graphql
+query SearchProducts($input: SearchInput!) {
+    search(input: $input) {
+        totalItems
+        items {
+            productId
+            productName
+            customProductMappings {
+                variantCount
+                reviewRating
+            }
+            customMappings {
+                ...on CustomProductMappings {
+                    variantCount
+                    reviewRating
+                }
+            }
+        }
+    }
+}
+```
+### customProductVariantMappings
+
+<MemberInfo kind="property" type={`{         [fieldName: string]: CustomMapping&#60;[<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>, <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]&#62;;     }`}   />
+
+This config option defines custom mappings which are accessible when the "groupByProduct" and "groupBySKU"
+input options are both set to `false`. In addition, custom product mappings can be accessed by using
+the `customProductMappings` field, which is always available.
+
+*Example*
+
+```graphql
+query SearchProducts($input: SearchInput!) {
+    search(input: $input) {
+        totalItems
+        items {
+            productId
+            productName
+            customProductVariantMappings {
+                weight
+            }
+            customMappings {
+                ...on CustomProductVariantMappings {
+                    weight
+                }
+            }
+        }
+    }
+}
+```
+### bufferUpdates
+
+<MemberInfo kind="property" type={`boolean`} default={`false`}  since="1.3.0"  />
+
+If set to `true`, updates to Products, ProductVariants and Collections will not immediately
+trigger an update to the search index. Instead, all these changes will be buffered and will
+only be run via a call to the `runPendingSearchIndexUpdates` mutation in the Admin API.
+
+This is very useful for installations with a large number of ProductVariants and/or
+Collections, as the buffering allows better control over when these expensive jobs are run,
+and also performs optimizations to minimize the amount of work that needs to be performed by
+the worker.
+### hydrateProductRelations
+
+<MemberInfo kind="property" type={`Array&#60;<a href='/reference/typescript-api/common/entity-relation-paths#entityrelationpaths'>EntityRelationPaths</a>&#60;<a href='/reference/typescript-api/entities/product#product'>Product</a>&#62;&#62;`} default={`[]`}  since="1.3.0"  />
+
+Additional product relations that will be fetched from DB while reindexing. This can be used
+in combination with `customProductMappings` to ensure that the required relations are joined
+before the `product` object is passed to the `valueFn`.
+
+*Example*
+
+```ts
+{
+  hydrateProductRelations: ['assets.asset'],
+  customProductMappings: {
+    assetPreviews: {
+      graphQlType: '[String!]',
+      // Here we can be sure that the `product.assets` array is populated
+      // with an Asset object
+      valueFn: (product) => product.assets.map(a => a.asset.preview),
+    }
+  }
+}
+```
+### hydrateProductVariantRelations
+
+<MemberInfo kind="property" type={`Array&#60;<a href='/reference/typescript-api/common/entity-relation-paths#entityrelationpaths'>EntityRelationPaths</a>&#60;<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>&#62;&#62;`} default={`[]`}  since="1.3.0"  />
+
+Additional variant relations that will be fetched from DB while reindexing. See
+`hydrateProductRelations` for more explanation and a usage example.
+### extendSearchInputType
+
+<MemberInfo kind="property" type={`{         [name: string]: PrimitiveTypeVariations&#60;GraphQlPrimitive&#62;;     }`} default={`{}`}  since="1.3.0"  />
+
+Allows the `SearchInput` type to be extended with new input fields. This allows arbitrary
+data to be passed in, which can then be used e.g. in the `mapQuery()` function or
+custom `scriptFields` functions.
+
+*Example*
+
+```ts
+extendSearchInputType: {
+  longitude: 'Float',
+  latitude: 'Float',
+  radius: 'Float',
+}
+```
+
+This allows the search query to include these new fields:
+
+*Example*
+
+```graphql
+query {
+  search(input: {
+    longitude: 101.7117,
+    latitude: 3.1584,
+    radius: 50.00
+  }) {
+    items {
+      productName
+    }
+  }
+}
+```
+### extendSearchSortType
+
+<MemberInfo kind="property" type={`string[]`} default={`[]`}  since="1.4.0"  />
+
+Adds a list of sort parameters. This is mostly important to make the
+correct sort order values available inside `input` parameter of the `mapSort` option.
+
+*Example*
+
+```ts
+extendSearchSortType: ["distance"]
+```
+
+will extend the `SearchResultSortParameter` input type like this:
+
+*Example*
+
+```graphql
+extend input SearchResultSortParameter {
+     distance: SortOrder
+}
+```
+
+
+</div>
+
+
+## SearchConfig
+
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/options.ts" sourceLine="397" packageName="@vendure/elasticsearch-plugin" />
+
+Configuration options for the internal Elasticsearch query which is generated when performing a search.
+
+```ts title="Signature"
+interface SearchConfig {
+    facetValueMaxSize?: number;
+    collectionMaxSize?: number;
+    totalItemsMaxSize?: number | boolean;
+    multiMatchType?: 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix';
+    boostFields?: BoostFieldsConfig;
+    priceRangeBucketInterval?: number;
+    mapQuery?: (
+        query: any,
+        input: ElasticSearchInput,
+        searchConfig: DeepRequired<SearchConfig>,
+        channelId: ID,
+        enabledOnly: boolean,
+        ctx: RequestContext,
+    ) => any;
+    scriptFields?: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> };
+    mapSort?: (sort: ElasticSearchSortInput, input: ElasticSearchInput) => ElasticSearchSortInput;
+}
+```
+
+<div className="members-wrapper">
+
+### facetValueMaxSize
+
+<MemberInfo kind="property" type={`number`} default={`50`}   />
+
+The maximum number of FacetValues to return from the search query. Internally, this
+value sets the "size" property of an Elasticsearch aggregation.
+### collectionMaxSize
+
+<MemberInfo kind="property" type={`number`} default={`50`}  since="1.1.0"  />
+
+The maximum number of Collections to return from the search query. Internally, this
+value sets the "size" property of an Elasticsearch aggregation.
+### totalItemsMaxSize
+
+<MemberInfo kind="property" type={`number | boolean`} default={`10000`}  since="1.2.0"  />
+
+The maximum number of totalItems to return from the search query. Internally, this
+value sets the "track_total_hits" property of an Elasticsearch query.
+If this parameter is set to "True", accurate count of totalItems will be returned.
+If this parameter is set to "False", totalItems will be returned as 0.
+If this parameter is set to integer, accurate count of totalItems will be returned not bigger than integer.
+### multiMatchType
+
+<MemberInfo kind="property" type={`'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix'`} default={`'best_fields'`}   />
+
+Defines the
+[multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
+used when matching against a search term.
+### boostFields
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/elasticsearch-plugin/elasticsearch-options#boostfieldsconfig'>BoostFieldsConfig</a>`}   />
+
+Set custom boost values for particular fields when matching against a search term.
+### priceRangeBucketInterval
+
+<MemberInfo kind="property" type={`number`}   />
+
+The interval used to group search results into buckets according to price range. For example, setting this to
+`2000` will group into buckets every $20.00:
+
+```json
+{
+  "data": {
+    "search": {
+      "totalItems": 32,
+      "priceRange": {
+        "buckets": [
+          {
+            "to": 2000,
+            "count": 21
+          },
+          {
+            "to": 4000,
+            "count": 7
+          },
+          {
+            "to": 6000,
+            "count": 3
+          },
+          {
+            "to": 12000,
+            "count": 1
+          }
+        ]
+      }
+    }
+  }
+}
+```
+### mapQuery
+
+<MemberInfo kind="property" type={`(         query: any,         input: ElasticSearchInput,         searchConfig: DeepRequired&#60;<a href='/reference/core-plugins/elasticsearch-plugin/elasticsearch-options#searchconfig'>SearchConfig</a>&#62;,         channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>,         enabledOnly: boolean,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,     ) =&#62; any`}   />
+
+This config option allows the the modification of the whole (already built) search query. This allows
+for e.g. wildcard / fuzzy searches on the index.
+
+*Example*
+
+```ts
+mapQuery: (query, input, searchConfig, channelId, enabledOnly, ctx) => {
+  if (query.bool.must) {
+    delete query.bool.must;
+  }
+  query.bool.should = [
+    {
+      query_string: {
+        query: "*" + term + "*",
+        fields: [
+          `productName^${searchConfig.boostFields.productName}`,
+          `productVariantName^${searchConfig.boostFields.productVariantName}`,
+        ]
+      }
+    },
+    {
+      multi_match: {
+        query: term,
+        type: searchConfig.multiMatchType,
+        fields: [
+          `description^${searchConfig.boostFields.description}`,
+          `sku^${searchConfig.boostFields.sku}`,
+        ],
+      },
+    },
+  ];
+
+  return query;
+}
+```
+### scriptFields
+
+<MemberInfo kind="property" type={`{ [fieldName: string]: CustomScriptMapping&#60;[ElasticSearchInput]&#62; }`}  since="1.3.0"  />
+
+Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
+
+The script field definition consists of three properties:
+
+* `graphQlType`: This is the type that will be returned when this script field is queried
+via the GraphQL API. It may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
+versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
+* `context`: determines whether this script field is available when grouping by product. Can be
+`product`, `variant` or `both`.
+* `scriptFn`: This is the function to run on each hit. Should return an object with a `script` property,
+as covered in the
+[Elasticsearch script fields docs](https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-fields.html#script-fields)
+
+*Example*
+
+```ts
+extendSearchInputType: {
+  latitude: 'Float',
+  longitude: 'Float',
+},
+indexMappingProperties: {
+  // The `product-location` field corresponds to the `location` customProductMapping
+  // defined below. Here we specify that it would be index as a `geo_point` type,
+  // which will allow us to perform geo-spacial calculations on it in our script field.
+  'product-location': {
+    type: 'geo_point', // contains function arcDistance
+  },
+},
+customProductMappings: {
+  location: {
+    graphQlType: 'String',
+    valueFn: (product: Product) => {
+      // Assume that the Product entity has this customField defined
+      const custom = product.customFields.location;
+      return `${custom.latitude},${custom.longitude}`;
+    },
+  }
+},
+searchConfig: {
+  scriptFields: {
+    distance: {
+      graphQlType: 'Float!',
+      // Run this script only when grouping results by product
+      context: 'product',
+      scriptFn: (input) => {
+        // The SearchInput was extended with latitude and longitude
+        // via the `extendSearchInputType` option above.
+        const lat = input.latitude;
+        const lon = input.longitude;
+        return {
+          script: `doc['product-location'].arcDistance(${lat}, ${lon})`,
+        }
+      }
+    }
+  }
+}
+```
+### mapSort
+
+<MemberInfo kind="property" type={`(sort: ElasticSearchSortInput, input: ElasticSearchInput) =&#62; ElasticSearchSortInput`} default={`{}`}  since="1.4.0"  />
+
+Allows extending the `sort` input of the elasticsearch body as covered in
+[Elasticsearch sort docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
+
+The `sort` input parameter contains the ElasticSearchSortInput generated for the default sort parameters "name" and "price".
+If neither of those are applied it will be empty.
+
+*Example*
+
+```ts
+mapSort: (sort, input) => {
+    // Assuming `extendSearchSortType: ["priority"]`
+    // Assuming priority is never undefined
+    const { priority } = input.sort;
+    return [
+         ...sort,
+         {
+             // The `product-priority` field corresponds to the `priority` customProductMapping
+             // Depending on the index type, this field might require a
+             // more detailed input (example: 'productName.keyword')
+             ["product-priority"]: {
+                 order: priority === SortOrder.ASC ? 'asc' : 'desc'
+             }
+         }
+     ];
+}
+```
+
+A more generic example would be a sort function based on a product location like this:
+
+*Example*
+
+```ts
+extendSearchInputType: {
+  latitude: 'Float',
+  longitude: 'Float',
+},
+extendSearchSortType: ["distance"],
+indexMappingProperties: {
+  // The `product-location` field corresponds to the `location` customProductMapping
+  // defined below. Here we specify that it would be index as a `geo_point` type,
+  // which will allow us to perform geo-spacial calculations on it in our script field.
+  'product-location': {
+    type: 'geo_point',
+  },
+},
+customProductMappings: {
+  location: {
+    graphQlType: 'String',
+    valueFn: (product: Product) => {
+      // Assume that the Product entity has this customField defined
+      const custom = product.customFields.location;
+      return `${custom.latitude},${custom.longitude}`;
+    },
+  }
+},
+searchConfig: {
+     mapSort: (sort, input) => {
+         // Assuming distance is never undefined
+         const { distance } = input.sort;
+         return [
+             ...sort,
+             {
+                 ["_geo_distance"]: {
+                     "product-location": [
+                         input.longitude,
+                         input.latitude
+                     ],
+                     order: distance === SortOrder.ASC ? 'asc' : 'desc',
+                     unit: "km"
+                 }
+             }
+         ];
+     }
+}
+```
+
+
+</div>
+
+
+## BoostFieldsConfig
+
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/options.ts" sourceLine="683" packageName="@vendure/elasticsearch-plugin" />
+
+Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
+the scores of given fields when performing a search against a term.
+
+Boosting a field acts as a score multiplier for matches against that field.
+
+```ts title="Signature"
+interface BoostFieldsConfig {
+    productName?: number;
+    productVariantName?: number;
+    description?: number;
+    sku?: number;
+}
+```
+
+<div className="members-wrapper">
+
+### productName
+
+<MemberInfo kind="property" type={`number`} default={`1`}   />
+
+Defines the boost factor for the productName field.
+### productVariantName
+
+<MemberInfo kind="property" type={`number`} default={`1`}   />
+
+Defines the boost factor for the productVariantName field.
+### description
+
+<MemberInfo kind="property" type={`number`} default={`1`}   />
+
+Defines the boost factor for the description field.
+### sku
+
+<MemberInfo kind="property" type={`number`} default={`1`}   />
+
+Defines the boost factor for the sku field.
+
+
+</div>

+ 169 - 0
docs/docs/reference/core-plugins/harden-plugin/index.mdx

@@ -0,0 +1,169 @@
+---
+title: "HardenPlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## HardenPlugin
+
+<GenerationInfo sourceFile="packages/harden-plugin/src/harden.plugin.ts" sourceLine="146" packageName="@vendure/harden-plugin" />
+
+The HardenPlugin hardens the Shop and Admin GraphQL APIs against attacks and abuse.
+
+- It analyzes the complexity on incoming graphql queries and rejects queries that are too complex and
+  could be used to overload the resources of the server.
+- It disables dev-mode API features such as introspection and the GraphQL playground app.
+- It removes field name suggestions to prevent trial-and-error schema sniffing.
+
+It is a recommended plugin for all production configurations.
+
+## Installation
+
+`yarn add @vendure/harden-plugin`
+
+or
+
+`npm install @vendure/harden-plugin`
+
+Then add the `HardenPlugin`, calling the `.init()` method with <a href='/reference/core-plugins/harden-plugin/harden-plugin-options#hardenpluginoptions'>HardenPluginOptions</a>:
+
+*Example*
+
+```ts
+import { HardenPlugin } from '@vendure/harden-plugin';
+
+const config: VendureConfig = {
+  // Add an instance of the plugin to the plugins array
+  plugins: [
+     HardenPlugin.init({
+       maxQueryComplexity: 650,
+       apiMode: process.env.APP_ENV === 'dev' ? 'dev' : 'prod',
+     }),
+  ],
+};
+```
+
+## Setting the max query complexity
+
+The `maxQueryComplexity` option determines how complex a query can be. The complexity of a query relates to how many, and how
+deeply-nested are the fields being selected, and is intended to roughly correspond to the amount of server resources that would
+be required to resolve that query.
+
+The goal of this setting is to prevent attacks in which a malicious actor crafts a very complex query in order to overwhelm your
+server resources. Here's an example of a request which would likely overwhelm a Vendure server:
+
+```graphql
+query EvilQuery {
+  products {
+    items {
+      collections {
+        productVariants {
+          items {
+            product {
+              collections {
+                productVariants {
+                  items {
+                    product {
+                      variants {
+                        name
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+```
+
+This evil query has a complexity score of 2,443,203 - much greater than the default of 1,000!
+
+The complexity score is calculated by the [graphql-query-complexity library](https://www.npmjs.com/package/graphql-query-complexity),
+and by default uses the <a href='/reference/core-plugins/harden-plugin/default-vendure-complexity-estimator#defaultvendurecomplexityestimator'>defaultVendureComplexityEstimator</a>, which is tuned specifically to the Vendure Shop API.
+
+:::caution
+Note: By default, if the "take" argument is omitted from a list query (e.g. the `products` or `collections` query), a default factor of 1000 is applied.
+:::
+
+The optimal max complexity score will vary depending on:
+
+- The requirements of your storefront and other clients using the Shop API
+- The resources available to your server
+
+You should aim to set the maximum as low as possible while still being able to service all the requests required.
+This will take some manual tuning.
+While tuning the max, you can turn on the `logComplexityScore` to get a detailed breakdown of the complexity of each query, as well as how
+that total score is derived from its child fields:
+
+*Example*
+
+```ts
+import { HardenPlugin } from '@vendure/harden-plugin';
+
+const config: VendureConfig = {
+  // A detailed summary is logged at the "debug" level
+  logger: new DefaultLogger({ level: LogLevel.Debug }),
+  plugins: [
+     HardenPlugin.init({
+       maxQueryComplexity: 650,
+       logComplexityScore: true,
+     }),
+  ],
+};
+```
+
+With logging configured as above, the following query:
+
+```graphql
+query ProductList {
+  products(options: { take: 5 }) {
+    items {
+      id
+      name
+      featuredAsset {
+        preview
+      }
+    }
+  }
+}
+```
+will log the following breakdown:
+
+```bash
+debug 16/12/22, 14:12 - [HardenPlugin] Calculating complexity of [ProductList]
+debug 16/12/22, 14:12 - [HardenPlugin] Product.id: ID!     childComplexity: 0, score: 1
+debug 16/12/22, 14:12 - [HardenPlugin] Product.name: String!       childComplexity: 0, score: 1
+debug 16/12/22, 14:12 - [HardenPlugin] Asset.preview: String!      childComplexity: 0, score: 1
+debug 16/12/22, 14:12 - [HardenPlugin] Product.featuredAsset: Asset        childComplexity: 1, score: 2
+debug 16/12/22, 14:12 - [HardenPlugin] ProductList.items: [Product!]!      childComplexity: 4, score: 20
+debug 16/12/22, 14:12 - [HardenPlugin] Query.products: ProductList!        childComplexity: 20, score: 35
+verbose 16/12/22, 14:12 - [HardenPlugin] Query complexity [ProductList]: 35
+```
+
+```ts title="Signature"
+class HardenPlugin {
+    static options: HardenPluginOptions;
+    init(options: HardenPluginOptions) => ;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/harden-plugin/harden-plugin-options#hardenpluginoptions'>HardenPluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/core-plugins/harden-plugin/harden-plugin-options#hardenpluginoptions'>HardenPluginOptions</a>) => `}   />
+
+
+
+
+</div>

+ 211 - 0
docs/docs/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin.mdx

@@ -0,0 +1,211 @@
+---
+title: "BullMQJobQueuePlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## BullMQJobQueuePlugin
+
+<GenerationInfo sourceFile="packages/job-queue-plugin/src/bullmq/plugin.ts" sourceLine="190" packageName="@vendure/job-queue-plugin" />
+
+This plugin is a drop-in replacement of the DefaultJobQueuePlugin, which implements a push-based
+job queue strategy built on top of the popular [BullMQ](https://github.com/taskforcesh/bullmq) library.
+
+## Advantages over the DefaultJobQueuePlugin
+
+The advantage of this approach is that jobs are stored in Redis rather than in the database. For more complex
+applications with many job queues and/or multiple worker instances, this can massively reduce the load on the
+DB server. The reason for this is that the DefaultJobQueuePlugin uses polling to check for new jobs. By default
+it will poll every 200ms. A typical Vendure instance uses at least 3 queues (handling emails, collections, search index),
+so even with a single worker instance this results in 15 queries per second to the DB constantly. Adding more
+custom queues and multiple worker instances can easily result in 50 or 100 queries per second. At this point
+performance may be impacted.
+
+Using this plugin, no polling is needed, as BullMQ will _push_ jobs to the worker(s) as and when they are added
+to the queue. This results in significantly more scalable performance characteristics, as well as lower latency
+in processing jobs.
+
+## Installation
+
+Note: To use this plugin, you need to manually install the `bullmq` package:
+
+```bash
+npm install bullmq@^5.4.2
+```
+
+**Note:** The v1.x version of this plugin is designed to work with bullmq v1.x, etc.
+
+*Example*
+
+```ts
+import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
+
+const config: VendureConfig = {
+  // Add an instance of the plugin to the plugins array
+  plugins: [
+    // DefaultJobQueuePlugin should be removed from the plugins array
+    BullMQJobQueuePlugin.init({
+      connection: {
+        port: 6379
+      }
+    }),
+  ],
+};
+```
+
+### Running Redis locally
+
+To develop with this plugin, you'll need an instance of Redis to connect to. Here's a docker-compose config
+that will set up [Redis](https://redis.io/) as well as [Redis Commander](https://github.com/joeferner/redis-commander),
+which is a web-based UI for interacting with Redis:
+
+```yaml
+version: "3"
+services:
+  redis:
+    image: redis:7.4
+    hostname: redis
+    container_name: redis
+    ports:
+      - "6379:6379"
+  redis-commander:
+    container_name: redis-commander
+    hostname: redis-commander
+    image: rediscommander/redis-commander:latest
+    environment:
+      - REDIS_HOSTS=local:redis:6379
+    ports:
+      - "8085:8081"
+```
+
+## Concurrency
+
+The default concurrency of a single worker is 3, i.e. up to 3 jobs will be processed at the same time.
+You can change the concurrency in the `workerOptions` passed to the `init()` method:
+
+*Example*
+
+```ts
+const config: VendureConfig = {
+  plugins: [
+    BullMQJobQueuePlugin.init({
+      workerOptions: {
+        concurrency: 10,
+      },
+    }),
+  ],
+};
+```
+
+## Removing old jobs
+
+By default, BullMQ will keep completed jobs in the `completed` set and failed jobs in the `failed` set. Over time,
+these sets can grow very large. Since Vendure v2.1, the default behaviour is to remove jobs from these sets after
+30 days or after a maximum of 5,000 completed or failed jobs.
+
+This can be configured using the `removeOnComplete` and `removeOnFail` options:
+
+*Example*
+
+```ts
+const config: VendureConfig = {
+  plugins: [
+    BullMQJobQueuePlugin.init({
+      workerOptions: {
+        removeOnComplete: {
+          count: 500,
+        },
+        removeOnFail: {
+          age: 60 * 60 * 24 * 7, // 7 days
+          count: 1000,
+        },
+      }
+    }),
+  ],
+};
+```
+
+The `count` option specifies the maximum number of jobs to keep in the set, while the `age` option specifies the
+maximum age of a job in seconds. If both options are specified, then the jobs kept will be the ones that satisfy
+both properties.
+
+## Job Priority
+Some jobs are more important than others. For example, sending out a timely email after a customer places an order
+is probably more important than a routine data import task. Sometimes you can get the situation where lower-priority
+jobs are blocking higher-priority jobs.
+
+Let's say you have a data import job that runs periodically and takes a long time to complete. If you have a high-priority
+job that needs to be processed quickly, it could be stuck behind the data import job in the queue. A customer might
+not get their confirmation email for 30 minutes while that data import job is processed!
+
+To solve this problem, you can set the `priority` option on a job. Jobs with a higher priority will be processed before
+jobs with a lower priority. By default, all jobs have a priority of 0 (which is the highest).
+
+Learn more about how priority works in BullMQ in their [documentation](https://docs.bullmq.io/guide/jobs/prioritized).
+
+You can set the priority by using the `setJobOptions` option (introduced in Vendure v3.2.0):
+
+*Example*
+
+```ts
+const config: VendureConfig = {
+  plugins: [
+    BullMQJobQueuePlugin.init({
+      setJobOptions: (queueName, job) => {
+        let priority = 10;
+        switch (queueName) {
+          case 'super-critical-task':
+            priority = 0;
+            break;
+          case 'send-email':
+            priority = 5;
+            break;
+          default:
+            priority = 10;
+        }
+        return { priority };
+      }
+    }),
+  ],
+};
+```
+
+## Setting Redis Prefix
+
+By default, the underlying BullMQ library will use the default Redis key prefix of `bull`. This can be changed by setting the `prefix` option
+in the `queueOptions` and `workerOptions` objects:
+
+```ts
+BullMQJobQueuePlugin.init({
+  workerOptions: {
+    prefix: 'my-prefix'
+  },
+  queueOptions: {
+    prefix: 'my-prefix'
+  }
+}),
+```
+
+```ts title="Signature"
+class BullMQJobQueuePlugin {
+    static options: BullMQPluginOptions;
+    init(options: BullMQPluginOptions) => ;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/job-queue-plugin/bull-mqplugin-options#bullmqpluginoptions'>BullMQPluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/core-plugins/job-queue-plugin/bull-mqplugin-options#bullmqpluginoptions'>BullMQPluginOptions</a>) => `}   />
+
+Configures the plugin.
+
+
+</div>

+ 93 - 0
docs/docs/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-strategy.mdx

@@ -0,0 +1,93 @@
+---
+title: "BullMQJobQueueStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## BullMQJobQueueStrategy
+
+<GenerationInfo sourceFile="packages/job-queue-plugin/src/bullmq/bullmq-job-queue-strategy.ts" sourceLine="53" packageName="@vendure/job-queue-plugin" />
+
+This JobQueueStrategy uses [BullMQ](https://docs.bullmq.io/) to implement a push-based job queue
+on top of Redis. It should not be used alone, but as part of the <a href='/reference/core-plugins/job-queue-plugin/bull-mqjob-queue-plugin#bullmqjobqueueplugin'>BullMQJobQueuePlugin</a>.
+
+Note: To use this strategy, you need to manually install the `bullmq` package:
+
+```bash
+npm install bullmq@^5.4.2
+```
+
+```ts title="Signature"
+class BullMQJobQueueStrategy implements InspectableJobQueueStrategy {
+    init(injector: Injector) => Promise<void>;
+    destroy() => ;
+    add(job: Job<Data>) => Promise<Job<Data>>;
+    cancelJob(jobId: string) => Promise<Job | undefined>;
+    findMany(options?: JobListOptions) => Promise<PaginatedList<Job>>;
+    findManyById(ids: ID[]) => Promise<Job[]>;
+    findOne(id: ID) => Promise<Job | undefined>;
+    removeSettledJobs(queueNames?: string[], olderThan?: Date) => Promise<number>;
+    start(queueName: string, process: (job: Job<Data>) => Promise<any>) => Promise<void>;
+    stop(queueName: string, process: (job: Job<Data>) => Promise<any>) => Promise<void>;
+}
+```
+* Implements: <code>InspectableJobQueueStrategy</code>
+
+
+
+<div className="members-wrapper">
+
+### init
+
+<MemberInfo kind="method" type={`(injector: Injector) => Promise&#60;void&#62;`}   />
+
+
+### destroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### add
+
+<MemberInfo kind="method" type={`(job: Job&#60;Data&#62;) => Promise&#60;Job&#60;Data&#62;&#62;`}   />
+
+
+### cancelJob
+
+<MemberInfo kind="method" type={`(jobId: string) => Promise&#60;Job | undefined&#62;`}   />
+
+
+### findMany
+
+<MemberInfo kind="method" type={`(options?: JobListOptions) => Promise&#60;PaginatedList&#60;Job&#62;&#62;`}   />
+
+
+### findManyById
+
+<MemberInfo kind="method" type={`(ids: ID[]) => Promise&#60;Job[]&#62;`}   />
+
+
+### findOne
+
+<MemberInfo kind="method" type={`(id: ID) => Promise&#60;Job | undefined&#62;`}   />
+
+
+### removeSettledJobs
+
+<MemberInfo kind="method" type={`(queueNames?: string[], olderThan?: Date) => Promise&#60;number&#62;`}   />
+
+
+### start
+
+<MemberInfo kind="method" type={`(queueName: string, process: (job: Job&#60;Data&#62;) =&#62; Promise&#60;any&#62;) => Promise&#60;void&#62;`}   />
+
+
+### stop
+
+<MemberInfo kind="method" type={`(queueName: string, process: (job: Job&#60;Data&#62;) =&#62; Promise&#60;any&#62;) => Promise&#60;void&#62;`}   />
+
+
+
+
+</div>

+ 65 - 0
docs/docs/reference/core-plugins/job-queue-plugin/pub-sub-job-queue-strategy.mdx

@@ -0,0 +1,65 @@
+---
+title: "PubSubJobQueueStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## PubSubJobQueueStrategy
+
+<GenerationInfo sourceFile="packages/job-queue-plugin/src/pub-sub/pub-sub-job-queue-strategy.ts" sourceLine="29" packageName="@vendure/job-queue-plugin" />
+
+This JobQueueStrategy uses Google Cloud Pub/Sub to implement a job queue for Vendure.
+It should not be used alone, but as part of the <a href='/reference/core-plugins/job-queue-plugin/pub-sub-plugin#pubsubplugin'>PubSubPlugin</a>.
+
+Note: To use this strategy, you need to manually install the `@google-cloud/pubsub` package:
+
+```bash
+npm install
+
+```ts title="Signature"
+class PubSubJobQueueStrategy extends InjectableJobQueueStrategy implements JobQueueStrategy {
+    init(injector: Injector) => ;
+    destroy() => ;
+    add(job: Job<Data>) => Promise<Job<Data>>;
+    start(queueName: string, process: (job: Job<Data>) => Promise<any>) => ;
+    stop(queueName: string, process: (job: Job<Data>) => Promise<any>) => ;
+}
+```
+* Extends: <code>InjectableJobQueueStrategy</code>
+
+
+* Implements: <code>JobQueueStrategy</code>
+
+
+
+<div className="members-wrapper">
+
+### init
+
+<MemberInfo kind="method" type={`(injector: Injector) => `}   />
+
+
+### destroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### add
+
+<MemberInfo kind="method" type={`(job: Job&#60;Data&#62;) => Promise&#60;Job&#60;Data&#62;&#62;`}   />
+
+
+### start
+
+<MemberInfo kind="method" type={`(queueName: string, process: (job: Job&#60;Data&#62;) =&#62; Promise&#60;any&#62;) => `}   />
+
+
+### stop
+
+<MemberInfo kind="method" type={`(queueName: string, process: (job: Job&#60;Data&#62;) =&#62; Promise&#60;any&#62;) => `}   />
+
+
+
+
+</div>

+ 36 - 0
docs/docs/reference/core-plugins/job-queue-plugin/pub-sub-plugin.mdx

@@ -0,0 +1,36 @@
+---
+title: "PubSubPlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## PubSubPlugin
+
+<GenerationInfo sourceFile="packages/job-queue-plugin/src/pub-sub/plugin.ts" sourceLine="22" packageName="@vendure/job-queue-plugin" />
+
+This plugin uses Google Cloud Pub/Sub to implement a job queue strategy for Vendure.
+
+## Installation
+
+Note: To use this plugin, you need to manually install the `@google-cloud/pubsub` package:
+
+```bash
+npm install
+
+```ts title="Signature"
+class PubSubPlugin {
+    init(options: PubSubOptions) => Type<PubSubPlugin>;
+}
+```
+
+<div className="members-wrapper">
+
+### init
+
+<MemberInfo kind="method" type={`(options: PubSubOptions) => Type&#60;<a href='/reference/core-plugins/job-queue-plugin/pub-sub-plugin#pubsubplugin'>PubSubPlugin</a>&#62;`}   />
+
+
+
+
+</div>

+ 350 - 0
docs/docs/reference/core-plugins/payments-plugin/braintree-plugin.mdx

@@ -0,0 +1,350 @@
+---
+title: "BraintreePlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## BraintreePlugin
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/braintree/braintree.plugin.ts" sourceLine="241" packageName="@vendure/payments-plugin" />
+
+This plugin enables payments to be processed by [Braintree](https://www.braintreepayments.com/), a popular payment provider.
+
+## Requirements
+
+1. You will need to create a Braintree sandbox account as outlined in https://developers.braintreepayments.com/start/overview.
+2. Then install `braintree` and `@types/braintree` from npm. This plugin was written with `v3.x` of the Braintree lib.
+    ```bash
+    yarn add @vendure/payments-plugin braintree
+    yarn add -D @types/braintree
+    ```
+    or
+    ```bash
+    npm install @vendure/payments-plugin braintree
+    npm install -D @types/braintree
+    ```
+
+## Setup
+
+1. Add the plugin to your VendureConfig `plugins` array:
+    ```ts
+    import { BraintreePlugin } from '@vendure/payments-plugin/package/braintree';
+    import { Environment } from 'braintree';
+
+    // ...
+
+    plugins: [
+      BraintreePlugin.init({
+        environment: Environment.Sandbox,
+        // This allows saving customer payment
+        // methods with Braintree (see "vaulting"
+        // section below for details)
+        storeCustomersInBraintree: true,
+      }),
+    ]
+    ```
+2. Create a new PaymentMethod in the Admin UI, and select "Braintree payments" as the handler.
+2. Fill in the `Merchant ID`, `Public Key` & `Private Key` from your Braintree sandbox account.
+
+## Storefront usage
+
+The plugin is designed to work with the [Braintree drop-in UI](https://developers.braintreepayments.com/guides/drop-in/overview/javascript/v3).
+This is a library provided by Braintree which will handle the payment UI for you. You can install it in your storefront project
+with:
+
+```bash
+yarn add braintree-web-drop-in
+# or
+npm install braintree-web-drop-in
+```
+
+The high-level workflow is:
+1. Generate a "client token" on the server by executing the `generateBraintreeClientToken` mutation which is exposed by this plugin.
+2. Use this client token to instantiate the Braintree Dropin UI.
+3. Listen for the `"paymentMethodRequestable"` event which emitted by the Dropin.
+4. Use the Dropin's `requestPaymentMethod()` method to get the required payment metadata.
+5. Pass that metadata to the `addPaymentToOrder` mutation. The metadata should be an object of type `{ nonce: string; }`
+
+Here is an example of how your storefront code will look. Note that this example is attempting to
+be framework-agnostic, so you'll need to adapt it to fit to your framework of choice.
+
+```ts
+// The Braintree Dropin instance
+let dropin: import('braintree-web-drop-in').Dropin;
+
+// Used to show/hide a "submit" button, which would be bound to the
+// `submitPayment()` method below.
+let showSubmitButton = false;
+
+// Used to display a "processing..." spinner
+let processing = false;
+
+//
+// This method would be invoked when the payment screen is mounted/created.
+//
+async function renderDropin(order: Order, clientToken: string) {
+  // Lazy load braintree dropin because it has a reference
+  // to `window` which breaks SSR
+  dropin = await import('braintree-web-drop-in').then((module) =>
+    module.default.create({
+      authorization: clientToken,
+      // This assumes a div in your view with the corresponding ID
+      container: '#dropin-container',
+      card: {
+        cardholderName: {
+            required: true,
+        },
+        overrides: {},
+      },
+      // Additional config is passed here depending on
+      // which payment methods you have enabled in your
+      // Braintree account.
+      paypal: {
+        flow: 'checkout',
+        amount: order.totalWithTax / 100,
+        currency: 'GBP',
+      },
+    }),
+  );
+
+  // If you are using the `storeCustomersInBraintree` option, then the
+  // customer might already have a stored payment method selected as
+  // soon as the dropin script loads. In this case, show the submit
+  // button immediately.
+  if (dropin.isPaymentMethodRequestable()) {
+    showSubmitButton = true;
+  }
+
+  dropin.on('paymentMethodRequestable', (payload) => {
+    if (payload.type === 'CreditCard') {
+      showSubmitButton = true;
+    }
+    if (payload.type === 'PayPalAccount') {
+      this.submitPayment();
+    }
+  });
+
+  dropin.on('noPaymentMethodRequestable', () => {
+    // Display an error
+  });
+}
+
+async function generateClientToken() {
+  const { generateBraintreeClientToken } = await graphQlClient.query(gql`
+    query GenerateBraintreeClientToken {
+      generateBraintreeClientToken
+    }
+  `);
+  return generateBraintreeClientToken;
+}
+
+async submitPayment() {
+  if (!dropin.isPaymentMethodRequestable()) {
+    return;
+  }
+  showSubmitButton = false;
+  processing = true;
+
+  const paymentResult = await dropin.requestPaymentMethod();
+
+  const { addPaymentToOrder } = await graphQlClient.query(gql`
+    mutation AddPayment($input: PaymentInput!) {
+      addPaymentToOrder(input: $input) {
+        ... on Order {
+          id
+          payments {
+            id
+            amount
+            errorMessage
+            method
+            state
+            transactionId
+            createdAt
+          }
+        }
+        ... on ErrorResult {
+          errorCode
+          message
+        }
+      }
+    }`, {
+      input: {
+        method: 'braintree', // The code of you Braintree PaymentMethod
+        metadata: paymentResult,
+      },
+    },
+  );
+
+  switch (addPaymentToOrder?.__typename) {
+      case 'Order':
+          // Adding payment succeeded!
+          break;
+      case 'OrderStateTransitionError':
+      case 'OrderPaymentStateError':
+      case 'PaymentDeclinedError':
+      case 'PaymentFailedError':
+        // Display an error to the customer
+        dropin.clearSelectedPaymentMethod();
+  }
+}
+```
+
+## Storing payment details (vaulting)
+
+Braintree has a [vault feature](https://developer.paypal.com/braintree/articles/control-panel/vault/overview) which allows the secure storage
+of customer's payment information. Using the vault allows you to offer a faster checkout for repeat customers without needing to worry about
+how to securely store payment details.
+
+To enable this feature, set the `storeCustomersInBraintree` option to `true`.
+
+```ts
+BraintreePlugin.init({
+  environment: Environment.Sandbox,
+  storeCustomersInBraintree: true,
+}),
+```
+
+Since v1.8, it is possible to override vaulting on a per-payment basis by passing `includeCustomerId: false` to the `generateBraintreeClientToken`
+mutation:
+
+```graphql
+const { generateBraintreeClientToken } = await graphQlClient.query(gql`
+  query GenerateBraintreeClientToken($includeCustomerId: Boolean) {
+    generateBraintreeClientToken(includeCustomerId: $includeCustomerId)
+  }
+`, { includeCustomerId: false });
+```
+
+as well as in the metadata of the `addPaymentToOrder` mutation:
+
+```ts
+const { addPaymentToOrder } = await graphQlClient.query(gql`
+  mutation AddPayment($input: PaymentInput!) {
+    addPaymentToOrder(input: $input) {
+      ...Order
+      ...ErrorResult
+    }
+  }`, {
+    input: {
+      method: 'braintree',
+      metadata: {
+        ...paymentResult,
+        includeCustomerId: false,
+      },
+    }
+  );
+```
+
+```ts title="Signature"
+class BraintreePlugin {
+    static options: BraintreePluginOptions = {};
+    init(options: BraintreePluginOptions) => Type<BraintreePlugin>;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/payments-plugin/braintree-plugin#braintreepluginoptions'>BraintreePluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/core-plugins/payments-plugin/braintree-plugin#braintreepluginoptions'>BraintreePluginOptions</a>) => Type&#60;<a href='/reference/core-plugins/payments-plugin/braintree-plugin#braintreeplugin'>BraintreePlugin</a>&#62;`}   />
+
+
+
+
+</div>
+
+
+## BraintreePluginOptions
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/braintree/types.ts" sourceLine="25" packageName="@vendure/payments-plugin" />
+
+Options for the Braintree plugin.
+
+```ts title="Signature"
+interface BraintreePluginOptions {
+    environment?: Environment;
+    storeCustomersInBraintree?: boolean;
+    extractMetadata?: (transaction: Transaction) => PaymentMetadata;
+}
+```
+
+<div className="members-wrapper">
+
+### environment
+
+<MemberInfo kind="property" type={`Environment`} default={`Environment.Sandbox`}   />
+
+The Braintree environment being targeted, e.g. sandbox or production.
+### storeCustomersInBraintree
+
+<MemberInfo kind="property" type={`boolean`} default={`false`}   />
+
+If set to `true`, a [Customer](https://developer.paypal.com/braintree/docs/guides/customers) object
+will be created in Braintree, which allows the secure storage ("vaulting") of previously-used payment methods.
+This is done by adding a custom field to the Customer entity to store the Braintree customer ID,
+so switching this on will require a database migration / synchronization.
+
+Since v1.8, it is possible to override vaulting on a per-payment basis by passing `includeCustomerId: false` to the
+`generateBraintreeClientToken` mutation.
+### extractMetadata
+
+<MemberInfo kind="property" type={`(transaction: <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a>) =&#62; PaymentMetadata`}  since="1.7.0"  />
+
+Allows you to configure exactly what information from the Braintree
+[Transaction object](https://developer.paypal.com/braintree/docs/reference/response/transaction#result-object) (which is returned by the
+`transaction.sale()` method of the SDK) should be persisted to the resulting Payment entity metadata.
+
+By default, the built-in extraction function will return a metadata object that looks like this:
+
+*Example*
+
+```ts
+const metadata = {
+  "status": "settling",
+  "currencyIsoCode": "GBP",
+  "merchantAccountId": "my_account_id",
+  "cvvCheck": "Not Applicable",
+  "avsPostCodeCheck": "Not Applicable",
+  "avsStreetAddressCheck": "Not Applicable",
+  "processorAuthorizationCode": null,
+  "processorResponseText": "Approved",
+  // for Paypal payments
+  "paymentMethod": "paypal_account",
+  "paypalData": {
+    "payerEmail": "michael-buyer@paypalsandbox.com",
+    "paymentId": "PAYID-MLCXYNI74301746XK8807043",
+    "authorizationId": "3BU93594D85624939",
+    "payerStatus": "VERIFIED",
+    "sellerProtectionStatus": "ELIGIBLE",
+    "transactionFeeAmount": "0.54"
+  },
+  // for credit card payments
+  "paymentMethod": "credit_card",
+  "cardData": {
+    "cardType": "MasterCard",
+    "last4": "5454",
+    "expirationDate": "02/2023"
+  }
+  // publicly-available metadata that will be
+  // readable from the Shop API
+  "public": {
+    "cardData": {
+      "cardType": "MasterCard",
+      "last4": "5454",
+      "expirationDate": "02/2023"
+    },
+    "paypalData": {
+      "authorizationId": "3BU93594D85624939",
+    }
+  }
+}
+```
+
+
+</div>

+ 209 - 0
docs/docs/reference/core-plugins/payments-plugin/mollie-plugin.mdx

@@ -0,0 +1,209 @@
+---
+title: "MolliePlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## MolliePlugin
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/mollie/mollie.plugin.ts" sourceLine="195" packageName="@vendure/payments-plugin" />
+
+Plugin to enable payments through the [Mollie platform](https://docs.mollie.com/).
+This plugin uses the Order API from Mollie, not the Payments API.
+
+## Requirements
+
+1. You will need to create a Mollie account and get your apiKey in the dashboard.
+2. Install the Payments plugin and the Mollie client:
+
+    `yarn add @vendure/payments-plugin @mollie/api-client`
+
+    or
+
+    `npm install @vendure/payments-plugin @mollie/api-client`
+
+## Setup
+
+1. Add the plugin to your VendureConfig `plugins` array:
+    ```ts
+    import { MolliePlugin } from '@vendure/payments-plugin/package/mollie';
+
+    // ...
+
+    plugins: [
+      MolliePlugin.init({ vendureHost: 'https://yourhost.io/' }),
+    ]
+    ```
+2. Create a new PaymentMethod in the Admin UI, and select "Mollie payments" as the handler.
+3. Set your Mollie apiKey in the `API Key` field.
+4. Set the `Fallback redirectUrl` to the url that the customer should be redirected to after completing the payment.
+You can override this url by passing the `redirectUrl` as an argument to the `createMolliePaymentIntent` mutation.
+
+## Storefront usage
+
+In your storefront you add a payment to an order using the `createMolliePaymentIntent` mutation. In this example, our Mollie
+PaymentMethod was given the code "mollie-payment-method". The `redirectUrl``is the url that is used to redirect the end-user
+back to your storefront after completing the payment.
+
+```graphql
+mutation CreateMolliePaymentIntent {
+  createMolliePaymentIntent(input: {
+    redirectUrl: "https://storefront/order/1234XYZ" // Optional, the fallback redirect url set in the admin UI will be used if not provided
+    paymentMethodCode: "mollie-payment-method" // Optional, the first method with Mollie as handler will be used if not provided
+    molliePaymentMethodCode: "ideal", // Optional argument to skip the method selection in the hosted checkout
+    locale: "nl_NL", // Optional, the browser language will be used by Mollie if not provided
+    immediateCapture: true, // Optional, default is true, set to false if you expect the order fulfillment to take longer than 24 hours
+  }) {
+         ... on MolliePaymentIntent {
+              url
+          }
+         ... on MolliePaymentIntentError {
+              errorCode
+              message
+         }
+  }
+}
+```
+
+The response will contain
+a redirectUrl, which can be used to redirect your customer to the Mollie
+platform.
+
+'molliePaymentMethodCode' is an optional parameter that can be passed to skip Mollie's hosted payment method selection screen
+You can get available Mollie payment methods with the following query:
+
+```graphql
+{
+ molliePaymentMethods(input: { paymentMethodCode: "mollie-payment-method" }) {
+   id
+   code
+   description
+   minimumAmount {
+     value
+     currency
+   }
+   maximumAmount {
+     value
+     currency
+   }
+   image {
+     size1x
+     size2x
+     svg
+   }
+ }
+}
+```
+
+After completing payment on the Mollie platform,
+the user is redirected to the redirect url that was provided in the `createMolliePaymentIntent` mutation, e.g. `https://storefront/order/CH234X5`
+
+## Pay later methods
+
+Mollie supports pay-later methods like 'Klarna Pay Later'. Pay-later methods are captured immediately after payment.
+
+If your order fulfillment time is longer than 24 hours You should pass `immediateCapture=false` to the `createMolliePaymentIntent` mutation.
+This will transition your order to 'PaymentAuthorized' after the Mollie hosted checkout.
+You need to manually capture the payment after the order is fulfilled, by settling existing payments, either via the admin UI or in custom code.
+
+Make sure to capture a payment within 28 days, after that the payment will be automaticallreleased.
+See the [Mollie documentation](https://docs.mollie.com/docs/place-a-hold-for-a-payment#authorization-expiration-window)
+for more information.
+
+## ArrangingAdditionalPayment state
+
+In some rare cases, a customer can add items to the active order, while a Mollie checkout is still open,
+for example by opening your storefront in another browser tab.
+This could result in an order being in `ArrangingAdditionalPayment` status after the customer finished payment.
+You should check if there is still an active order with status `ArrangingAdditionalPayment` on your order confirmation page,
+and if so, allow your customer to pay for the additional items by creating another Mollie payment.
+
+```ts title="Signature"
+class MolliePlugin {
+    static options: MolliePluginOptions;
+    init(options: MolliePluginOptions) => typeof MolliePlugin;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/payments-plugin/mollie-plugin#molliepluginoptions'>MolliePluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/core-plugins/payments-plugin/mollie-plugin#molliepluginoptions'>MolliePluginOptions</a>) => typeof <a href='/reference/core-plugins/payments-plugin/mollie-plugin#mollieplugin'>MolliePlugin</a>`}   />
+
+Initialize the mollie payment plugin
+
+
+</div>
+
+
+## MolliePluginOptions
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/mollie/mollie.plugin.ts" sourceLine="28" packageName="@vendure/payments-plugin" />
+
+Configuration options for the Mollie payments plugin.
+
+```ts title="Signature"
+interface MolliePluginOptions {
+    vendureHost: string;
+    enabledPaymentMethodsParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order | null,
+    ) => AdditionalEnabledPaymentMethodsParams | Promise<AdditionalEnabledPaymentMethodsParams>;
+}
+```
+
+<div className="members-wrapper">
+
+### vendureHost
+
+<MemberInfo kind="property" type={`string`}   />
+
+The host of your Vendure server, e.g. `'https://my-vendure.io'`.
+This is used by Mollie to send webhook events to the Vendure server
+### enabledPaymentMethodsParams
+
+<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a> | null,     ) =&#62; AdditionalEnabledPaymentMethodsParams | Promise&#60;AdditionalEnabledPaymentMethodsParams&#62;`}  since="2.2.0"  />
+
+Provide additional parameters to the Mollie enabled payment methods API call. By default,
+the plugin will already pass the `resource` parameter.
+
+For example, if you want to provide a `locale` and `billingCountry` for the API call, you can do so like this:
+
+**Note:** The `order` argument is possibly `null`, this could happen when you fetch the available payment methods
+before the order is created.
+
+*Example*
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { MolliePlugin, getLocale } from '@vendure/payments-plugin/package/mollie';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    MolliePlugin.init({
+      enabledPaymentMethodsParams: (injector, ctx, order) => {
+        const locale = order?.billingAddress?.countryCode
+            ? getLocale(order.billingAddress.countryCode, ctx.languageCode)
+            : undefined;
+
+        return {
+          locale,
+          billingCountry: order?.billingAddress?.countryCode,
+        },
+      }
+    }),
+  ],
+};
+```
+
+
+</div>

+ 355 - 0
docs/docs/reference/core-plugins/payments-plugin/stripe-plugin.mdx

@@ -0,0 +1,355 @@
+---
+title: "StripePlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## StripePlugin
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/stripe/stripe.plugin.ts" sourceLine="160" packageName="@vendure/payments-plugin" />
+
+Plugin to enable payments through [Stripe](https://stripe.com/docs) via the Payment Intents API.
+
+## Requirements
+
+1. You will need to create a Stripe account and get your secret key in the dashboard.
+2. Create a webhook endpoint in the Stripe dashboard (Developers -> Webhooks, "Add an endpoint") which listens to the `payment_intent.succeeded`
+and `payment_intent.payment_failed` events. The URL should be `https://my-server.com/payments/stripe`, where
+`my-server.com` is the host of your Vendure server. *Note:* for local development, you'll need to use
+the Stripe CLI to test your webhook locally. See the _local development_ section below.
+3. Get the signing secret for the newly created webhook.
+4. Install the Payments plugin and the Stripe Node library:
+
+    `yarn add @vendure/payments-plugin stripe`
+
+    or
+
+    `npm install @vendure/payments-plugin stripe`
+
+## Setup
+
+1. Add the plugin to your VendureConfig `plugins` array:
+    ```ts
+    import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+    // ...
+
+    plugins: [
+      StripePlugin.init({
+        // This prevents different customers from using the same PaymentIntent
+        storeCustomersInStripe: true,
+      }),
+    ]
+    ````
+    For all the plugin options, see the <a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripepluginoptions'>StripePluginOptions</a> type.
+2. Create a new PaymentMethod in the Admin UI, and select "Stripe payments" as the handler.
+3. Set the webhook secret and API key in the PaymentMethod form.
+
+## Storefront usage
+
+The plugin is designed to work with the [Custom payment flow](https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements).
+In this flow, Stripe provides libraries which handle the payment UI and confirmation for you. You can install it in your storefront project
+with:
+
+```bash
+yarn add @stripe/stripe-js
+# or
+npm install @stripe/stripe-js
+```
+
+If you are using React, you should also consider installing `@stripe/react-stripe-js`, which is a wrapper around Stripe Elements.
+
+The high-level workflow is:
+1. Create a "payment intent" on the server by executing the `createStripePaymentIntent` mutation which is exposed by this plugin.
+2. Use the returned client secret to instantiate the Stripe Payment Element:
+   ```ts
+   import { Elements } from '@stripe/react-stripe-js';
+   import { loadStripe, Stripe } from '@stripe/stripe-js';
+   import { CheckoutForm } from './CheckoutForm';
+
+   const stripePromise = getStripe('pk_test_....wr83u');
+
+   type StripePaymentsProps = {
+     clientSecret: string;
+     orderCode: string;
+   }
+
+   export function StripePayments({ clientSecret, orderCode }: StripePaymentsProps) {
+     const options = {
+       // passing the client secret obtained from the server
+       clientSecret,
+     }
+     return (
+       <Elements stripe={stripePromise} options={options}>
+         <CheckoutForm orderCode={orderCode} />
+       </Elements>
+     );
+   }
+   ```
+   ```ts
+   // CheckoutForm.tsx
+   import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
+   import { FormEvent } from 'react';
+
+   export const CheckoutForm = ({ orderCode }: { orderCode: string }) => {
+     const stripe = useStripe();
+     const elements = useElements();
+
+     const handleSubmit = async (event: FormEvent) => {
+       // We don't want to let default form submission happen here,
+       // which would refresh the page.
+       event.preventDefault();
+
+       if (!stripe || !elements) {
+         // Stripe.js has not yet loaded.
+         // Make sure to disable form submission until Stripe.js has loaded.
+         return;
+       }
+
+       const result = await stripe.confirmPayment({
+         //`Elements` instance that was used to create the Payment Element
+         elements,
+         confirmParams: {
+           return_url: location.origin + `/checkout/confirmation/${orderCode}`,
+         },
+       });
+
+       if (result.error) {
+         // Show error to your customer (for example, payment details incomplete)
+         console.log(result.error.message);
+       } else {
+         // Your customer will be redirected to your `return_url`. For some payment
+         // methods like iDEAL, your customer will be redirected to an intermediate
+         // site first to authorize the payment, then redirected to the `return_url`.
+       }
+     };
+
+     return (
+       <form onSubmit={handleSubmit}>
+         <PaymentElement />
+         <button disabled={!stripe}>Submit</button>
+       </form>
+     );
+   };
+   ```
+3. Once the form is submitted and Stripe processes the payment, the webhook takes care of updating the order without additional action
+in the storefront. As in the code above, the customer will be redirected to `/checkout/confirmation/${orderCode}`.
+
+:::info
+A full working storefront example of the Stripe integration can be found in the
+[Remix Starter repo](https://github.com/vendurehq/storefront-remix-starter/tree/master/app/components/checkout/stripe)
+:::
+
+## Local development
+
+1. Download & install the Stripe CLI: https://stripe.com/docs/stripe-cli
+2. From your Stripe dashboard, go to Developers -> Webhooks and click "Add an endpoint" and follow the instructions
+under "Test in a local environment".
+3. The Stripe CLI command will look like
+   ```bash
+   stripe listen --forward-to localhost:3000/payments/stripe
+   ```
+4. The Stripe CLI will create a webhook signing secret you can then use in your config of the StripePlugin.
+
+```ts title="Signature"
+class StripePlugin {
+    static options: StripePluginOptions;
+    init(options: StripePluginOptions) => Type<StripePlugin>;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripepluginoptions'>StripePluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripepluginoptions'>StripePluginOptions</a>) => Type&#60;<a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripeplugin'>StripePlugin</a>&#62;`}   />
+
+Initialize the Stripe payment plugin
+
+
+</div>
+
+
+## StripePluginOptions
+
+<GenerationInfo sourceFile="packages/payments-plugin/src/stripe/types.ts" sourceLine="29" packageName="@vendure/payments-plugin" />
+
+Configuration options for the Stripe payments plugin.
+
+```ts title="Signature"
+interface StripePluginOptions {
+    storeCustomersInStripe?: boolean;
+    metadata?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => Stripe.MetadataParam | Promise<Stripe.MetadataParam>;
+    paymentIntentCreateParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => AdditionalPaymentIntentCreateParams | Promise<AdditionalPaymentIntentCreateParams>;
+    requestOptions?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => AdditionalRequestOptions | Promise<AdditionalRequestOptions>;
+    customerCreateParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => AdditionalCustomerCreateParams | Promise<AdditionalCustomerCreateParams>;
+    skipPaymentIntentsWithoutExpectedMetadata?: boolean;
+}
+```
+
+<div className="members-wrapper">
+
+### storeCustomersInStripe
+
+<MemberInfo kind="property" type={`boolean`} default={`false`}   />
+
+If set to `true`, a [Customer](https://stripe.com/docs/api/customers) object will be created in Stripe - if
+it doesn't already exist - for authenticated users, which prevents payment methods attached to other Customers
+to be used with the same PaymentIntent. This is done by adding a custom field to the Customer entity to store
+the Stripe customer ID, so switching this on will require a database migration / synchronization.
+### metadata
+
+<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; Stripe.MetadataParam | Promise&#60;Stripe.MetadataParam&#62;`}  since="1.9.7"  />
+
+Attach extra metadata to Stripe payment intent creation call.
+
+*Example*
+
+```ts
+import { EntityHydrator, VendureConfig } from '@vendure/core';
+import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    StripePlugin.init({
+      metadata: async (injector, ctx, order) => {
+        const hydrator = injector.get(EntityHydrator);
+        await hydrator.hydrate(ctx, order, { relations: ['customer'] });
+        return {
+          description: `Order #${order.code} for ${order.customer!.emailAddress}`
+        },
+      }
+    }),
+  ],
+};
+```
+
+Note: If the `paymentIntentCreateParams` is also used and returns a `metadata` key, then the values
+returned by both functions will be merged.
+### paymentIntentCreateParams
+
+<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; AdditionalPaymentIntentCreateParams | Promise&#60;AdditionalPaymentIntentCreateParams&#62;`}  since="2.1.0"  />
+
+Provide additional parameters to the Stripe payment intent creation. By default,
+the plugin will already pass the `amount`, `currency`, `customer` and `automatic_payment_methods: { enabled: true }` parameters.
+
+For example, if you want to provide a `description` for the payment intent, you can do so like this:
+
+*Example*
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    StripePlugin.init({
+      paymentIntentCreateParams: (injector, ctx, order) => {
+        return {
+          description: `Order #${order.code} for ${order.customer?.emailAddress}`
+        },
+      }
+    }),
+  ],
+};
+```
+### requestOptions
+
+<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; AdditionalRequestOptions | Promise&#60;AdditionalRequestOptions&#62;`}  since="3.1.0"  />
+
+Provide additional options to the Stripe payment intent creation. By default,
+the plugin will already pass the `idempotencyKey` parameter.
+
+For example, if you want to provide a `stripeAccount` for the payment intent, you can do so like this:
+
+*Example*
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    StripePlugin.init({
+      requestOptions: (injector, ctx, order) => {
+        return {
+          stripeAccount: ctx.channel.seller?.customFields.connectedAccountId
+        },
+      }
+    }),
+  ],
+};
+```
+### customerCreateParams
+
+<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; AdditionalCustomerCreateParams | Promise&#60;AdditionalCustomerCreateParams&#62;`}  since="2.1.0"  />
+
+Provide additional parameters to the Stripe customer creation. By default,
+the plugin will already pass the `email` and `name` parameters.
+
+For example, if you want to provide an address for the customer:
+
+*Example*
+
+```ts
+import { EntityHydrator, VendureConfig } from '@vendure/core';
+import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    StripePlugin.init({
+      storeCustomersInStripe: true,
+      customerCreateParams: async (injector, ctx, order) => {
+        const entityHydrator = injector.get(EntityHydrator);
+        const customer = order.customer;
+        await entityHydrator.hydrate(ctx, customer, { relations: ['addresses'] });
+        const defaultBillingAddress = customer.addresses.find(a => a.defaultBillingAddress) ?? customer.addresses[0];
+        return {
+          address: {
+              line1: defaultBillingAddress.streetLine1 || order.shippingAddress?.streetLine1,
+              postal_code: defaultBillingAddress.postalCode || order.shippingAddress?.postalCode,
+              city: defaultBillingAddress.city || order.shippingAddress?.city,
+              state: defaultBillingAddress.province || order.shippingAddress?.province,
+              country: defaultBillingAddress.country.code || order.shippingAddress?.countryCode,
+          },
+        },
+      }
+    }),
+  ],
+};
+```
+### skipPaymentIntentsWithoutExpectedMetadata
+
+<MemberInfo kind="property" type={`boolean`}   />
+
+If your Stripe account also generates payment intents which are independent of Vendure orders, you can set this
+to `true` to skip processing those payment intents.
+
+
+</div>

+ 158 - 0
docs/docs/reference/core-plugins/sentry-plugin/index.mdx

@@ -0,0 +1,158 @@
+---
+title: "SentryPlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## SentryPlugin
+
+<GenerationInfo sourceFile="packages/sentry-plugin/src/sentry-plugin.ts" sourceLine="146" packageName="@vendure/sentry-plugin" />
+
+This plugin integrates the [Sentry](https://sentry.io) error tracking & performance monitoring
+service with your Vendure server. In addition to capturing errors, it also provides built-in
+support for [tracing](https://docs.sentry.io/product/sentry-basics/concepts/tracing/) as well as
+enriching your Sentry events with additional context about the request.
+
+:::info
+This documentation applies from v3.5.0 of the plugin, which works differently to previous
+versions. Documentation for prior versions can
+be found [here](https://github.com/vendurehq/vendure/blob/1bb9cf8ca1584bce026ccc82f33f866b766ef47d/packages/sentry-plugin/src/sentry-plugin.ts).
+:::
+
+## Pre-requisites
+
+This plugin depends on access to Sentry, which can be self-hosted or used as a cloud service.
+
+If using the hosted SaaS option, you must have a Sentry account and a project set up ([sign up here](https://sentry.io/signup/)). When setting up your project,
+select the "Node.js" platform and no framework.
+
+Once set up, you will be given a [Data Source Name (DSN)](https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/)
+which you will need to provide to the plugin.
+
+## Installation
+
+```bash
+npm install --save @vendure/sentry-plugin
+```
+
+## Environment Variables
+
+The following environment variables are used to control how the Sentry
+integration behaves:
+
+- `SENTRY_DSN`: (required) Sentry Data Source Name
+- `SENTRY_TRACES_SAMPLE_RATE`: Number between 0 and 1
+- `SENTRY_PROFILES_SAMPLE_RATE`: Number between 0 and 1
+- `SENTRY_ENABLE_LOGS`: Boolean. Captures calls to the console API as logs in Sentry. Default `false`
+- `SENTRY_CAPTURE_LOG_LEVELS`: 'debug' | 'info' | 'warn' | 'error' | 'log' | 'assert' | 'trace'
+
+## Configuration
+
+Setting up the Sentry plugin requires two steps:
+
+### Step 1: Preload the Sentry instrument file
+
+Make sure the `SENTRY_DSN` environment variable is defined.
+
+The Sentry SDK must be initialized before your application starts. This is done by preloading
+the instrument file when starting your Vendure server:
+
+```bash
+node --import @vendure/sentry-plugin/instrument ./dist/index.js
+```
+
+Or if using TypeScript directly with tsx:
+
+```bash
+tsx --import @vendure/sentry-plugin/instrument ./src/index.ts
+```
+
+### Step 2: Add the SentryPlugin to your Vendure config
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { SentryPlugin } from '@vendure/sentry-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        // ...
+        // highlight-start
+        SentryPlugin.init({
+            // Optional configuration
+            includeErrorTestMutation: true,
+        }),
+        // highlight-end
+    ],
+};
+```
+
+## Tracing
+
+This plugin includes built-in support for [tracing](https://docs.sentry.io/product/sentry-basics/concepts/tracing/), which allows you to see the performance of your.
+To enable tracing, preload the instrument file as described in [Step 1](#step-1-preload-the-sentry-instrument-file).
+This ensures that the Sentry SDK is initialized before any other code is executed.
+
+You can also set the `tracesSampleRate` and `profilesSampleRate` options to control the sample rate for
+tracing and profiling, with the following environment variables:
+
+- `SENTRY_TRACES_SAMPLE_RATE`
+- `SENTRY_PROFILES_SAMPLE_RATE`
+
+The sample rate for tracing should be between 0 and 1. The sample rate for profiling should be between 0 and 1.
+
+By default, both are set to `undefined`, which means that tracing and profiling are disabled.
+
+## Instrumenting your own code
+
+You may want to add your own custom spans to your code. To do so, you can use the `Sentry` object
+from the `@sentry/node` package. For example:
+
+```ts
+import * as Sentry from "@sentry/node";
+
+export class MyService {
+    async myMethod() {
+         Sentry.setContext('My Custom Context,{
+             key: 'value',
+         });
+    }
+}
+```
+
+## Error test mutation
+
+To test whether your Sentry configuration is working correctly, you can set the `includeErrorTestMutation` option to `true`. This will add a mutation to the Admin API
+which will throw an error of the type specified in the `errorType` argument. For example:
+
+```graphql
+mutation CreateTestError {
+    createTestError(errorType: DATABASE_ERROR)
+}
+```
+
+You should then be able to see the error in your Sentry dashboard (it may take a couple of minutes to appear).
+
+```ts title="Signature"
+class SentryPlugin {
+    static options: SentryPluginOptions = {} as any;
+    init(options?: SentryPluginOptions) => ;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/core-plugins/sentry-plugin/sentry-plugin-options#sentrypluginoptions'>SentryPluginOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options?: <a href='/reference/core-plugins/sentry-plugin/sentry-plugin-options#sentrypluginoptions'>SentryPluginOptions</a>) => `}   />
+
+
+
+
+</div>

+ 97 - 0
docs/docs/reference/typescript-api/auth/authentication-strategy.mdx

@@ -0,0 +1,97 @@
+---
+title: "AuthenticationStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## AuthenticationStrategy
+
+<GenerationInfo sourceFile="packages/core/src/config/auth/authentication-strategy.ts" sourceLine="23" packageName="@vendure/core" />
+
+An AuthenticationStrategy defines how a User (which can be a Customer in the Shop API or
+and Administrator in the Admin API) may be authenticated.
+
+Real-world examples can be found in the [Authentication guide](/guides/core-concepts/auth/).
+
+:::info
+
+This is configured via the `authOptions.shopAuthenticationStrategy` and `authOptions.adminAuthenticationStrategy`
+properties of your VendureConfig.
+
+:::
+
+```ts title="Signature"
+interface AuthenticationStrategy<Data = unknown> extends InjectableStrategy {
+    readonly name: string;
+    defineInputType(): DocumentNode;
+    authenticate(ctx: RequestContext, data: Data): Promise<User | false | string>;
+    onLogOut?(ctx: RequestContext, user: User): Promise<void>;
+}
+```
+* Extends: <code><a href='/reference/typescript-api/common/injectable-strategy#injectablestrategy'>InjectableStrategy</a></code>
+
+
+
+<div className="members-wrapper">
+
+### name
+
+<MemberInfo kind="property" type={`string`}   />
+
+The name of the strategy, for example `'facebook'`, `'google'`, `'keycloak'`.
+### defineInputType
+
+<MemberInfo kind="method" type={`() => DocumentNode`}   />
+
+Defines the type of the GraphQL Input object expected by the `authenticate`
+mutation. The final input object will be a map, with the key being the name
+of the strategy. The shape of the input object should match the generic `Data`
+type argument.
+
+*Example*
+
+For example, given the following:
+
+```ts
+defineInputType() {
+  return gql`
+     input MyAuthInput {
+       token: String!
+     }
+  `;
+}
+```
+
+assuming the strategy name is "my_auth", then the resulting call to `authenticate`
+would look like:
+
+```graphql
+authenticate(input: {
+  my_auth: {
+    token: "foo"
+  }
+}) {
+  # ...
+}
+```
+
+**Note:** if more than one graphql `input` type is being defined (as in a nested input type), then
+the _first_ input will be assumed to be the top-level input.
+### authenticate
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, data: Data) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a> | false | string&#62;`}   />
+
+Used to authenticate a user with the authentication provider. This method
+will implement the provider-specific authentication logic, and should resolve to either a
+<a href='/reference/typescript-api/entities/user#user'>User</a> object on success, or `false | string` on failure.
+A `string` return could be used to describe what error happened, otherwise `false` to an unknown error.
+### onLogOut
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, user: <a href='/reference/typescript-api/entities/user#user'>User</a>) => Promise&#60;void&#62;`}   />
+
+Called when a user logs out, and may perform any required tasks
+related to the user logging out with the external provider.
+
+
+</div>

+ 85 - 0
docs/docs/reference/typescript-api/cache/redis-cache-plugin.mdx

@@ -0,0 +1,85 @@
+---
+title: "RedisCachePlugin"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## RedisCachePlugin
+
+<GenerationInfo sourceFile="packages/core/src/plugin/redis-cache-plugin/redis-cache-plugin.ts" sourceLine="25" packageName="@vendure/core" since="3.1.0" />
+
+This plugin provides a Redis-based <a href='/reference/typescript-api/cache/redis-cache-strategy#rediscachestrategy'>RedisCacheStrategy</a> which stores cached items in a Redis instance.
+This is a high-performance cache strategy which is suitable for production use, and is a drop-in
+replacement for the <a href='/reference/typescript-api/cache/default-cache-plugin#defaultcacheplugin'>DefaultCachePlugin</a>.
+
+Note: To use this plugin, you need to manually install the `ioredis` package:
+
+```bash
+npm install ioredis@^5.3.2
+```
+
+```ts title="Signature"
+class RedisCachePlugin {
+    static options: RedisCachePluginInitOptions = {
+        maxItemSizeInBytes: 128_000,
+        redisOptions: {},
+        namespace: 'vendure-cache',
+    };
+    init(options: RedisCachePluginInitOptions) => ;
+}
+```
+
+<div className="members-wrapper">
+
+### options
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/cache/redis-cache-plugin#rediscacheplugininitoptions'>RedisCachePluginInitOptions</a>`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/typescript-api/cache/redis-cache-plugin#rediscacheplugininitoptions'>RedisCachePluginInitOptions</a>) => `}   />
+
+
+
+
+</div>
+
+
+## RedisCachePluginInitOptions
+
+<GenerationInfo sourceFile="packages/core/src/plugin/redis-cache-plugin/types.ts" sourceLine="9" packageName="@vendure/core" since="3.1.0" />
+
+Configuration options for the <a href='/reference/typescript-api/cache/redis-cache-plugin#rediscacheplugin'>RedisCachePlugin</a>.
+
+```ts title="Signature"
+interface RedisCachePluginInitOptions {
+    maxItemSizeInBytes?: number;
+    namespace?: string;
+    redisOptions?: import('ioredis').RedisOptions;
+}
+```
+
+<div className="members-wrapper">
+
+### maxItemSizeInBytes
+
+<MemberInfo kind="property" type={`number`} default={`128kb`}   />
+
+The maximum size of a single cache item in bytes. If a cache item exceeds this size, it will not be stored
+and an error will be logged.
+### namespace
+
+<MemberInfo kind="property" type={`string`} default={`'vendure-cache'`}   />
+
+The namespace to use for all keys stored in Redis. This can be useful if you are sharing a Redis instance
+between multiple applications.
+### redisOptions
+
+<MemberInfo kind="property" type={`import('ioredis').RedisOptions`}   />
+
+Options to pass to the `ioredis` Redis client.
+
+
+</div>

+ 75 - 0
docs/docs/reference/typescript-api/cache/redis-cache-strategy.mdx

@@ -0,0 +1,75 @@
+---
+title: "RedisCacheStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## RedisCacheStrategy
+
+<GenerationInfo sourceFile="packages/core/src/plugin/redis-cache-plugin/redis-cache-strategy.ts" sourceLine="23" packageName="@vendure/core" since="3.1.0" />
+
+A <a href='/reference/typescript-api/cache/cache-strategy#cachestrategy'>CacheStrategy</a> which stores cached items in a Redis instance.
+This is a high-performance cache strategy which is suitable for production use.
+
+Note: To use this strategy, you need to manually install the `ioredis` package:
+
+```bash
+npm install ioredis@^5.3.2
+```
+
+```ts title="Signature"
+class RedisCacheStrategy implements CacheStrategy {
+    constructor(options: RedisCachePluginInitOptions)
+    init() => ;
+    destroy() => ;
+    get(key: string) => Promise<T | undefined>;
+    set(key: string, value: T, options?: SetCacheKeyOptions) => Promise<void>;
+    delete(key: string) => Promise<void>;
+    invalidateTags(tags: string[]) => Promise<void>;
+}
+```
+* Implements: <code><a href='/reference/typescript-api/cache/cache-strategy#cachestrategy'>CacheStrategy</a></code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(options: <a href='/reference/typescript-api/cache/redis-cache-plugin#rediscacheplugininitoptions'>RedisCachePluginInitOptions</a>) => RedisCacheStrategy`}   />
+
+
+### init
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### destroy
+
+<MemberInfo kind="method" type={`() => `}   />
+
+
+### get
+
+<MemberInfo kind="method" type={`(key: string) => Promise&#60;T | undefined&#62;`}   />
+
+
+### set
+
+<MemberInfo kind="method" type={`(key: string, value: T, options?: <a href='/reference/typescript-api/cache/cache-strategy#setcachekeyoptions'>SetCacheKeyOptions</a>) => Promise&#60;void&#62;`}   />
+
+
+### delete
+
+<MemberInfo kind="method" type={`(key: string) => Promise&#60;void&#62;`}   />
+
+
+### invalidateTags
+
+<MemberInfo kind="method" type={`(tags: string[]) => Promise&#60;void&#62;`}   />
+
+
+
+
+</div>

+ 239 - 0
docs/docs/reference/typescript-api/data-access/list-query-builder.mdx

@@ -0,0 +1,239 @@
+---
+title: "ListQueryBuilder"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ListQueryBuilder
+
+<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="207" packageName="@vendure/core" />
+
+This helper class is used when fetching entities the database from queries which return a <a href='/reference/typescript-api/common/paginated-list#paginatedlist'>PaginatedList</a> type.
+These queries all follow the same format:
+
+In the GraphQL definition, they return a type which implements the `Node` interface, and the query returns a
+type which implements the `PaginatedList` interface:
+
+```graphql
+type BlogPost implements Node {
+  id: ID!
+  published: DateTime!
+  title: String!
+  body: String!
+}
+
+type BlogPostList implements PaginatedList {
+  items: [BlogPost!]!
+  totalItems: Int!
+}
+
+# Generated at run-time by Vendure
+input BlogPostListOptions
+
+extend type Query {
+   blogPosts(options: BlogPostListOptions): BlogPostList!
+}
+```
+When Vendure bootstraps, it will find the `BlogPostListOptions` input and, because it is used in a query
+returning a `PaginatedList` type, it knows that it should dynamically generate this input. This means
+all primitive field of the `BlogPost` type (namely, "published", "title" and "body") will have `filter` and
+`sort` inputs created for them, as well a `skip` and `take` fields for pagination.
+
+Your resolver function will then look like this:
+
+```ts
+@Resolver()
+export class BlogPostResolver
+  constructor(private blogPostService: BlogPostService) {}
+
+  @Query()
+  async blogPosts(
+    @Ctx() ctx: RequestContext,
+    @Args() args: any,
+  ): Promise<PaginatedList<BlogPost>> {
+    return this.blogPostService.findAll(ctx, args.options || undefined);
+  }
+}
+```
+
+and the corresponding service will use the ListQueryBuilder:
+
+```ts
+@Injectable()
+export class BlogPostService {
+  constructor(private listQueryBuilder: ListQueryBuilder) {}
+
+  findAll(ctx: RequestContext, options?: ListQueryOptions<BlogPost>) {
+    return this.listQueryBuilder
+      .build(BlogPost, options)
+      .getManyAndCount()
+      .then(async ([items, totalItems]) => {
+        return { items, totalItems };
+      });
+  }
+}
+```
+
+```ts title="Signature"
+class ListQueryBuilder implements OnApplicationBootstrap {
+    constructor(connection: TransactionalConnection, configService: ConfigService)
+    filterObjectHasProperty(filterObject: FP | NullOptionals<FP> | null | undefined, property: keyof FP) => boolean;
+    build(entity: Type<T>, options: ListQueryOptions<T> = {}, extendedOptions: ExtendedListQueryOptions<T> = {}) => SelectQueryBuilder<T>;
+}
+```
+* Implements: <code>OnApplicationBootstrap</code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(connection: <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a>, configService: ConfigService) => ListQueryBuilder`}   />
+
+
+### filterObjectHasProperty
+
+<MemberInfo kind="method" type={`(filterObject: FP | NullOptionals&#60;FP&#62; | null | undefined, property: keyof FP) => boolean`}   />
+
+Used to determine whether a list query `filter` object contains the
+given property, either at the top level or nested inside a boolean
+`_and` or `_or` expression.
+
+This is useful when a custom property map is used to map a filter
+field to a related entity, and we need to determine whether the
+filter object contains that property, which then means we would need
+to join that relation.
+### build
+
+<MemberInfo kind="method" type={`(entity: Type&#60;T&#62;, options: ListQueryOptions&#60;T&#62; = {}, extendedOptions: <a href='/reference/typescript-api/data-access/list-query-builder#extendedlistqueryoptions'>ExtendedListQueryOptions</a>&#60;T&#62; = {}) => SelectQueryBuilder&#60;T&#62;`}   />
+
+
+
+
+</div>
+
+
+## ExtendedListQueryOptions
+
+<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="48" packageName="@vendure/core" />
+
+Options which can be passed to the ListQueryBuilder's `build()` method.
+
+```ts title="Signature"
+type ExtendedListQueryOptions<T extends VendureEntity> = {
+    relations?: string[];
+    channelId?: ID;
+    where?: FindOptionsWhere<T>;
+    orderBy?: FindOneOptions<T>['order'];
+    entityAlias?: string;
+    ctx?: RequestContext;
+    customPropertyMap?: { [name: string]: string };
+    ignoreQueryLimits?: boolean;
+}
+```
+
+<div className="members-wrapper">
+
+### relations
+
+<MemberInfo kind="property" type={`string[]`}   />
+
+
+### channelId
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/common/id#id'>ID</a>`}   />
+
+
+### where
+
+<MemberInfo kind="property" type={`FindOptionsWhere&#60;T&#62;`}   />
+
+
+### orderBy
+
+<MemberInfo kind="property" type={`FindOneOptions&#60;T&#62;['order']`}   />
+
+
+### entityAlias
+
+<MemberInfo kind="property" type={`string`}  since="1.6.0"  />
+
+Allows you to specify the alias used for the entity `T` in the generated SQL query.
+Defaults to the entity class name lower-cased, i.e. `ProductVariant` -> `'productvariant'`.
+### ctx
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>`}   />
+
+When a RequestContext is passed, then the query will be
+executed as part of any outer transaction.
+### customPropertyMap
+
+<MemberInfo kind="property" type={`{ [name: string]: string }`}   />
+
+One of the main tasks of the ListQueryBuilder is to auto-generate filter and sort queries based on the
+available columns of a given entity. However, it may also be sometimes desirable to allow filter/sort
+on a property of a relation. In this case, the `customPropertyMap` can be used to define a property
+of the `options.sort` or `options.filter` which does not correspond to a direct column of the current
+entity, and then provide a mapping to the related property to be sorted/filtered.
+
+Example: we want to allow sort/filter by and Order's `customerLastName`. The actual lastName property is
+not a column in the Order table, it exists on the Customer entity, and Order has a relation to Customer via
+`Order.customer`. Therefore, we can define a customPropertyMap like this:
+
+*Example*
+
+```graphql
+"""
+Manually extend the filter & sort inputs to include the new
+field that we want to be able to use in building list queries.
+"""
+input OrderFilterParameter {
+    customerLastName: StringOperators
+}
+
+input OrderSortParameter {
+    customerLastName: SortOrder
+}
+```
+
+*Example*
+
+```ts
+const qb = this.listQueryBuilder.build(Order, options, {
+  relations: ['customer'],
+  customPropertyMap: {
+    // Tell TypeORM how to map that custom
+    // sort/filter field to the property on a
+    // related entity.
+    customerLastName: 'customer.lastName',
+  },
+};
+```
+We can now use the `customerLastName` property to filter or sort
+on the list query:
+
+*Example*
+
+```graphql
+query {
+  myOrderQuery(options: {
+    filter: {
+      customerLastName: { contains: "sm" }
+    }
+  }) {
+    # ...
+  }
+}
+```
+### ignoreQueryLimits
+
+<MemberInfo kind="property" type={`boolean`} default={`false`}  since="2.0.2"  />
+
+When set to `true`, the configured `shopListQueryLimit` and `adminListQueryLimit` values will be ignored,
+allowing unlimited results to be returned. Use caution when exposing an unlimited list query to the public,
+as it could become a vector for a denial of service attack if an attacker requests a very large list.
+
+
+</div>

+ 203 - 0
docs/docs/reference/typescript-api/orders/active-order-strategy.mdx

@@ -0,0 +1,203 @@
+---
+title: "ActiveOrderStrategy"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## ActiveOrderStrategy
+
+<GenerationInfo sourceFile="packages/core/src/config/order/active-order-strategy.ts" sourceLine="127" packageName="@vendure/core" since="1.9.0" />
+
+This strategy is used to determine the active Order for all order-related operations in
+the Shop API. By default, all the Shop API operations that relate to the active Order (e.g.
+`activeOrder`, `addItemToOrder`, `applyCouponCode` etc.) will implicitly create a new Order
+and set it on the current Session, and then read the session to obtain the active Order.
+This behaviour is defined by the <a href='/reference/typescript-api/orders/default-active-order-strategy#defaultactiveorderstrategy'>DefaultActiveOrderStrategy</a>.
+
+The `InputType` generic argument should correspond to the input type defined by the
+`defineInputType()` method.
+
+When `defineInputType()` is used, then the following Shop API operations will receive an additional
+`activeOrderInput` argument allowing the active order input to be specified:
+
+- `activeOrder`
+- `eligibleShippingMethods`
+- `eligiblePaymentMethods`
+- `nextOrderStates`
+- `addItemToOrder`
+- `adjustOrderLine`
+- `removeOrderLine`
+- `removeAllOrderLines`
+- `applyCouponCode`
+- `removeCouponCode`
+- `addPaymentToOrder`
+- `setCustomerForOrder`
+- `setOrderShippingAddress`
+- `setOrderBillingAddress`
+- `setOrderShippingMethod`
+- `setOrderCustomFields`
+- `transitionOrderToState`
+
+*Example*
+
+```graphql {hl_lines=[5]}
+mutation AddItemToOrder {
+  addItemToOrder(
+    productVariantId: 42,
+    quantity: 1,
+    activeOrderInput: { token: "123456" }
+  ) {
+    ...on Order {
+      id
+      # ...etc
+    }
+  }
+}
+```
+
+*Example*
+
+```ts
+import { ID } from '@vendure/common/lib/shared-types';
+import {
+  ActiveOrderStrategy,
+  CustomerService,
+  idsAreEqual,
+  Injector,
+  Order,
+  OrderService,
+  RequestContext,
+  TransactionalConnection,
+} from '@vendure/core';
+import gql from 'graphql-tag';
+
+// This strategy assumes a "orderToken" custom field is defined on the Order
+// entity, and uses that token to perform a lookup to determine the active Order.
+//
+// Additionally, it does _not_ define a `createActiveOrder()` method, which
+// means that a custom mutation would be required to create the initial Order in
+// the first place and set the "orderToken" custom field.
+class TokenActiveOrderStrategy implements ActiveOrderStrategy<{ token: string }> {
+  readonly name = 'orderToken';
+
+  private connection: TransactionalConnection;
+  private orderService: OrderService;
+
+  init(injector: Injector) {
+    this.connection = injector.get(TransactionalConnection);
+    this.orderService = injector.get(OrderService);
+  }
+
+  defineInputType = () => gql`
+    input OrderTokenActiveOrderInput {
+      token: String
+    }
+  `;
+
+  async determineActiveOrder(ctx: RequestContext, input: { token: string }) {
+    const qb = this.connection
+      .getRepository(ctx, Order)
+      .createQueryBuilder('order')
+      .leftJoinAndSelect('order.customer', 'customer')
+      .leftJoinAndSelect('customer.user', 'user')
+      .where('order.customFields.orderToken = :orderToken', { orderToken: input.token });
+
+    const order = await qb.getOne();
+    if (!order) {
+      return;
+    }
+    // Ensure the active user is the owner of this Order
+    const orderUserId = order.customer && order.customer.user && order.customer.user.id;
+    if (order.customer && idsAreEqual(orderUserId, ctx.activeUserId)) {
+      return order;
+    }
+  }
+}
+
+// in vendure-config.ts
+export const config = {
+  // ...
+  orderOptions: {
+    activeOrderStrategy: new TokenActiveOrderStrategy(),
+  },
+}
+```
+
+```ts title="Signature"
+interface ActiveOrderStrategy<InputType extends Record<string, any> | void = void> extends InjectableStrategy {
+    readonly name: string;
+    defineInputType?: () => DocumentNode;
+    createActiveOrder?: (ctx: RequestContext, input: InputType) => Promise<Order>;
+    determineActiveOrder(ctx: RequestContext, input: InputType): Promise<Order | undefined>;
+}
+```
+* Extends: <code><a href='/reference/typescript-api/common/injectable-strategy#injectablestrategy'>InjectableStrategy</a></code>
+
+
+
+<div className="members-wrapper">
+
+### name
+
+<MemberInfo kind="property" type={`string`}   />
+
+The name of the strategy, e.g. "orderByToken", which will also be used as the
+field name in the ActiveOrderInput type.
+### defineInputType
+
+<MemberInfo kind="property" type={`() =&#62; DocumentNode`}   />
+
+Defines the type of the GraphQL Input object expected by the `activeOrderInput`
+input argument.
+
+*Example*
+
+For example, given the following:
+
+```ts
+defineInputType() {
+  return gql`
+     input OrderTokenInput {
+       token: String!
+     }
+  `;
+}
+```
+
+assuming the strategy name is "orderByToken", then the resulting call to `activeOrder` (or any of the other
+affected Shop API operations) would look like:
+
+```graphql
+activeOrder(activeOrderInput: {
+  orderByToken: {
+    token: "foo"
+  }
+}) {
+  # ...
+}
+```
+
+**Note:** if more than one graphql `input` type is being defined (as in a nested input type), then
+the _first_ input will be assumed to be the top-level input.
+### createActiveOrder
+
+<MemberInfo kind="property" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: InputType) =&#62; Promise&#60;<a href='/reference/typescript-api/entities/order#order'>Order</a>&#62;`}   />
+
+Certain mutations such as `addItemToOrder` can automatically create a new Order if one does not exist.
+In these cases, this method will be called to create the new Order.
+
+If automatic creation of an Order does not make sense in your strategy, then leave this method
+undefined. You'll then need to take care of creating an order manually by defining a custom mutation.
+### determineActiveOrder
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: InputType) => Promise&#60;<a href='/reference/typescript-api/entities/order#order'>Order</a> | undefined&#62;`}   />
+
+This method is used to determine the active Order based on the current RequestContext in addition to any
+input values provided, as defined by the `defineInputType` method of this strategy.
+
+Note that this method is invoked frequently, so you should aim to keep it efficient. The returned Order,
+for example, does not need to have its various relations joined.
+
+
+</div>

+ 31 - 0
docs/docs/reference/typescript-api/payment/dummy-payment-handler.mdx

@@ -0,0 +1,31 @@
+---
+title: "DummyPaymentHandler"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## dummyPaymentHandler
+
+<GenerationInfo sourceFile="packages/core/src/config/payment/dummy-payment-method-handler.ts" sourceLine="27" packageName="@vendure/core" />
+
+A dummy PaymentMethodHandler which simply creates a Payment without any integration
+with an external payment provider. Intended only for use in development.
+
+By specifying certain metadata keys, failures can be simulated:
+
+*Example*
+
+```graphql
+addPaymentToOrder(input: {
+  method: 'dummy-payment-method',
+  metadata: {
+    shouldDecline: false,
+    shouldError: false,
+    shouldErrorOnSettle: true,
+  }
+}) {
+  # ...
+}
+```
+

+ 116 - 0
docs/docs/reference/typescript-api/request/relations-decorator.mdx

@@ -0,0 +1,116 @@
+---
+title: "Relations Decorator"
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+
+
+## Relations
+
+<GenerationInfo sourceFile="packages/core/src/api/decorators/relations.decorator.ts" sourceLine="136" packageName="@vendure/core" since="1.6.0" />
+
+Resolver param decorator which returns an array of relation paths which can be passed through
+to the TypeORM data layer in order to join only the required relations. This works by inspecting
+the GraphQL `info` object, examining the field selection, and then comparing this with information
+about the return type's relations.
+
+In addition to analyzing the field selection, this decorator also checks for any `@Calculated()`
+properties on the entity, and additionally includes relations from the `relations` array of the calculated
+metadata, if defined.
+
+So if, for example, the query only selects the `id` field of an Order, then no other relations need
+be joined in the resulting SQL query. This can massively speed up execution time for queries which do
+not include many deep nested relations.
+
+*Example*
+
+```ts
+@Query()
+@Allow(Permission.ReadOrder)
+orders(
+    @Ctx() ctx: RequestContext,
+    @Args() args: QueryOrdersArgs,
+    @Relations(Order) relations: RelationPaths<Order>,
+): Promise<PaginatedList<Order>> {
+    return this.orderService.findAll(ctx, args.options || undefined, relations);
+}
+```
+
+In the above example, given the following query:
+
+*Example*
+
+```graphql
+{
+  orders(options: { take: 10 }) {
+    items {
+      id
+      customer {
+        id
+        firstName
+        lastName
+      }
+      totalQuantity
+      totalWithTax
+    }
+  }
+}
+```
+then the value of `relations` will be
+
+```
+['customer', 'lines'']
+```
+The `'customer'` comes from the fact that the query is nesting the "customer" object, and the `'lines'` is taken
+from the `Order` entity's `totalQuantity` property, which uses <a href='/reference/typescript-api/data-access/calculated#calculated'>Calculated</a> decorator and defines those relations as dependencies
+for deriving the calculated value.
+
+## Depth
+
+By default, when inspecting the GraphQL query, the Relations decorator will look 3 levels deep in any nested fields. So, e.g. if
+the above `orders` query were changed to:
+
+*Example*
+
+```graphql
+{
+  orders(options: { take: 10 }) {
+    items {
+      id
+      lines {
+        productVariant {
+          product {
+            featuredAsset {
+              preview
+            }
+          }
+        }
+      }
+    }
+  }
+}
+```
+then the `relations` array would include `'lines'`, `'lines.productVariant'`, & `'lines.productVariant.product'` - 3 levels deep - but it would
+_not_ include `'lines.productVariant.product.featuredAsset'` since that exceeds the default depth. To specify a custom depth, you would
+use the decorator like this:
+
+*Example*
+
+```ts
+@Relations({ entity: Order, depth: 2 }) relations: RelationPaths<Order>,
+```
+
+## Omit
+
+The `omit` option is used to explicitly omit certain relations from the calculated relations array. This is useful in certain
+cases where we know for sure that we need to run the field resolver _anyway_. A good example is the `Collection.productVariants` relation.
+When a GraphQL query comes in for a Collection and also requests its `productVariants` field, there is no point using a lookahead to eagerly
+join that relation, because we will throw that data away anyway when the `productVariants` field resolver executes, since it returns a
+PaginatedList query rather than a simple array.
+
+*Example*
+
+```ts
+@Relations({ entity: Collection, omit: ['productVariant'] }) relations: RelationPaths<Collection>,
+```
+