Browse Source

Merge branch 'master' into minor

Michael Bromley 1 year ago
parent
commit
983a00977b
88 changed files with 5626 additions and 5083 deletions
  1. 16 0
      CHANGELOG.md
  2. 5 0
      LICENSE.md
  3. 1 1
      README.md
  4. 1 1
      docs/docs/guides/core-concepts/channels/index.md
  5. 70 34
      docs/docs/guides/deployment/deploy-to-northflank/index.md
  6. 117 0
      docs/docs/guides/developer-guide/channel-aware/index.md
  7. 137 0
      docs/docs/guides/developer-guide/dataloaders/index.md
  8. 3 3
      docs/docs/guides/how-to/publish-plugin/index.mdx
  9. 3 2
      docs/docs/reference/core-plugins/elasticsearch-plugin/index.md
  10. 38 20
      docs/docs/reference/graphql-api/admin/input-types.md
  11. 1 1
      docs/docs/reference/typescript-api/common/currency-code.md
  12. 1 1
      docs/docs/reference/typescript-api/common/job-state.md
  13. 1 1
      docs/docs/reference/typescript-api/common/language-code.md
  14. 1 1
      docs/docs/reference/typescript-api/common/permission.md
  15. 9 9
      docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md
  16. 1 1
      docs/docs/reference/typescript-api/custom-fields/custom-field-type.md
  17. 3 2
      docs/docs/reference/typescript-api/services/channel-service.md
  18. 3 3
      docs/docs/reference/typescript-api/services/product-variant-service.md
  19. 2 0
      docs/sidebars.js
  20. 1 1
      lerna.json
  21. 5 27
      license/license-faq.md
  22. 20 0
      license/plugin-exception.txt
  23. 72 0
      license/signatures/version1/cla.json
  24. 58 59
      package-lock.json
  25. 4 4
      packages/admin-ui-plugin/package.json
  26. 2 2
      packages/admin-ui/package.json
  27. 14 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html
  28. 9 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.scss
  29. 14 7
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts
  30. 7 3
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts
  31. 22 11
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  32. 2 2
      packages/admin-ui/src/lib/core/src/common/utilities/selection-manager.ts
  33. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  34. 7 0
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  35. 362 362
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  36. 437 458
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  37. 3 3
      packages/asset-server-plugin/package.json
  38. 3 3
      packages/cli/package.json
  39. 1 1
      packages/common/package.json
  40. 653 674
      packages/common/src/generated-shop-types.ts
  41. 19 11
      packages/common/src/generated-types.ts
  42. 7 0
      packages/common/src/normalize-string.spec.ts
  43. 4 1
      packages/common/src/normalize-string.ts
  44. 1 1
      packages/common/src/shared-types.ts
  45. 6 0
      packages/core/e2e/cache-service-default.e2e-spec.ts
  46. 6 0
      packages/core/e2e/cache-service-in-memory.e2e-spec.ts
  47. 31 2
      packages/core/e2e/cache-service-redis.e2e-spec.ts
  48. 61 0
      packages/core/e2e/custom-fields.e2e-spec.ts
  49. 25 2
      packages/core/e2e/fixtures/cache-service-shared-tests.ts
  50. 437 458
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  51. 624 645
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  52. 2 2
      packages/core/package.json
  53. 2 1
      packages/core/src/api/config/graphql-custom-fields.ts
  54. 9 2
      packages/core/src/api/schema/admin-api/product.api.graphql
  55. 2 0
      packages/core/src/config/entity/entity-duplicators/product-duplicator.ts
  56. 13 2
      packages/core/src/plugin/redis-cache-plugin/redis-cache-strategy.ts
  57. 65 0
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  58. 1 1
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  59. 3 2
      packages/core/src/service/services/channel.service.ts
  60. 17 2
      packages/core/src/service/services/product-variant.service.ts
  61. 3 3
      packages/create/package.json
  62. 1 1
      packages/create/templates/docker-compose.hbs
  63. 9 9
      packages/dev-server/package.json
  64. 1 1
      packages/elasticsearch-plugin/e2e/e2e-helpers.ts
  65. 2 3
      packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
  66. 437 458
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  67. 3 3
      packages/elasticsearch-plugin/package.json
  68. 8 4
      packages/elasticsearch-plugin/src/build-elastic-body.spec.ts
  69. 4 3
      packages/elasticsearch-plugin/src/build-elastic-body.ts
  70. 2 2
      packages/elasticsearch-plugin/src/options.ts
  71. 2 1
      packages/elasticsearch-plugin/src/plugin.ts
  72. 3 3
      packages/email-plugin/package.json
  73. 3 3
      packages/harden-plugin/package.json
  74. 3 3
      packages/job-queue-plugin/package.json
  75. 383 403
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  76. 628 649
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  77. 4 0
      packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts
  78. 4 4
      packages/payments-plugin/package.json
  79. 2 2
      packages/payments-plugin/src/braintree/braintree.handler.ts
  80. 657 678
      packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts
  81. 3 2
      packages/payments-plugin/src/stripe/stripe-utils.ts
  82. 5 5
      packages/payments-plugin/src/stripe/stripe.controller.ts
  83. 1 0
      packages/payments-plugin/src/stripe/stripe.service.ts
  84. 3 3
      packages/sentry-plugin/package.json
  85. 3 3
      packages/stellate-plugin/package.json
  86. 3 3
      packages/testing/package.json
  87. 4 4
      packages/ui-devkit/package.json
  88. 0 0
      schema-admin.json

+ 16 - 0
CHANGELOG.md

@@ -1,3 +1,19 @@
+## <small>3.1.2 (2025-01-22)</small>
+
+
+#### Fixes
+
+* **admin-ui** Add ProductVariantPrice custom fields ui inputs (#3327) ([0d22b25](https://github.com/vendure-ecommerce/vendure/commit/0d22b25)), closes [#3327](https://github.com/vendure-ecommerce/vendure/issues/3327)
+* **admin-ui** Update Polish localization (#3309) ([82787cf](https://github.com/vendure-ecommerce/vendure/commit/82787cf)), closes [#3309](https://github.com/vendure-ecommerce/vendure/issues/3309)
+* **common** Contract multiple sequential replacers to just one in normalizeString (#3289) ([f362a4b](https://github.com/vendure-ecommerce/vendure/commit/f362a4b)), closes [#3289](https://github.com/vendure-ecommerce/vendure/issues/3289)
+* **core** Clear previous line adjustments before testing order item promotions (#3320) ([c970dea](https://github.com/vendure-ecommerce/vendure/commit/c970dea)), closes [#3320](https://github.com/vendure-ecommerce/vendure/issues/3320)
+* **core** Fix schema error with readonly Address custom field ([1bddbcc](https://github.com/vendure-ecommerce/vendure/commit/1bddbcc)), closes [#3326](https://github.com/vendure-ecommerce/vendure/issues/3326)
+* **core** Improvements to Redis cache plugin (#3303) ([b631781](https://github.com/vendure-ecommerce/vendure/commit/b631781)), closes [#3303](https://github.com/vendure-ecommerce/vendure/issues/3303)
+* **core** Include variant custom fields when duplicating a product (#3203) ([69a1de0](https://github.com/vendure-ecommerce/vendure/commit/69a1de0)), closes [#3203](https://github.com/vendure-ecommerce/vendure/issues/3203)
+* **create** Specify Typesense Docker image version ([fd6a9fd](https://github.com/vendure-ecommerce/vendure/commit/fd6a9fd))
+* **payments-plugin** Fix null access error in BraintreePlugin ([627d930](https://github.com/vendure-ecommerce/vendure/commit/627d930))
+* **payments-plugin** Stripe plugin supports correct languageCode (#3298) ([4349ef8](https://github.com/vendure-ecommerce/vendure/commit/4349ef8)), closes [#3298](https://github.com/vendure-ecommerce/vendure/issues/3298)
+
 ## <small>3.1.1 (2024-12-17)</small>
 
 

+ 5 - 0
LICENSE.md

@@ -24,6 +24,11 @@ GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+Additional permission under GNU GPL version 3 section 7:
+
+An additional exception under section 7 of the GPL is included in the plugin-exception.txt file,
+which allows you to distribute Vendure plugins (i.e. extensions) under a different license.
+
 ## Vendure Commercial License (VCL)
 
 Alternatively, commercial and supported versions of the program - also known as

+ 1 - 1
README.md

@@ -12,7 +12,7 @@ An open-source headless commerce platform built on [Node.js](https://nodejs.org)
 ### [www.vendure.io](https://www.vendure.io/)
 
 * [Getting Started](https://docs.vendure.io/guides/getting-started/installation/): Get Vendure up and running locally in a matter of minutes with a single command
-* [Live Demo](https://demo.vendure.io/)
+* [Request Demo](https://vendure.io/demo)
 * [Vendure Discord](https://www.vendure.io/community): Join us on Discord for support and answers to your questions
 
 ## Branches

+ 1 - 1
docs/docs/guides/core-concepts/channels/index.md

@@ -31,7 +31,7 @@ Many entities are channel-aware, meaning that they can be associated with a mult
 
 - [`Asset`](/reference/typescript-api/entities/asset/)
 - [`Collection`](/reference/typescript-api/entities/collection/)
-- [`Customer`](/reference/typescript-api/entities/customer-group/)
+- [`Customer`](/reference/typescript-api/entities/customer/)
 - [`Facet`](/reference/typescript-api/entities/facet/)
 - [`FacetValue`](/reference/typescript-api/entities/facet-value/)
 - [`Order`](/reference/typescript-api/entities/order/)

+ 70 - 34
docs/docs/guides/deployment/deploy-to-northflank/index.md

@@ -2,7 +2,7 @@
 title: "Deploying to Northflank"
 showtoc: true
 images: 
-    - "/docs/deployment/deploy-to-northflank/deploy-to-northflank.webp"
+  - "/docs/deployment/deploy-to-northflank/deploy-to-northflank.webp"
 ---
 
 import Tabs from '@theme/Tabs';
@@ -47,26 +47,29 @@ If you want to use the free plan, use the "Lite Template".
 
 ```json
 {
-  "apiVersion": "v1",
-  "name": "Vendure Template",
-  "description": "Vendure is a modern, open-source composable commerce platform",
-  "project": {
-    "spec": {
-      "name": "Vendure",
-      "region": "europe-west",
-      "description": "Vendure is a modern, open-source composable commerce platform",
-      "color": "#57637A"
-    }
-  },
+  "apiVersion": "v1.2",
   "spec": {
     "kind": "Workflow",
     "spec": {
       "type": "sequential",
       "steps": [
+        {
+          "kind": "Project",
+          "ref": "project",
+          "spec": {
+            "name": "Vendure",
+            "region": "europe-west",
+            "description": "Vendure is a modern, open-source composable commerce platform",
+            "color": "#17b9ff"
+          }
+        },
         {
           "kind": "Workflow",
           "spec": {
             "type": "parallel",
+            "context": {
+              "projectId": "${refs.project.id}"
+            },
             "steps": [
               {
                 "kind": "Addon",
@@ -114,6 +117,7 @@ If you want to use the free plan, use the "Lite Template".
         {
           "kind": "SecretGroup",
           "spec": {
+            "projectId": "${refs.project.id}",
             "secretType": "environment-arguments",
             "priority": 10,
             "name": "secrets",
@@ -199,6 +203,7 @@ If you want to use the free plan, use the "Lite Template".
           "ref": "builder",
           "spec": {
             "name": "builder",
+            "projectId": "${refs.project.id}",
             "billing": {
               "deploymentPlan": "nf-compute-20"
             },
@@ -222,6 +227,7 @@ If you want to use the free plan, use the "Lite Template".
           "kind": "Build",
           "spec": {
             "id": "${refs.builder.id}",
+            "projectId": "${refs.project.id}",
             "type": "service",
             "branch": "master",
             "buildOverrides": {
@@ -236,18 +242,28 @@ If you want to use the free plan, use the "Lite Template".
           "kind": "Workflow",
           "spec": {
             "type": "parallel",
+            "context": {
+              "projectId": "${refs.project.id}"
+            },
             "steps": [
               {
                 "kind": "DeploymentService",
                 "spec": {
                   "deployment": {
                     "instances": 1,
+                    "storage": {
+                      "ephemeralStorage": {
+                        "storageSize": 1024
+                      },
+                      "shmSize": 64
+                    },
                     "docker": {
                       "configType": "customCommand",
                       "customCommand": "node ./dist/index.js"
                     },
                     "internal": {
-                      "buildId": "${refs.build.id}",
+                      "id": "${refs.builder.id}",
+                      "branch": "master",
                       "buildSHA": "latest"
                     }
                   },
@@ -271,29 +287,39 @@ If you want to use the free plan, use the "Lite Template".
                   ],
                   "runtimeEnvironment": {},
                   "runtimeFiles": {}
-                }
+                },
+                "ref": "server"
               },
               {
                 "kind": "DeploymentService",
                 "spec": {
-                  "name": "worker",
-                  "billing": {
-                    "deploymentPlan": "nf-compute-10"
-                  },
                   "deployment": {
                     "instances": 1,
+                    "storage": {
+                      "ephemeralStorage": {
+                        "storageSize": 1024
+                      },
+                      "shmSize": 64
+                    },
                     "docker": {
                       "configType": "customCommand",
                       "customCommand": "node ./dist/index-worker.js"
                     },
                     "internal": {
-                      "buildId": "${refs.build.id}",
+                      "id": "${refs.builder.id}",
+                      "branch": "master",
                       "buildSHA": "latest"
                     }
                   },
+                  "name": "worker",
+                  "billing": {
+                    "deploymentPlan": "nf-compute-10"
+                  },
                   "ports": [],
-                  "runtimeEnvironment": {}
-                }
+                  "runtimeEnvironment": {},
+                  "runtimeFiles": {}
+                },
+                "ref": "worker"
               }
             ]
           }
@@ -324,22 +350,22 @@ This setup is suitable for testing purposes, but is not recommended for producti
 
 ```json
 {
-  "apiVersion": "v1",
-  "name": "Vendure Lite Template",
-  "description": "Vendure is a modern, open-source composable commerce platform",
-  "project": {
-    "spec": {
-      "name": "Vendure Lite",
-      "region": "europe-west",
-      "description": "Vendure is a modern, open-source composable commerce platform",
-      "color": "#17b9ff"
-    }
-  },
+  "apiVersion": "v1.2",
   "spec": {
     "kind": "Workflow",
     "spec": {
       "type": "sequential",
       "steps": [
+        {
+          "kind": "Project",
+          "ref": "project", 
+          "spec": {
+            "name": "Vendure Lite",
+            "region": "europe-west",
+            "description": "Vendure is a modern, open-source composable commerce platform",
+            "color": "#17b9ff"
+          }
+        },
         {
           "kind": "Addon",
           "spec": {
@@ -361,6 +387,7 @@ This setup is suitable for testing purposes, but is not recommended for producti
         {
           "kind": "SecretGroup",
           "spec": {
+            "projectId": "${refs.project.id}",
             "name": "secrets",
             "secretType": "environment-arguments",
             "priority": 10,
@@ -423,6 +450,7 @@ This setup is suitable for testing purposes, but is not recommended for producti
           "kind": "BuildService",
           "spec": {
             "name": "builder",
+            "projectId": "${refs.project.id}",
             "billing": {
               "deploymentPlan": "nf-compute-10"
             },
@@ -446,7 +474,8 @@ This setup is suitable for testing purposes, but is not recommended for producti
           "kind": "Build",
           "ref": "build",
           "spec": {
-            "id": "builder",
+            "id": "${refs.builder.id}",
+            "projectId": "${refs.project.id}",
             "type": "service",
             "branch": "master",
             "reuseExistingBuilds": true
@@ -463,12 +492,19 @@ This setup is suitable for testing purposes, but is not recommended for producti
             },
             "deployment": {
               "instances": 1,
+              "storage": {
+                "ephemeralStorage": {
+                  "storageSize": 1024
+                },
+                "shmSize": 64
+              },
               "docker": {
                 "configType": "customCommand",
                 "customCommand": "yarn start:server"
               },
               "internal": {
-                "buildId": "${refs.build.id}",
+                "id": "${refs.builder.id}",
+                "branch": "master",
                 "buildSHA": "latest"
               }
             },

+ 117 - 0
docs/docs/guides/developer-guide/channel-aware/index.md

@@ -0,0 +1,117 @@
+---
+title: "Implementing ChannelAware"
+showtoc: true
+---
+
+## Defining channel-aware entities
+
+Making an entity channel-aware means that it can be associated with a specific [Channel](/reference/typescript-api/entities/channel).
+This is useful when you want to have different data or features for different channels. First you will have to create
+an entity ([Define a database entity](/guides/developer-guide/database-entity/)) that implements the `ChannelAware` interface.
+This interface requires the entity to provide a `channels` property
+
+```ts title="src/plugins/requests/entities/product-request.entity.ts"
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { VendureEntity, Product, EntityId, ID, ChannelAware } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+
+@Entity()
+class ProductRequest extends VendureEntity implements ChannelAware {
+    constructor(input?: DeepPartial<ProductRequest>) {
+        super(input);
+    }
+
+    @ManyToOne(type => Product)
+    product: Product;
+
+    @EntityId()
+    productId: ID;
+
+    @Column()
+    text: string;
+    
+    // highlight-start
+    @ManyToMany(() => Channel)
+    @JoinTable()
+    channels: Channel[];
+    // highlight-end
+}
+```
+
+## Creating channel-aware entities
+
+Creating a channel-aware entity is similar to creating a regular entity. The only difference is that you need to assign the entity to the current channel.
+This can be done by using the `ChannelService` which provides the `assignToCurrentChannel` helper function. 
+
+:::info
+The `assignToCurrentChannel` function will only assign the `channels` property of the entity. You will still need to save the entity to the database.
+:::
+
+```ts title="src/plugins/requests/service/product-request.service.ts"
+import { ChannelService } from '@vendure/core';
+
+export class RequestService {
+
+    constructor(private channelService: ChannelService) {}
+
+    async create(ctx: RequestContext, input: CreateRequestInput): Promise<ProductRequest> {
+        const request = new ProductRequest(input);
+        // Now we need to assign the request to the current channel (+ default channel)
+// highlight-next-line
+        await this.channelService.assignToCurrentChannel(input, ctx);
+        
+        return await this.connection.getRepository(ProductRequest).save(request);
+    }
+}
+```
+For [Translatable entities](/guides/developer-guide/translations/), the best place to assign the channels is inside the `beforeSave` input of the [TranslateableSave](/reference/typescript-api/service-helpers/translatable-saver/) helper class.
+
+
+## Querying channel-aware entities
+
+When querying channel-aware entities, you can use the [ListQueryBuilder](/reference/typescript-api/data-access/list-query-builder/#extendedlistqueryoptions) or
+the [TransactionalConnection](/reference/typescript-api/data-access/transactional-connection/#findoneinchannel) to automatically filter entities based on the provided channel id.
+
+
+```ts title="src/plugins/requests/service/product-request.service.ts"
+import { ChannelService, ListQueryBuilder, TransactionalConnection } from '@vendure/core';
+
+export class RequestService {
+
+    constructor(
+        private connection: TransactionalConnection,
+        private listQueryBuilder: ListQueryBuilder,
+        private channelService: ChannelService) {}
+
+    findOne(ctx: RequestContext,
+            requestId: ID,
+            relations?: RelationPaths<ProductRequest>) {
+// highlight-start
+        return this.connection.findOneInChannel(ctx, ProductRequest, requestId, ctx.channelId, {
+            relations: unique(effectiveRelations)
+        });
+// highlight-end
+    }
+
+    findAll(
+        ctx: RequestContext,
+        options?: ProductRequestListOptions,
+        relations?: RelationPaths<ProductRequest>,
+    ): Promise<PaginatedList<ProductRequest>> {
+        return this.listQueryBuilder
+            .build(ProductRequest, options, {
+                ctx,
+                relations,
+// highlight-next-line
+                channelId: ctx.channelId,
+            })
+            .getManyAndCount()
+            .then(([items, totalItems]) => {
+                return {
+                    items,
+                    totalItems,
+                };
+            });
+    }
+}
+```

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

@@ -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
+
+```sh
+npm install dataloader
+```
+
+**Dataloader skeleton**
+
+```ts title="src/plugins/my-plugin/api/datalaoder.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!
+:::
+

+ 3 - 3
docs/docs/guides/how-to/publish-plugin/index.mdx

@@ -52,9 +52,9 @@ is using a compatible version of Vendure.
 
 ### License
 
-Your plugin **must** use a license that is compatible with the GPL v3 license that Vendure uses. This means your package.json
-file should include `"license": "GPL-3.0-or-later"`, and your repository should include a license file (usually named `LICENSE.txt`) containing the
-[full text of the GPL v3 license](https://www.gnu.org/licenses/gpl-3.0.txt).
+You are free to license your plugin as you wish. Although Vendure itself is licensed under the GPLv3, there is
+a special exception for plugins which allows you to distribute them under a different license. See the
+[plugin exception](https://github.com/vendure-ecommerce/vendure/blob/master/license/plugin-exception.txt) for more details.
 
 ## Publishing to npm
 

+ 3 - 2
docs/docs/reference/core-plugins/elasticsearch-plugin/index.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ElasticsearchPlugin
 
-<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="224" packageName="@vendure/elasticsearch-plugin" />
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="225" packageName="@vendure/elasticsearch-plugin" />
 
 This plugin allows your product search to be powered by [Elasticsearch](https://github.com/elastic/elasticsearch) - a powerful Open Source search
 engine. This is a drop-in replacement for the DefaultSearchPlugin which exposes many powerful configuration options enabling your storefront
@@ -53,7 +53,7 @@ const config: VendureConfig = {
 ## Search API Extensions
 This plugin extends the default search query of the Shop API, allowing richer querying of your product data.
 
-The [SearchResponse](/reference/graphql-api/admin/object-types/#searchresponse) type is extended with information
+The [SearchResponse](/reference/graphql-api/shop/object-types/#searchresponse) type is extended with information
 about price ranges in the result set:
 ```graphql
 extend type SearchResponse {
@@ -75,6 +75,7 @@ type PriceRangeBucket {
 extend input SearchInput {
     priceRange: PriceRangeInput
     priceRangeWithTax: PriceRangeInput
+    inStock: Boolean
 }
 
 input PriceRangeInput {

+ 38 - 20
docs/docs/reference/graphql-api/admin/input-types.md

@@ -1127,6 +1127,8 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">price: <a href="/reference/graphql-api/admin/object-types#money">Money</a></div>
 
+<div class="graphql-code-line ">prices: [<a href="/reference/graphql-api/admin/input-types#createproductvariantpriceinput">CreateProductVariantPriceInput</a>]</div>
+
 <div class="graphql-code-line ">taxCategoryId: <a href="/reference/graphql-api/admin/object-types#id">ID</a></div>
 
 <div class="graphql-code-line ">optionIds: [<a href="/reference/graphql-api/admin/object-types#id">ID</a>!]</div>
@@ -1162,6 +1164,20 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">translations: [<a href="/reference/graphql-api/admin/input-types#productoptiontranslationinput">ProductOptionTranslationInput</a>!]!</div>
 
 
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## CreateProductVariantPriceInput
+
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level">input <span class="graphql-code-identifier">CreateProductVariantPriceInput</span> &#123;</div>
+<div class="graphql-code-line ">currencyCode: <a href="/reference/graphql-api/admin/enums#currencycode">CurrencyCode</a>!</div>
+
+<div class="graphql-code-line ">price: <a href="/reference/graphql-api/admin/object-types#money">Money</a>!</div>
+
+<div class="graphql-code-line ">customFields: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
+
+
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 
@@ -2629,25 +2645,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">filterOperator: <a href="/reference/graphql-api/admin/enums#logicaloperator">LogicalOperator</a></div>
 
 
-<div class="graphql-code-line top-level">&#125;</div>
-</div>
-
-## ProductVariantPriceInput
-
-<div class="graphql-code-block">
-<div class="graphql-code-line top-level comment">"""</div>
-<div class="graphql-code-line top-level comment">Used to set up update the price of a ProductVariant in a particular Channel.</div>
-
-<div class="graphql-code-line top-level comment">If the <code>delete</code> flag is `true`, the price will be deleted for the given Channel.</div>
-<div class="graphql-code-line top-level comment">"""</div>
-<div class="graphql-code-line top-level">input <span class="graphql-code-identifier">ProductVariantPriceInput</span> &#123;</div>
-<div class="graphql-code-line ">currencyCode: <a href="/reference/graphql-api/admin/enums#currencycode">CurrencyCode</a>!</div>
-
-<div class="graphql-code-line ">price: <a href="/reference/graphql-api/admin/object-types#money">Money</a>!</div>
-
-<div class="graphql-code-line ">delete: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
-
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 
@@ -4216,7 +4213,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line comment">"""</div>
 <div class="graphql-code-line comment">Allows multiple prices to be set for the ProductVariant in different currencies.</div>
 <div class="graphql-code-line comment">"""</div>
-<div class="graphql-code-line ">prices: [<a href="/reference/graphql-api/admin/input-types#productvariantpriceinput">ProductVariantPriceInput</a>!]</div>
+<div class="graphql-code-line ">prices: [<a href="/reference/graphql-api/admin/input-types#updateproductvariantpriceinput">UpdateProductVariantPriceInput</a>!]</div>
 
 <div class="graphql-code-line ">featuredAssetId: <a href="/reference/graphql-api/admin/object-types#id">ID</a></div>
 
@@ -4235,6 +4232,27 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">customFields: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
 
 
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## UpdateProductVariantPriceInput
+
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level comment">Used to set up update the price of a ProductVariant in a particular Channel.</div>
+
+<div class="graphql-code-line top-level comment">If the <code>delete</code> flag is `true`, the price will be deleted for the given Channel.</div>
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level">input <span class="graphql-code-identifier">UpdateProductVariantPriceInput</span> &#123;</div>
+<div class="graphql-code-line ">currencyCode: <a href="/reference/graphql-api/admin/enums#currencycode">CurrencyCode</a>!</div>
+
+<div class="graphql-code-line ">price: <a href="/reference/graphql-api/admin/object-types#money">Money</a>!</div>
+
+<div class="graphql-code-line ">delete: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
+
+<div class="graphql-code-line ">customFields: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
+
+
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 

+ 1 - 1
docs/docs/reference/typescript-api/common/currency-code.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## CurrencyCode
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="994" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="1001" packageName="@vendure/common" />
 
 ISO 4217 currency code
 

+ 1 - 1
docs/docs/reference/typescript-api/common/job-state.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## JobState
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2238" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2245" packageName="@vendure/common" />
 
 The state of a Job in the JobQueue
 

+ 1 - 1
docs/docs/reference/typescript-api/common/language-code.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## LanguageCode
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2256" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2263" packageName="@vendure/common" />
 
 Languages in the form of a ISO 639-1 language code with optional
 region or script modifier (e.g. de_AT). The selection available is based

+ 1 - 1
docs/docs/reference/typescript-api/common/permission.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Permission
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="4422" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="4429" packageName="@vendure/common" />
 
 Permissions for administrators and customers. Used to control access to
 GraphQL resolvers via the <a href='/reference/typescript-api/request/allow-decorator#allow'>Allow</a> decorator.

+ 9 - 9
docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md

@@ -29,15 +29,15 @@ type DefaultFormConfigHash = {
     'product-selector-form-input': Record<string, never>;
     'relation-form-input': Record<string, never>;
     'rich-text-form-input': Record<string, never>;
-    'select-form-input': {
-        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
+    'select-form-input': {
+        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
     };
     'text-form-input': { prefix?: string; suffix?: string };
-    'textarea-form-input': {
-        spellcheck?: boolean;
+    'textarea-form-input': {
+        spellcheck?: boolean;
     };
-    'product-multi-form-input': {
-        selectionMode?: 'product' | 'variant';
+    'product-multi-form-input': {
+        selectionMode?: 'product' | 'variant';
     };
     'combination-mode-form-input': Record<string, never>;
     'struct-form-input': Record<string, never>;
@@ -108,7 +108,7 @@ type DefaultFormConfigHash = {
 
 ### 'select-form-input'
 
-<MemberInfo kind="property" type={`{
         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;
     }`}   />
+<MemberInfo kind="property" type={`{         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;     }`}   />
 
 
 ### 'text-form-input'
@@ -118,12 +118,12 @@ type DefaultFormConfigHash = {
 
 ### 'textarea-form-input'
 
-<MemberInfo kind="property" type={`{
         spellcheck?: boolean;
     }`}   />
+<MemberInfo kind="property" type={`{         spellcheck?: boolean;     }`}   />
 
 
 ### 'product-multi-form-input'
 
-<MemberInfo kind="property" type={`{
         selectionMode?: 'product' | 'variant';
     }`}   />
+<MemberInfo kind="property" type={`{         selectionMode?: 'product' | 'variant';     }`}   />
 
 
 ### 'combination-mode-form-input'

+ 1 - 1
docs/docs/reference/typescript-api/custom-fields/custom-field-type.md

@@ -29,7 +29,7 @@ datetime     | datetime (m,s), timestamp (p)         | DateTime
 struct       | json (m), jsonb (p), text (s)         | JSON
 relation     | many-to-one / many-to-many relation   | As specified in config
 
-Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#custom-field-config-properties)
+Additionally, the CustomFieldType also dictates which [configuration options](/guides/developer-guide/custom-fields/#custom-field-config-properties)
 are available for that custom field.
 
 ```ts title="Signature"

+ 3 - 2
docs/docs/reference/typescript-api/services/channel-service.md

@@ -46,12 +46,13 @@ class ChannelService {
 <MemberInfo kind="method" type={`(entity: T, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) => Promise&#60;T&#62;`}   />
 
 Assigns a ChannelAware entity to the default Channel as well as any channel
-specified in the RequestContext.
+specified in the RequestContext. This method will not save the entity to the database, but
+assigns the `channels` property of the entity.
 ### assignToChannels
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, entityType: Type&#60;T&#62;, entityId: <a href='/reference/typescript-api/common/id#id'>ID</a>, channelIds: <a href='/reference/typescript-api/common/id#id'>ID</a>[]) => Promise&#60;T&#62;`}   />
 
-Assigns the entity to the given Channels and saves.
+Assigns the entity to the given Channels and saves all changes to the database.
 ### removeFromChannels
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, entityType: Type&#60;T&#62;, entityId: <a href='/reference/typescript-api/common/id#id'>ID</a>, channelIds: <a href='/reference/typescript-api/common/id#id'>ID</a>[]) => Promise&#60;T | undefined&#62;`}   />

+ 3 - 3
docs/docs/reference/typescript-api/services/product-variant-service.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ProductVariantService
 
-<GenerationInfo sourceFile="packages/core/src/service/services/product-variant.service.ts" sourceLine="68" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/product-variant.service.ts" sourceLine="69" packageName="@vendure/core" />
 
 Contains methods relating to <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a> entities.
 
@@ -34,7 +34,7 @@ class ProductVariantService {
     getFulfillableStockLevel(ctx: RequestContext, variant: ProductVariant) => Promise<number>;
     create(ctx: RequestContext, input: CreateProductVariantInput[]) => Promise<Array<Translated<ProductVariant>>>;
     update(ctx: RequestContext, input: UpdateProductVariantInput[]) => Promise<Array<Translated<ProductVariant>>>;
-    createOrUpdateProductVariantPrice(ctx: RequestContext, productVariantId: ID, price: number, channelId: ID, currencyCode?: CurrencyCode) => Promise<ProductVariantPrice>;
+    createOrUpdateProductVariantPrice(ctx: RequestContext, productVariantId: ID, price: number, channelId: ID, currencyCode?: CurrencyCode, customFields?: CustomFieldsObject) => Promise<ProductVariantPrice>;
     deleteProductVariantPrice(ctx: RequestContext, variantId: ID, channelId: ID, currencyCode: CurrencyCode) => ;
     softDelete(ctx: RequestContext, id: ID | ID[]) => Promise<DeletionResponse>;
     hydratePriceFields(ctx: RequestContext, variant: ProductVariant, priceField: F) => Promise<ProductVariant[F]>;
@@ -139,7 +139,7 @@ for those variants which are tracking inventory.
 
 ### createOrUpdateProductVariantPrice
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, productVariantId: <a href='/reference/typescript-api/common/id#id'>ID</a>, price: number, channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>, currencyCode?: <a href='/reference/typescript-api/common/currency-code#currencycode'>CurrencyCode</a>) => Promise&#60;<a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a>&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, productVariantId: <a href='/reference/typescript-api/common/id#id'>ID</a>, price: number, channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>, currencyCode?: <a href='/reference/typescript-api/common/currency-code#currencycode'>CurrencyCode</a>, customFields?: CustomFieldsObject) => Promise&#60;<a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a>&#62;`}   />
 
 Creates a <a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a> for the given ProductVariant/Channel combination.
 If the `currencyCode` is not specified, the default currency of the Channel will be used.

+ 2 - 0
docs/sidebars.js

@@ -91,7 +91,9 @@ const sidebars = {
                     value: 'Advanced Topics',
                     className: 'sidebar-section-header',
                 },
+                'guides/developer-guide/channel-aware/index',
                 'guides/developer-guide/cache/index',
+                'guides/developer-guide/dataloaders/index',
                 'guides/developer-guide/db-subscribers/index',
                 'guides/developer-guide/importing-data/index',
                 'guides/developer-guide/logging/index',

+ 1 - 1
lerna.json

@@ -1,6 +1,6 @@
 {
     "packages": ["packages/*"],
-    "version": "3.1.1",
+    "version": "3.1.2",
     "npmClient": "npm",
     "command": {
         "version": {

+ 5 - 27
license/license-faq.md

@@ -26,7 +26,8 @@ All Vendure contributors retain copyright on their code, but agree to release it
 If you are unable or unwilling to contribute a patch under the GPL version 3 and the Vendure Commercial License, do not submit a patch.
 
 ## I want to release my work under a different license than GPLv3, is that possible?
-No. You can only release your work under any GPL version 3 or later compatible license.
+Yes. There is a special exception described in the file `plugin-exception.txt` that allows you to distribute Vendure plugins under a different license.
+If you modify the Vendure core code, you must still release your changes under the GPLv3.
 
 ## The GPL requires that I distribute the "source code" of my files. What does that mean for a web application?
 The "source code" of a file means the format that is intended for people to edit.
@@ -35,34 +36,11 @@ For TypeScript, CSS, and HTML code, the file itself, without any compression or
 The "source code" is whichever version is intended to be edited by people.
 
 ## If I write a plugin for my Vendure application, do I have to license it under the GPL?
-Yes. Vendure plugins for your application are a derivative work of Vendure.
-If you distribute them, you must do so under the terms of the GPL version 3 or later.
-
-You are not required to distribute them at all, however.
-
-However, when distributing your own Vendure-based work, it is important to keep in mind what the GPLv3 applies to.
-The GPLv3 on code applies to code that interacts with that code, but not to data.
-That is, Vendure's TypeScript code is under the GPLv3, and so all TypeScript code that interacts with it must also be
-under the GPLv3 or GPLv3 compatible. Images and JSON files that TypeScript sends to the browser are not
-affected by the GPL because they are data
-
-When distributing your own plugin, therefore,
-the GPLv3 applies to any pieces that directly interact with parts of Vendure that are under the GPLv3.
-Images and other asset files you create yourself are not affected.
-
-## If I write a plugin for my application, do I have to give it away to everyone?
-No. The GPL requires that if you make a derivative work of Vendure and distribute it to someone else,
-you must provide that person with the source code under the terms of the GPLv3 so that they may modify and redistribute
-it under the terms of the GPLv3 as well. However, you are under no obligation to distribute the code to anyone else.
-If you do not distribute the code but use it only within your organization,
-then you are not required to distribute it to anyone at all.
-
-However, if your plugin is of general use then it is often a good idea to contribute it back to the community anyway.
-You can get feedback, bug reports, and new feature patches from others who find it useful.
+No. There is a special exception described in the file `plugin-exception.txt` that allows you to 
+distribute Vendure plugins under a different license.
 
 ## Is it permitted for me to sell Vendure or a Vendure plugin?
-Yes. However, you must distribute it under the GPL version 3 or later,
-so those you sell it to must be allowed to modify and redistribute it as well. See questions above.
+Yes. However, any modifications to Vendure Core must be made available under the terms of the GPL version 3.
 
 ## Do I have to give the code for my web site to anyone who visits it?
 

+ 20 - 0
license/plugin-exception.txt

@@ -0,0 +1,20 @@
+Vendure Plugin Exception
+
+0. Definitions
+
+"Vendure Core" includes all the source code files included in this repository, which are licensed under the
+GPL v3.0 license.
+
+A "Plugin" is a software module that is designed to enable additional functionality by
+ using APIs exposed by the Vendure Core packages, including but not limited to the public
+APIs described in the Vendure documentation at https://docs.vendure.io. A Plugin does not include any source code from
+the Vendure Core itself.
+
+1. Grant of Additional Permission.
+
+For Plugins that are distributed separately from Vendure Core (such as via a package repository),
+Vendure GmbH hereby grants you permission to link the Vendure Core from that Plugin and to
+distribute the Plugin under terms of your choice, including licenses which are not compatible with the GPL v3.0,
+ without any of the additional requirements listed in the GNU Library General Public License.
+
+(The GPL v3.0 restrictions do apply in other respects; for example, they cover modifications made to the Vendure Core.)

+ 72 - 0
license/signatures/version1/cla.json

@@ -391,6 +391,78 @@
       "created_at": "2024-12-13T14:40:27Z",
       "repoId": 136938012,
       "pullRequestNo": 3272
+    },
+    {
+      "name": "Ankish76",
+      "id": 16212112,
+      "comment_id": 2556961146,
+      "created_at": "2024-12-20T13:00:49Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3282
+    },
+    {
+      "name": "ankitdev10",
+      "id": 103364681,
+      "comment_id": 2558144493,
+      "created_at": "2024-12-21T14:59:05Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3288
+    },
+    {
+      "name": "HerclasNido",
+      "id": 55491202,
+      "comment_id": 2563246384,
+      "created_at": "2024-12-27T02:26:40Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3298
+    },
+    {
+      "name": "xtul9",
+      "id": 136118080,
+      "comment_id": 2575421766,
+      "created_at": "2025-01-07T14:26:53Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3309
+    },
+    {
+      "name": "markbmullins",
+      "id": 36978139,
+      "comment_id": 2579153369,
+      "created_at": "2025-01-09T04:22:05Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3308
+    },
+    {
+      "name": "wpplumber",
+      "id": 52907282,
+      "comment_id": 2584123844,
+      "created_at": "2025-01-10T20:50:24Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3313
+    },
+    {
+      "name": "putermancer",
+      "id": 334262,
+      "comment_id": 2596876359,
+      "created_at": "2025-01-16T20:58:44Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3319
+    },
+    {
+      "name": "apoca",
+      "id": 5594091,
+      "comment_id": 2602828392,
+      "created_at": "2025-01-20T16:21:41Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3325
+    },
+    {
+      "name": "rein1410",
+      "id": 72182093,
+      "comment_id": 2606078156,
+      "created_at": "2025-01-22T01:21:31Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3315
     }
   ]
 }

+ 58 - 59
package-lock.json

@@ -14,8 +14,7 @@
                 "eslint-config-prettier": "^8.8.0",
                 "eslint-plugin-import": "^2.27.5",
                 "eslint-plugin-jsdoc": "^45.0.0",
-                "eslint-plugin-prefer-arrow": "^1.2.3",
-                "sharp": "^0.33.5"
+                "eslint-plugin-prefer-arrow": "^1.2.3"
             },
             "devDependencies": {
                 "@commitlint/cli": "^19.1.0",
@@ -27,7 +26,7 @@
                 "@graphql-codegen/typescript": "4.0.9",
                 "@graphql-codegen/typescript-operations": "4.2.3",
                 "@graphql-tools/schema": "^10.0.4",
-                "@swc/core": "^1.10.1",
+                "@swc/core": "^1.4.6",
                 "@types/klaw-sync": "^6.0.5",
                 "@types/node": "^20.11.19",
                 "concurrently": "^8.2.2",
@@ -32688,7 +32687,7 @@
         },
         "packages/admin-ui": {
             "name": "@vendure/admin-ui",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@angular/animations": "^17.2.4",
@@ -32711,7 +32710,7 @@
                 "@ng-select/ng-select": "^12.0.7",
                 "@ngx-translate/core": "^15.0.0",
                 "@ngx-translate/http-loader": "^8.0.0",
-                "@vendure/common": "^3.1.1",
+                "@vendure/common": "^3.1.2",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "apollo-angular": "^6.0.0",
                 "apollo-upload-client": "^18.0.1",
@@ -32782,7 +32781,7 @@
         },
         "packages/admin-ui-plugin": {
             "name": "@vendure/admin-ui-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "date-fns": "^2.30.0",
@@ -32792,9 +32791,9 @@
             "devDependencies": {
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/admin-ui": "^3.1.1",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
+                "@vendure/admin-ui": "^3.1.2",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
                 "express": "^4.18.3",
                 "rimraf": "^5.0.5",
                 "typescript": "5.4.2"
@@ -32825,7 +32824,7 @@
         },
         "packages/asset-server-plugin": {
             "name": "@vendure/asset-server-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "file-type": "^19.0.0",
@@ -32838,8 +32837,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/node-fetch": "^2.6.11",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
                 "express": "^4.18.3",
                 "node-fetch": "^2.7.0",
                 "rimraf": "^5.0.5",
@@ -32851,11 +32850,11 @@
         },
         "packages/cli": {
             "name": "@vendure/cli",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^3.1.1",
+                "@vendure/common": "^3.1.2",
                 "change-case": "^4.1.2",
                 "commander": "^11.0.0",
                 "dotenv": "^16.4.5",
@@ -32869,7 +32868,7 @@
                 "vendure": "dist/cli.js"
             },
             "devDependencies": {
-                "@vendure/core": "^3.1.1",
+                "@vendure/core": "^3.1.2",
                 "typescript": "5.3.3"
             },
             "funding": {
@@ -32904,7 +32903,7 @@
         },
         "packages/common": {
             "name": "@vendure/common",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "rimraf": "^5.0.5",
@@ -32916,7 +32915,7 @@
         },
         "packages/core": {
             "name": "@vendure/core",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@apollo/server": "^4.11.2",
@@ -32928,7 +32927,7 @@
                 "@nestjs/platform-express": "~10.4.12",
                 "@nestjs/terminus": "~10.2.3",
                 "@nestjs/typeorm": "~10.0.2",
-                "@vendure/common": "^3.1.1",
+                "@vendure/common": "^3.1.2",
                 "bcrypt": "^5.1.1",
                 "body-parser": "^1.20.2",
                 "cookie-session": "^2.1.0",
@@ -33084,11 +33083,11 @@
         },
         "packages/create": {
             "name": "@vendure/create",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "^3.1.1",
+                "@vendure/common": "^3.1.2",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "fs-extra": "^11.2.0",
@@ -33106,7 +33105,7 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
-                "@vendure/core": "^3.1.1",
+                "@vendure/core": "^3.1.2",
                 "rimraf": "^5.0.5",
                 "ts-node": "^10.9.2",
                 "typescript": "5.3.3"
@@ -33123,21 +33122,21 @@
             }
         },
         "packages/dev-server": {
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@nestjs/axios": "^3.0.2",
-                "@vendure/admin-ui-plugin": "^3.1.1",
-                "@vendure/asset-server-plugin": "^3.1.1",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
-                "@vendure/elasticsearch-plugin": "^3.1.1",
-                "@vendure/email-plugin": "^3.1.1",
+                "@vendure/admin-ui-plugin": "^3.1.2",
+                "@vendure/asset-server-plugin": "^3.1.2",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
+                "@vendure/elasticsearch-plugin": "^3.1.2",
+                "@vendure/email-plugin": "^3.1.2",
                 "typescript": "5.3.3"
             },
             "devDependencies": {
-                "@vendure/testing": "^3.1.1",
-                "@vendure/ui-devkit": "^3.1.1",
+                "@vendure/testing": "^3.1.2",
+                "@vendure/ui-devkit": "^3.1.2",
                 "commander": "^12.0.0",
                 "concurrently": "^8.2.2",
                 "csv-stringify": "^6.4.6",
@@ -33155,7 +33154,7 @@
         },
         "packages/elasticsearch-plugin": {
             "name": "@vendure/elasticsearch-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@elastic/elasticsearch": "~7.9.1",
@@ -33163,8 +33162,8 @@
                 "fast-deep-equal": "^3.1.3"
             },
             "devDependencies": {
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -33174,7 +33173,7 @@
         },
         "packages/email-plugin": {
             "name": "@vendure/email-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@types/nodemailer": "^6.4.9",
@@ -33190,8 +33189,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/mjml": "^4.7.4",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -33201,14 +33200,14 @@
         },
         "packages/harden-plugin": {
             "name": "@vendure/harden-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "graphql-query-complexity": "^0.12.0"
             },
             "devDependencies": {
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1"
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -33216,12 +33215,12 @@
         },
         "packages/job-queue-plugin": {
             "name": "@vendure/job-queue-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "@google-cloud/pubsub": "^2.8.0",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
                 "bullmq": "^5.4.2",
                 "ioredis": "^5.3.2",
                 "rimraf": "^5.0.5",
@@ -33233,7 +33232,7 @@
         },
         "packages/payments-plugin": {
             "name": "@vendure/payments-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "currency.js": "2.0.4"
@@ -33242,9 +33241,9 @@
                 "@mollie/api-client": "^3.7.0",
                 "@types/braintree": "^3.3.11",
                 "@types/localtunnel": "2.0.4",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1",
-                "@vendure/testing": "^3.1.1",
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2",
+                "@vendure/testing": "^3.1.2",
                 "braintree": "^3.22.0",
                 "localtunnel": "2.0.2",
                 "nock": "^13.1.4",
@@ -33298,12 +33297,12 @@
         },
         "packages/sentry-plugin": {
             "name": "@vendure/sentry-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
                 "@sentry/node": "^7.106.1",
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1"
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -33314,14 +33313,14 @@
         },
         "packages/stellate-plugin": {
             "name": "@vendure/stellate-plugin",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "node-fetch": "^2.7.0"
             },
             "devDependencies": {
-                "@vendure/common": "^3.1.1",
-                "@vendure/core": "^3.1.1"
+                "@vendure/common": "^3.1.2",
+                "@vendure/core": "^3.1.2"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -33329,11 +33328,11 @@
         },
         "packages/testing": {
             "name": "@vendure/testing",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@graphql-typed-document-node/core": "^3.2.0",
-                "@vendure/common": "^3.1.1",
+                "@vendure/common": "^3.1.2",
                 "faker": "^4.1.0",
                 "form-data": "^4.0.0",
                 "graphql": "~16.9.0",
@@ -33346,7 +33345,7 @@
                 "@types/mysql": "^2.15.26",
                 "@types/node-fetch": "^2.6.4",
                 "@types/pg": "^8.11.2",
-                "@vendure/core": "^3.1.1",
+                "@vendure/core": "^3.1.2",
                 "mysql": "^2.18.1",
                 "pg": "^8.11.3",
                 "rimraf": "^5.0.5",
@@ -33362,15 +33361,15 @@
         },
         "packages/ui-devkit": {
             "name": "@vendure/ui-devkit",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "license": "GPL-3.0-or-later",
             "dependencies": {
                 "@angular-devkit/build-angular": "^17.2.3",
                 "@angular/cli": "^17.2.3",
                 "@angular/compiler": "^17.2.4",
                 "@angular/compiler-cli": "^17.2.4",
-                "@vendure/admin-ui": "^3.1.1",
-                "@vendure/common": "^3.1.1",
+                "@vendure/admin-ui": "^3.1.2",
+                "@vendure/common": "^3.1.2",
                 "chalk": "^4.1.0",
                 "chokidar": "^3.6.0",
                 "fs-extra": "^11.2.0",
@@ -33381,7 +33380,7 @@
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/core": "^3.1.1",
+                "@vendure/core": "^3.1.2",
                 "react": "^18.2.0",
                 "react-dom": "^18.2.0",
                 "rimraf": "^5.0.5",

+ 4 - 4
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -21,9 +21,9 @@
     "devDependencies": {
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/admin-ui": "^3.1.1",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
+        "@vendure/admin-ui": "^3.1.2",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
         "express": "^4.18.3",
         "rimraf": "^5.0.5",
         "typescript": "5.4.2"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "scripts": {
         "ng": "ng",
@@ -49,7 +49,7 @@
         "@ng-select/ng-select": "^12.0.7",
         "@ngx-translate/core": "^15.0.0",
         "@ngx-translate/http-loader": "^8.0.0",
-        "@vendure/common": "^3.1.1",
+        "@vendure/common": "^3.1.2",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^6.0.0",
         "apollo-upload-client": "^18.0.1",

+ 14 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html

@@ -195,12 +195,26 @@
                         [priceIncludesTax]="channelPriceIncludesTax$ | async"
                         [taxCategoryId]="detailForm.get('taxCategoryId')!.value"
                     />
+
+                    <div class="form-grid-span" *ngIf="customPriceFields.length">
+                        <div class="title-row">
+                            <span class="title">{{ 'common.custom-fields' | translate }}</span>
+                        </div>
+                        <vdr-tabbed-custom-fields
+                            entityName="ProductVariantPrice"
+                            [customFields]="customPriceFields"
+                            [customFieldsFormGroup]="price.get(['customFields'])"
+                            [readonly]="!(updatePermissions | hasPermission)"
+                        />
+                    </div>
                 </div>
+
                 <vdr-variant-price-strategy-detail
                     [channelPriceIncludesTax]="channelPriceIncludesTax$ | async"
                     [channelDefaultCurrencyCode]="channelDefaultCurrencyCode"
                     [variant]="variant"
                 />
+
                 <ng-container *ngIf="unusedCurrencyCodes$ | async as unusedCurrencyCodes">
                     <div *ngIf="unusedCurrencyCodes.length">
                         <vdr-dropdown>

+ 9 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.scss

@@ -29,3 +29,12 @@ vdr-product-variant-quick-jump {
         }
     }
 }
+
+.title-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+.title {
+    font-size: var(--font-size-base);
+}

+ 14 - 7
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts

@@ -71,6 +71,7 @@ export class ProductVariantDetailComponent
 {
     public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
     readonly customFields = this.getCustomFieldConfig('ProductVariant');
+    readonly customPriceFields = this.getCustomFieldConfig('ProductVariantPrice');
     readonly customOptionFields = this.getCustomFieldConfig('ProductOption');
     stockLevels$: Observable<NonNullable<GetProductVariantDetailQuery['productVariant']>['stockLevels']>;
     detailForm = this.formBuilder.group<VariantFormValue>({
@@ -99,6 +100,7 @@ export class ProductVariantDetailComponent
             price: FormControl<number | null>;
             currencyCode: FormControl<CurrencyCode | null>;
             delete: FormControl<boolean | null>;
+            customFields: FormGroup<any>; //TODO: Add type
         }>
     >([]);
     assetChanges: SelectedAssets = {};
@@ -181,6 +183,7 @@ export class ProductVariantDetailComponent
                 currencyCode,
                 price: 0,
                 delete: false as boolean,
+                customFields: this.formBuilder.group(getCustomFieldsDefaults(this.customPriceFields)),
             }),
         );
     }
@@ -246,6 +249,7 @@ export class ProductVariantDetailComponent
                                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                                 currencyCode: control.value.currencyCode!,
                                 delete: control.value.delete === true,
+                                customFields: control.get('customFields')?.value,
                             }));
                     }
                     return this.dataService.mutate(ProductVariantUpdateMutationDocument, {
@@ -353,13 +357,16 @@ export class ProductVariantDetailComponent
         }
         this.pricesForm.clear();
         for (const price of variant.prices) {
-            this.pricesForm.push(
-                this.formBuilder.group({
-                    price: price.price,
-                    currencyCode: price.currencyCode,
-                    delete: false as boolean,
-                }),
-            );
+            const priceForm = this.formBuilder.group({
+                price: price.price,
+                currencyCode: price.currencyCode,
+                delete: false as boolean,
+                customFields: this.formBuilder.group(getCustomFieldsDefaults(this.customPriceFields)),
+            });
+            if (this.customPriceFields.length) {
+                this.setCustomFieldFormValues(this.customPriceFields, priceForm.get(['customFields']), price);
+            }
+            this.pricesForm.push(priceForm);
         }
         if (this.customFields.length) {
             this.setCustomFieldFormValues(

+ 7 - 3
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts

@@ -1,4 +1,8 @@
-import { ASSET_FRAGMENT, PRODUCT_OPTION_FRAGMENT } from '@vendure/admin-ui/core';
+import {
+    ASSET_FRAGMENT,
+    PRODUCT_OPTION_FRAGMENT,
+    PRODUCT_VARIANT_PRICE_FRAGMENT,
+} from '@vendure/admin-ui/core';
 import { gql } from 'apollo-angular';
 
 export const PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT = gql`
@@ -12,8 +16,7 @@ export const PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT = gql`
         price
         currencyCode
         prices {
-            price
-            currencyCode
+            ...ProductVariantPrice
         }
         priceWithTax
         stockOnHand
@@ -87,6 +90,7 @@ export const PRODUCT_VARIANT_DETAIL_QUERY_PRODUCT_VARIANT_FRAGMENT = gql`
             }
         }
     }
+    ${PRODUCT_VARIANT_PRICE_FRAGMENT}
 `;
 
 export const PRODUCT_VARIANT_DETAIL_QUERY = gql`

File diff suppressed because it is too large
+ 22 - 11
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 2 - 2
packages/admin-ui/src/lib/core/src/common/utilities/selection-manager.ts

@@ -47,13 +47,13 @@ export class SelectionManager<T> {
                 ...this.items.slice(start, end).filter(a => !this._selection.find(s => itemsAreEqual(a, s))),
             );
         } else if (index === -1) {
-            if (multiSelect && (event?.ctrlKey || event?.shiftKey || additiveMode)) {
+            if (multiSelect && ((event?.ctrlKey || event?.metaKey) || event?.shiftKey || additiveMode)) {
                 this._selection.push(item);
             } else {
                 this._selection = [item];
             }
         } else {
-            if (multiSelect && event?.ctrlKey) {
+            if (multiSelect && (event?.ctrlKey || event?.metaKey)) {
                 this._selection.splice(index, 1);
             } else if (1 < this._selection.length && !additiveMode) {
                 this._selection = [item];

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '3.1.1';
+export const ADMIN_UI_VERSION = '3.1.2';

+ 7 - 0
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -62,6 +62,13 @@ export const PRODUCT_OPTION_FRAGMENT = gql`
     }
 `;
 
+export const PRODUCT_VARIANT_PRICE_FRAGMENT = gql`
+    fragment ProductVariantPrice on ProductVariantPrice {
+        price
+        currencyCode
+    }
+`;
+
 export const PRODUCT_VARIANT_FRAGMENT = gql`
     fragment ProductVariant on ProductVariant {
         id

File diff suppressed because it is too large
+ 362 - 362
packages/admin-ui/src/lib/static/i18n-messages/pl.json


File diff suppressed because it is too large
+ 437 - 458
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/asset-server-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -26,8 +26,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/node-fetch": "^2.6.11",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
         "express": "^4.18.3",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",

+ 3 - 3
packages/cli/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/cli",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -35,7 +35,7 @@
     ],
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.1.1",
+        "@vendure/common": "^3.1.2",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
         "dotenv": "^16.4.5",
@@ -46,7 +46,7 @@
         "tsconfig-paths": "^4.2.0"
     },
     "devDependencies": {
-        "@vendure/core": "^3.1.1",
+        "@vendure/core": "^3.1.2",
         "typescript": "5.3.3"
     }
 }

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/common",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "scripts": {

File diff suppressed because it is too large
+ 653 - 674
packages/common/src/generated-shop-types.ts


+ 19 - 11
packages/common/src/generated-types.ts

@@ -894,6 +894,7 @@ export type CreateProductVariantInput = {
   optionIds?: InputMaybe<Array<Scalars['ID']['input']>>;
   outOfStockThreshold?: InputMaybe<Scalars['Int']['input']>;
   price?: InputMaybe<Scalars['Money']['input']>;
+  prices?: InputMaybe<Array<InputMaybe<CreateProductVariantPriceInput>>>;
   productId: Scalars['ID']['input'];
   sku: Scalars['String']['input'];
   stockLevels?: InputMaybe<Array<StockLevelInput>>;
@@ -910,6 +911,12 @@ export type CreateProductVariantOptionInput = {
   translations: Array<ProductOptionTranslationInput>;
 };
 
+export type CreateProductVariantPriceInput = {
+  currencyCode: CurrencyCode;
+  customFields?: InputMaybe<Scalars['JSON']['input']>;
+  price: Scalars['Money']['input'];
+};
+
 export type CreatePromotionInput = {
   actions: Array<ConfigurableOperationInput>;
   conditions: Array<ConfigurableOperationInput>;
@@ -4877,16 +4884,6 @@ export type ProductVariantPrice = {
   price: Scalars['Money']['output'];
 };
 
-/**
- * Used to set up update the price of a ProductVariant in a particular Channel.
- * If the `delete` flag is `true`, the price will be deleted for the given Channel.
- */
-export type ProductVariantPriceInput = {
-  currencyCode: CurrencyCode;
-  delete?: InputMaybe<Scalars['Boolean']['input']>;
-  price: Scalars['Money']['input'];
-};
-
 export type ProductVariantSortParameter = {
   createdAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
@@ -6604,7 +6601,7 @@ export type UpdateProductVariantInput = {
   /** Sets the price for the ProductVariant in the Channel's default currency */
   price?: InputMaybe<Scalars['Money']['input']>;
   /** Allows multiple prices to be set for the ProductVariant in different currencies. */
-  prices?: InputMaybe<Array<ProductVariantPriceInput>>;
+  prices?: InputMaybe<Array<UpdateProductVariantPriceInput>>;
   sku?: InputMaybe<Scalars['String']['input']>;
   stockLevels?: InputMaybe<Array<StockLevelInput>>;
   stockOnHand?: InputMaybe<Scalars['Int']['input']>;
@@ -6614,6 +6611,17 @@ export type UpdateProductVariantInput = {
   useGlobalOutOfStockThreshold?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
+/**
+ * Used to set up update the price of a ProductVariant in a particular Channel.
+ * If the `delete` flag is `true`, the price will be deleted for the given Channel.
+ */
+export type UpdateProductVariantPriceInput = {
+  currencyCode: CurrencyCode;
+  customFields?: InputMaybe<Scalars['JSON']['input']>;
+  delete?: InputMaybe<Scalars['Boolean']['input']>;
+  price: Scalars['Money']['input'];
+};
+
 export type UpdatePromotionInput = {
   actions?: InputMaybe<Array<ConfigurableOperationInput>>;
   conditions?: InputMaybe<Array<ConfigurableOperationInput>>;

+ 7 - 0
packages/common/src/normalize-string.spec.ts

@@ -46,4 +46,11 @@ describe('normalizeString()', () => {
     it('replaces combining diaeresis with e', () => {
         expect(normalizeString('Ja quäkt Schwyz Pöbel vor Gmünd')).toBe('ja quaekt schwyz poebel vor gmuend');
     });
+
+    it('contracts multiple sequential replacers to a single replacer', () => {
+        expect(normalizeString('foo - bar', '-')).toBe('foo-bar');
+        expect(normalizeString('foo--bar', '-')).toBe('foo-bar');
+        expect(normalizeString('foo - bar', '_')).toBe('foo_-_bar');
+        expect(normalizeString('foo _ bar', '_')).toBe('foo_bar');
+    });
 });

+ 4 - 1
packages/common/src/normalize-string.ts

@@ -4,6 +4,8 @@
  * Based on https://stackoverflow.com/a/37511463/772859
  */
 export function normalizeString(input: string, spaceReplacer = ' '): string {
+    const multipleSequentialReplacerRegex = new RegExp(`([${spaceReplacer}]){2,}`, 'g');
+
     return (input || '')
         .normalize('NFD')
         .replace(/[\u00df]/g, 'ss')
@@ -12,5 +14,6 @@ export function normalizeString(input: string, spaceReplacer = ' '): string {
         .replace(/[\u0300-\u036f]/g, '')
         .toLowerCase()
         .replace(/[!"£$%^&*()+[\]{};:@#~?\\/,|><`¬'=‘’©®™]/g, '')
-        .replace(/\s+/g, spaceReplacer);
+        .replace(/\s+/g, spaceReplacer)
+        .replace(multipleSequentialReplacerRegex, spaceReplacer);
 }

+ 1 - 1
packages/common/src/shared-types.ts

@@ -96,7 +96,7 @@ export type ID = string | number;
  * struct       | json (m), jsonb (p), text (s)         | JSON
  * relation     | many-to-one / many-to-many relation   | As specified in config
  *
- * Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#custom-field-config-properties)
+ * Additionally, the CustomFieldType also dictates which [configuration options](/guides/developer-guide/custom-fields/#custom-field-config-properties)
  * are available for that custom field.
  *
  * @docsCategory custom-fields

+ 6 - 0
packages/core/e2e/cache-service-default.e2e-spec.ts

@@ -15,7 +15,9 @@ import {
     invalidatesByMultipleTags,
     invalidatesBySingleTag,
     invalidatesManyByMultipleTags,
+    invalidTTLsShouldNotSetKey,
     setsAKey,
+    setsAKeyWithSubSecondTtl,
     setsAKeyWithTtl,
     setsArrayOfObjects,
     setsArrayValue,
@@ -66,6 +68,8 @@ describe('CacheService with DefaultCachePlugin (sql)', () => {
 
     it('sets a key with ttl', () => setsAKeyWithTtl(cacheService, ttlProvider));
 
+    it('sets a key with sub-second ttl', () => setsAKeyWithSubSecondTtl(cacheService, ttlProvider));
+
     it('evicts the oldest key when cache is full', () => evictsTheOldestKeyWhenCacheIsFull(cacheService));
 
     it('invalidates by single tag', () => invalidatesBySingleTag(cacheService));
@@ -75,4 +79,6 @@ describe('CacheService with DefaultCachePlugin (sql)', () => {
     it('invalidates many by multiple tags', () => invalidatesManyByMultipleTags(cacheService));
 
     it('invalidates a large number of keys by tag', () => invalidatesALargeNumberOfKeysByTag(cacheService));
+
+    it('invalid ttls should not set key', () => invalidTTLsShouldNotSetKey(cacheService));
 });

+ 6 - 0
packages/core/e2e/cache-service-in-memory.e2e-spec.ts

@@ -16,7 +16,9 @@ import {
     invalidatesByMultipleTags,
     invalidatesBySingleTag,
     invalidatesManyByMultipleTags,
+    invalidTTLsShouldNotSetKey,
     setsAKey,
+    setsAKeyWithSubSecondTtl,
     setsAKeyWithTtl,
     setsArrayOfObjects,
     setsArrayValue,
@@ -67,6 +69,8 @@ describe('CacheService in-memory', () => {
 
     it('sets a key with ttl', () => setsAKeyWithTtl(cacheService, ttlProvider));
 
+    it('sets a key with sub-second ttl', () => setsAKeyWithSubSecondTtl(cacheService, ttlProvider));
+
     it('evicts the oldest key when cache is full', () => evictsTheOldestKeyWhenCacheIsFull(cacheService));
 
     it('invalidates by single tag', () => invalidatesBySingleTag(cacheService));
@@ -76,4 +80,6 @@ describe('CacheService in-memory', () => {
     it('invalidates many by multiple tags', () => invalidatesManyByMultipleTags(cacheService));
 
     it('invalidates a large number of keys by tag', () => invalidatesALargeNumberOfKeysByTag(cacheService));
+
+    it('invalid ttls should not set key', () => invalidTTLsShouldNotSetKey(cacheService));
 });

+ 31 - 2
packages/core/e2e/cache-service-redis.e2e-spec.ts

@@ -1,7 +1,7 @@
-import { CacheService, mergeConfig, RedisCachePlugin } from '@vendure/core';
+import { CacheService, Logger, mergeConfig, RedisCachePlugin } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import path from 'path';
-import { afterAll, beforeAll, describe, it } from 'vitest';
+import { vi, afterAll, beforeAll, describe, it, expect } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
@@ -13,7 +13,10 @@ import {
     invalidatesByMultipleTags,
     invalidatesBySingleTag,
     invalidatesManyByMultipleTags,
+    invalidTTLsShouldNotSetKey,
     setsAKey,
+    setsAKeyWithSubSecondTtl,
+    setsAKeyWithTtl,
     setsArrayOfObjects,
     setsArrayValue,
     setsObjectValue,
@@ -61,6 +64,30 @@ describe('CacheService with RedisCachePlugin', () => {
 
     it('deletes a key', () => deletesAKey(cacheService));
 
+    it('sets a key with ttl', async () => {
+        await cacheService.set('test-key1', 'test-value', { ttl: 1000 });
+        const result = await cacheService.get('test-key1');
+        expect(result).toBe('test-value');
+
+        await new Promise(resolve => setTimeout(resolve, 1500));
+
+        const result2 = await cacheService.get('test-key1');
+
+        expect(result2).toBeUndefined();
+    });
+
+    it('sets a key with sub-second ttl', async () => {
+        await cacheService.set('test-key2', 'test-value', { ttl: 900 });
+        const result = await cacheService.get('test-key2');
+        expect(result).toBe('test-value');
+
+        await new Promise(resolve => setTimeout(resolve, 1500));
+
+        const result2 = await cacheService.get('test-key2');
+
+        expect(result2).toBeUndefined();
+    });
+
     it('invalidates by single tag', () => invalidatesBySingleTag(cacheService));
 
     it('invalidates by multiple tags', () => invalidatesByMultipleTags(cacheService));
@@ -68,4 +95,6 @@ describe('CacheService with RedisCachePlugin', () => {
     it('invalidates many by multiple tags', () => invalidatesManyByMultipleTags(cacheService));
 
     it('invalidates a large number of keys by tag', () => invalidatesALargeNumberOfKeysByTag(cacheService));
+
+    it('invalid ttls should not set key', () => invalidTTLsShouldNotSetKey(cacheService));
 });

+ 61 - 0
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -188,6 +188,22 @@ const customConfig = mergeConfig(testConfig(), {
             { name: 'secretKey2', type: 'string', defaultValue: '', public: false, internal: false },
         ],
         OrderLine: [{ name: 'validateInt', type: 'int', min: 0, max: 10 }],
+        ProductVariantPrice: [
+            {
+                name: 'costPrice',
+                type: 'int',
+            }
+        ],  
+        // Single readonly Address custom field to test
+        // https://github.com/vendure-ecommerce/vendure/issues/3326
+        Address: [
+            {
+                name: 'hereId',
+                type: 'string',
+                readonly: true,
+                nullable: true,
+            },
+        ],
     } as CustomFields,
 });
 
@@ -1155,4 +1171,49 @@ describe('Custom fields', () => {
             }
         });
     });
+
+    it('on ProductVariantPrice', async () => {
+        const { updateProductVariants } = await adminClient.query(
+            gql`
+                mutation UpdateProductVariants($input: [UpdateProductVariantInput!]!) {
+                    updateProductVariants(input: $input) {
+                        id
+                        prices {
+                            currencyCode
+                            price
+                            customFields {
+                                costPrice
+                            }
+                        }
+                    }
+                }
+            `,
+            {
+                input: [
+                    {
+                        id: 'T_1',
+                        prices: [
+                            {
+                                price: 129900,
+                                currencyCode: 'USD',
+                                customFields: {
+                                    costPrice: 100,
+                                },
+                            },
+                        ],
+                    },
+                ],
+            },
+        );
+
+        expect(updateProductVariants[0].prices).toEqual([
+            {
+                currencyCode: 'USD',
+                price: 129900,
+                customFields: {
+                    costPrice: 100,
+                },
+            },
+        ]);
+    });
 });

+ 25 - 2
packages/core/e2e/fixtures/cache-service-shared-tests.ts

@@ -1,5 +1,5 @@
-import { CacheService } from '@vendure/core';
-import { expect } from 'vitest';
+import { CacheService, Logger } from '@vendure/core';
+import { expect, vi } from 'vitest';
 
 import { TestingCacheTtlProvider } from '../../src/cache/cache-ttl-provider';
 
@@ -59,6 +59,22 @@ export async function setsAKeyWithTtl(cacheService: CacheService, ttlProvider: T
     expect(result2).toBeUndefined();
 }
 
+export async function setsAKeyWithSubSecondTtl(
+    cacheService: CacheService,
+    ttlProvider: TestingCacheTtlProvider,
+) {
+    ttlProvider.setTime(new Date().getTime());
+    await cacheService.set('test-key', 'test-value', { ttl: 900 });
+    const result = await cacheService.get('test-key');
+    expect(result).toBe('test-value');
+
+    ttlProvider.incrementTime(2000);
+
+    const result2 = await cacheService.get('test-key');
+
+    expect(result2).toBeUndefined();
+}
+
 export async function evictsTheOldestKeyWhenCacheIsFull(cacheService: CacheService) {
     await cacheService.set('key1', 'value1');
     await cacheService.set('key2', 'value2');
@@ -127,3 +143,10 @@ export async function invalidatesALargeNumberOfKeysByTag(cacheService: CacheServ
         expect(await cacheService.get(`taggedKey${i}`)).toBeUndefined();
     }
 }
+
+export async function invalidTTLsShouldNotSetKey(cacheService: CacheService) {
+    await cacheService.set('test-key', 'test-value', { ttl: -1500 });
+    const result = await cacheService.get('test-key');
+
+    expect(result).toBeUndefined();
+}

File diff suppressed because it is too large
+ 437 - 458
packages/core/e2e/graphql/generated-e2e-admin-types.ts


File diff suppressed because it is too large
+ 624 - 645
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/core",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -49,7 +49,7 @@
         "@nestjs/platform-express": "~10.4.12",
         "@nestjs/terminus": "~10.2.3",
         "@nestjs/typeorm": "~10.0.2",
-        "@vendure/common": "^3.1.1",
+        "@vendure/common": "^3.1.2",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "cookie-session": "^2.1.0",

+ 2 - 1
packages/core/src/api/config/graphql-custom-fields.ts

@@ -247,6 +247,7 @@ export function addGraphQLCustomFields(
     const publicAddressFields = customFieldConfig.Address?.filter(
         config => !config.internal && (publicOnly === true ? config.public !== false : true),
     );
+    const writeablePublicAddressFields = publicAddressFields?.filter(field => !field.readonly);
     if (publicAddressFields?.length) {
         // For custom fields on the Address entity, we also extend the OrderAddress
         // type (which is used to store address snapshots on Orders)
@@ -257,7 +258,7 @@ export function addGraphQLCustomFields(
                 }
             `;
         }
-        if (schema.getType('UpdateOrderAddressInput')) {
+        if (schema.getType('UpdateOrderAddressInput') && writeablePublicAddressFields?.length) {
             customFieldTypeDefs += `
                 extend input UpdateOrderAddressInput {
                     customFields: UpdateAddressCustomFieldsInput

+ 9 - 2
packages/core/src/api/schema/admin-api/product.api.graphql

@@ -130,11 +130,17 @@ input StockLevelInput {
     stockOnHand: Int!
 }
 
+
+input CreateProductVariantPriceInput {
+    currencyCode: CurrencyCode!
+    price: Money!
+}
+
 """
 Used to set up update the price of a ProductVariant in a particular Channel.
 If the `delete` flag is `true`, the price will be deleted for the given Channel.
 """
-input ProductVariantPriceInput {
+input UpdateProductVariantPriceInput {
     currencyCode: CurrencyCode!
     price: Money!
     delete: Boolean
@@ -146,6 +152,7 @@ input CreateProductVariantInput {
     facetValueIds: [ID!]
     sku: String!
     price: Money
+    prices: [CreateProductVariantPriceInput]
     taxCategoryId: ID
     optionIds: [ID!]
     featuredAssetId: ID
@@ -172,7 +179,7 @@ input UpdateProductVariantInput {
     """
     Allows multiple prices to be set for the ProductVariant in different currencies.
     """
-    prices: [ProductVariantPriceInput!]
+    prices: [UpdateProductVariantPriceInput!]
     featuredAssetId: ID
     assetIds: [ID!]
     stockOnHand: Int

+ 2 - 0
packages/core/src/config/entity/entity-duplicators/product-duplicator.ts

@@ -182,6 +182,7 @@ export const productDuplicator = new EntityDuplicator({
                         return {
                             languageCode: translation.languageCode,
                             name: translation.name,
+                            customFields: translation.customFields,
                         };
                     }),
                     optionIds: options.map(option => option.id),
@@ -190,6 +191,7 @@ export const productDuplicator = new EntityDuplicator({
                         stockLocationId: stockLevel.stockLocationId,
                         stockOnHand: stockLevel.stockOnHand,
                     })),
+                    customFields: variant.customFields,
                 };
             });
             const duplicatedProductVariants = await productVariantService.create(ctx, variantInput);

+ 13 - 2
packages/core/src/plugin/redis-cache-plugin/redis-cache-strategy.ts

@@ -63,13 +63,24 @@ export class RedisCacheStrategy implements CacheStrategy {
                     return;
                 }
             }
-            multi.set(namedspacedKey, JSON.stringify(value), 'EX', ttl);
+            if (Math.round(ttl) <= 0) {
+                Logger.error(
+                    `Could not set cache item ${key}: TTL must be greater than 0 seconds`,
+                    loggerCtx,
+                );
+                return;
+            }
+            multi.set(namedspacedKey, JSON.stringify(value), 'EX', Math.round(ttl));
             if (options?.tags) {
                 for (const tag of options.tags) {
                     multi.sadd(this.tagNamespace(tag), namedspacedKey);
                 }
             }
-            await multi.exec();
+            const results = await multi.exec();
+            const resultWithError = results?.find(([err, _]) => err);
+            if (resultWithError) {
+                throw resultWithError[0];
+            }
         } catch (e: any) {
             Logger.error(`Could not set cache item ${key}: ${e.message as string}`, loggerCtx);
         }

+ 65 - 0
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -1182,6 +1182,71 @@ describe('OrderCalculator', () => {
                     expect(order.subTotalWithTax).toBe(5719);
                 });
             });
+
+            it(`clear previous promotion state before testing`, async () => {
+                const noLineDiscountsCondition = new PromotionCondition({
+                    args: {},
+                    code: 'no_other_discounts_condition',
+                    description: [{ languageCode: LanguageCode.en, value: '' }],
+                    check(_ctx, _order) {
+                        const linesToDiscount = _order.lines
+                            .filter(line => !line.adjustments.length)
+                            .map(line => line.id);
+                        return linesToDiscount.length ? { lines: linesToDiscount } : false;
+                    },
+                });
+                const discountMatchedLinesAction = new PromotionItemAction({
+                    code: 'discount_matched_lines_action',
+                    conditions: [noLineDiscountsCondition],
+                    description: [{ languageCode: LanguageCode.en, value: '' }],
+                    args: { discount: { type: 'int' } },
+                    execute(_ctx, orderLine, args, state) {
+                        if (state.no_other_discounts_condition.lines.includes(orderLine.id)) {
+                            return -args.discount;
+                        }
+                        return 0;
+                    },
+                });
+
+                const discountAllUndiscountedItems = new Promotion({
+                    id: 1,
+                    name: 'Discount all undiscounted items',
+                    conditions: [
+                        {
+                            code: noLineDiscountsCondition.code,
+                            args: [],
+                        },
+                    ],
+                    promotionConditions: [noLineDiscountsCondition],
+                    actions: [
+                        {
+                            code: discountMatchedLinesAction.code,
+                            args: [{ name: 'discount', value: '50' }],
+                        },
+                    ],
+                    promotionActions: [discountMatchedLinesAction],
+                });
+
+                const ctx = createRequestContext({ pricesIncludeTax: false });
+                const order = createOrder({
+                    ctx,
+                    lines: [
+                        {
+                            listPrice: 500,
+                            taxCategory: taxCategoryStandard,
+                            quantity: 2,
+                        },
+                    ],
+                });
+
+                await orderCalculator.applyPriceAdjustments(ctx, order, [discountAllUndiscountedItems]);
+                // everything gets discounted by 50
+                expect(order.subTotal).toBe(900);
+                // should still be discounted after changing quantity
+                order.lines[0].quantity = 3;
+                await orderCalculator.applyPriceAdjustments(ctx, order, [discountAllUndiscountedItems]);
+                expect(order.subTotal).toBe(1350);
+            });
         });
     });
 

+ 1 - 1
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -186,8 +186,8 @@ export class OrderCalculator {
         for (const line of order.lines) {
             // Must be re-calculated for each line, since the previous lines may have triggered promotions
             // which affected the order price.
-            const applicablePromotions = await filterAsync(promotions, p => p.test(ctx, order).then(Boolean));
             line.clearAdjustments();
+            const applicablePromotions = await filterAsync(promotions, p => p.test(ctx, order).then(Boolean));
 
             for (const promotion of applicablePromotions) {
                 let priceAdjusted = false;

+ 3 - 2
packages/core/src/service/services/channel.service.ts

@@ -111,7 +111,8 @@ export class ChannelService {
     /**
      * @description
      * Assigns a ChannelAware entity to the default Channel as well as any channel
-     * specified in the RequestContext.
+     * specified in the RequestContext. This method will not save the entity to the database, but
+     * assigns the `channels` property of the entity.
      */
     async assignToCurrentChannel<T extends ChannelAware & VendureEntity>(
         entity: T,
@@ -171,7 +172,7 @@ export class ChannelService {
 
     /**
      * @description
-     * Assigns the entity to the given Channels and saves.
+     * Assigns the entity to the given Channels and saves all changes to the database.
      */
     async assignToChannels<T extends ChannelAware & VendureEntity>(
         ctx: RequestContext,

+ 17 - 2
packages/core/src/service/services/product-variant.service.ts

@@ -11,7 +11,7 @@ import {
     RemoveProductVariantsFromChannelInput,
     UpdateProductVariantInput,
 } from '@vendure/common/lib/generated-types';
-import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
+import { CustomFieldsObject, ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { In, IsNull } from 'typeorm';
 
@@ -48,6 +48,7 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
 import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatorService } from '../helpers/translator/translator.service';
+import { patchEntity } from '../helpers/utils/patch-entity';
 import { samplesEach } from '../helpers/utils/samples-each';
 
 import { AssetService } from './asset.service';
@@ -552,6 +553,7 @@ export class ProductVariantService {
                         priceInput.price,
                         ctx.channelId,
                         priceInput.currencyCode,
+                        priceInput.customFields,
                     );
                 }
             }
@@ -570,6 +572,7 @@ export class ProductVariantService {
         price: number,
         channelId: ID,
         currencyCode?: CurrencyCode,
+        customFields?: CustomFieldsObject,
     ): Promise<ProductVariantPrice> {
         const { productVariantPriceUpdateStrategy } = this.configService.catalogOptions;
         const allPrices = await this.connection.getRepository(ctx, ProductVariantPrice).find({
@@ -608,10 +611,22 @@ export class ProductVariantService {
             );
             targetPrice = createdPrice;
         } else {
-            targetPrice.price = price;
+            patchEntity(targetPrice, {
+                price,
+                customFields: customFields || targetPrice.customFields,
+            });
             const updatedPrice = await this.connection
                 .getRepository(ctx, ProductVariantPrice)
                 .save(targetPrice);
+
+            if (customFields) {
+                await this.customFieldRelationService.updateRelations(
+                    ctx,
+                    ProductVariantPrice,
+                    customFields,
+                    updatedPrice,
+                );
+            }
             await this.eventBus.publish(new ProductVariantPriceEvent(ctx, [updatedPrice], 'updated'));
             additionalPricesToUpdate = await productVariantPriceUpdateStrategy.onPriceUpdated(
                 ctx,

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/create",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "bin": {
         "create": "./index.js"
@@ -27,14 +27,14 @@
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
-        "@vendure/core": "^3.1.1",
+        "@vendure/core": "^3.1.2",
         "rimraf": "^5.0.5",
         "ts-node": "^10.9.2",
         "typescript": "5.3.3"
     },
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.1.1",
+        "@vendure/common": "^3.1.2",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "fs-extra": "^11.2.0",

+ 1 - 1
packages/create/templates/docker-compose.hbs

@@ -63,7 +63,7 @@ services:
     # Checkout our advanced search plugin: https://vendure.io/hub/vendure-plus-advanced-search-plugin
     # To run the typesense container run "docker compose up -d typesense"
     typesense:
-        image: typesense/typesense:27
+        image: typesense/typesense:27.0
         command: [ '--data-dir', '/data', '--api-key', 'SuperSecret' ]
         ports:
             - "8208:8108"

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
     "name": "dev-server",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "private": true,
@@ -15,17 +15,17 @@
     },
     "dependencies": {
         "@nestjs/axios": "^3.0.2",
-        "@vendure/admin-ui-plugin": "^3.1.1",
-        "@vendure/asset-server-plugin": "^3.1.1",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
-        "@vendure/elasticsearch-plugin": "^3.1.1",
-        "@vendure/email-plugin": "^3.1.1",
+        "@vendure/admin-ui-plugin": "^3.1.2",
+        "@vendure/asset-server-plugin": "^3.1.2",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
+        "@vendure/elasticsearch-plugin": "^3.1.2",
+        "@vendure/email-plugin": "^3.1.2",
         "typescript": "5.3.3"
     },
     "devDependencies": {
-        "@vendure/testing": "^3.1.1",
-        "@vendure/ui-devkit": "^3.1.1",
+        "@vendure/testing": "^3.1.2",
+        "@vendure/ui-devkit": "^3.1.2",
         "commander": "^12.0.0",
         "concurrently": "^8.2.2",
         "csv-stringify": "^6.4.6",

+ 1 - 1
packages/elasticsearch-plugin/e2e/e2e-helpers.ts

@@ -63,8 +63,8 @@ export async function testMatchSearchTerm(client: SimpleGraphQLClient) {
         },
     );
     expect(result.search.items.map(i => i.productName)).toEqual([
-        'Instant Camera',
         'Camera Lens',
+        'Instant Camera',
         'SLR Camera',
     ]);
 }

+ 2 - 3
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -913,9 +913,8 @@ describe('Elasticsearch plugin', () => {
                     groupByProduct: true,
                     term: 'gaming',
                 });
-                expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
-                    { productId: 'T_3', enabled: false },
-                ]);
+                const t3 = result.search.items.find(i => i.productId === 'T_3');
+                expect(t3?.enabled).toEqual(false);
             });
 
             // https://github.com/vendure-ecommerce/vendure/issues/295

File diff suppressed because it is too large
+ 437 - 458
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts


+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/elasticsearch-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -26,8 +26,8 @@
         "fast-deep-equal": "^3.1.3"
     },
     "devDependencies": {
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

+ 8 - 4
packages/elasticsearch-plugin/src/build-elastic-body.spec.ts

@@ -21,7 +21,8 @@ describe('buildElasticBody()', () => {
                         multi_match: {
                             query: 'test',
                             type: 'best_fields',
-                            fields: ['productName^1', 'productVariantName^1', 'description^1', 'sku^1'],
+                            fuzziness: 'AUTO',
+                            fields: ['productName^5', 'productVariantName^5', 'description^1', 'sku^1'],
                         },
                     },
                 ],
@@ -389,7 +390,8 @@ describe('buildElasticBody()', () => {
                             multi_match: {
                                 query: 'test',
                                 type: 'best_fields',
-                                fields: ['productName^1', 'productVariantName^1', 'description^1', 'sku^1'],
+                                fuzziness: 'AUTO',
+                                fields: ['productName^5', 'productVariantName^5', 'description^1', 'sku^1'],
                             },
                         },
                     ],
@@ -427,7 +429,8 @@ describe('buildElasticBody()', () => {
                         multi_match: {
                             query: 'test',
                             type: 'phrase',
-                            fields: ['productName^1', 'productVariantName^1', 'description^1', 'sku^1'],
+                            fuzziness: 'AUTO',
+                            fields: ['productName^5', 'productVariantName^5', 'description^1', 'sku^1'],
                         },
                     },
                 ],
@@ -456,6 +459,7 @@ describe('buildElasticBody()', () => {
                         multi_match: {
                             query: 'test',
                             type: 'best_fields',
+                            fuzziness: 'AUTO',
                             fields: ['productName^3', 'productVariantName^4', 'description^2', 'sku^5'],
                         },
                     },
@@ -482,7 +486,7 @@ describe('buildElasticBody()', () => {
         const result = buildElasticBody({ term: 'test' }, config, CHANNEL_ID, LanguageCode.en);
         expect(result.script_fields).toEqual({
             test: {
-                script: 'doc[\'property\'].dummyScript(test)',
+                script: "doc['property'].dummyScript(test)",
             },
         });
     });

+ 4 - 3
packages/elasticsearch-plugin/src/build-elastic-body.ts

@@ -41,6 +41,7 @@ export function buildElasticBody(
             {
                 multi_match: {
                     query: term,
+                    fuzziness: 'AUTO',
                     type: searchConfig.multiMatchType,
                     fields: [
                         `productName^${searchConfig.boostFields.productName}`,
@@ -166,13 +167,13 @@ function createScriptFields(
             for (const name of fields) {
                 const scriptField = scriptFields[name];
                 if (scriptField.context === 'product' && groupByProduct === true) {
-                    (result )[name] = scriptField.scriptFn(input);
+                    result[name] = scriptField.scriptFn(input);
                 }
                 if (scriptField.context === 'variant' && groupByProduct === false) {
-                    (result )[name] = scriptField.scriptFn(input);
+                    result[name] = scriptField.scriptFn(input);
                 }
                 if (scriptField.context === 'both' || scriptField.context === undefined) {
-                    (result )[name] = scriptField.scriptFn(input);
+                    result[name] = scriptField.scriptFn(input);
                 }
             }
             return result;

+ 2 - 2
packages/elasticsearch-plugin/src/options.ts

@@ -728,8 +728,8 @@ export const defaultOptions: ElasticsearchRuntimeOptions = {
         totalItemsMaxSize: 10000,
         multiMatchType: 'best_fields',
         boostFields: {
-            productName: 1,
-            productVariantName: 1,
+            productName: 5,
+            productVariantName: 5,
             description: 1,
             sku: 1,
         },

+ 2 - 1
packages/elasticsearch-plugin/src/plugin.ts

@@ -97,7 +97,7 @@ function getCustomResolvers(options: ElasticsearchRuntimeOptions) {
  * ## Search API Extensions
  * This plugin extends the default search query of the Shop API, allowing richer querying of your product data.
  *
- * The [SearchResponse](/reference/graphql-api/admin/object-types/#searchresponse) type is extended with information
+ * The [SearchResponse](/reference/graphql-api/shop/object-types/#searchresponse) type is extended with information
  * about price ranges in the result set:
  * ```graphql
  * extend type SearchResponse {
@@ -119,6 +119,7 @@ function getCustomResolvers(options: ElasticsearchRuntimeOptions) {
  * extend input SearchInput {
  *     priceRange: PriceRangeInput
  *     priceRangeWithTax: PriceRangeInput
+ *     inStock: Boolean
  * }
  *
  * input PriceRangeInput {

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/email-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -34,8 +34,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/mjml": "^4.7.4",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
     }

+ 3 - 3
packages/harden-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/harden-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "graphql-query-complexity": "^0.12.0"
     },
     "devDependencies": {
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1"
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2"
     }
 }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/job-queue-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -23,8 +23,8 @@
     },
     "devDependencies": {
         "@google-cloud/pubsub": "^2.8.0",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
         "bullmq": "^5.4.2",
         "ioredis": "^5.3.2",
         "rimraf": "^5.0.5",

File diff suppressed because it is too large
+ 383 - 403
packages/payments-plugin/e2e/graphql/generated-admin-types.ts


File diff suppressed because it is too large
+ 628 - 649
packages/payments-plugin/e2e/graphql/generated-shop-types.ts


+ 4 - 0
packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts

@@ -175,6 +175,7 @@ describe('Stripe payments', () => {
             'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
             'metadata[orderId]': '1',
             'metadata[orderCode]': activeOrder?.code,
+            'metadata[languageCode]': 'en',
         });
         expect(createStripePaymentIntent).toEqual('test-client-secret');
     });
@@ -207,6 +208,7 @@ describe('Stripe payments', () => {
             'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
             'metadata[orderId]': '1',
             'metadata[orderCode]': activeOrder?.code,
+            'metadata[languageCode]': 'en',
             'metadata[customerEmail]': customers[0].emailAddress,
         });
         expect(createStripePaymentIntent).toEqual('test-client-secret');
@@ -240,6 +242,7 @@ describe('Stripe payments', () => {
             description: `Order #${activeOrder!.code} for ${activeOrder!.customer!.emailAddress}`,
             'automatic_payment_methods[enabled]': 'true',
             'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
+            'metadata[languageCode]': 'en',
             'metadata[orderId]': '1',
             'metadata[orderCode]': activeOrder?.code,
         });
@@ -280,6 +283,7 @@ describe('Stripe payments', () => {
             'automatic_payment_methods[enabled]': 'true',
             'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
             'metadata[orderId]': '1',
+            'metadata[languageCode]': 'en',
             'metadata[orderCode]': activeOrder?.code,
         });
         expect(connectedAccountHeader).toEqual('acct_connected');

+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -46,9 +46,9 @@
         "@mollie/api-client": "^3.7.0",
         "@types/braintree": "^3.3.11",
         "@types/localtunnel": "2.0.4",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1",
-        "@vendure/testing": "^3.1.1",
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2",
+        "@vendure/testing": "^3.1.2",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",

+ 2 - 2
packages/payments-plugin/src/braintree/braintree.handler.ts

@@ -106,12 +106,12 @@ async function processPayment(
         },
     });
     const extractMetadataFn = pluginOptions.extractMetadata ?? defaultExtractMetadataFn;
-    const metadata = extractMetadataFn(response.transaction);
+    const metadata = response.transaction && extractMetadataFn(response.transaction);
     if (!response.success) {
         return {
             amount,
             state: 'Declined' as const,
-            transactionId: response.transaction.id,
+            transactionId: response.transaction?.id,
             errorMessage: response.message,
             metadata,
         };

File diff suppressed because it is too large
+ 657 - 678
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts


+ 3 - 2
packages/payments-plugin/src/stripe/stripe-utils.ts

@@ -1,4 +1,4 @@
-import { CurrencyCode, Order } from '@vendure/core';
+import { CurrencyCode, Order, LanguageCode } from '@vendure/core';
 import Stripe from 'stripe';
 
 /**
@@ -34,7 +34,7 @@ function currencyHasFractionPart(currencyCode: CurrencyCode): boolean {
         currencyDisplay: 'symbol',
     }).formatToParts(123.45);
 
-    return !!parts.find(p => p.type === 'fraction');
+    return parts.some(p => p.type === 'fraction');
 }
 
 /**
@@ -46,6 +46,7 @@ export function isExpectedVendureStripeEventMetadata(metadata: Stripe.Metadata):
     channelToken: string;
     orderCode: string;
     orderId: string;
+    languageCode: LanguageCode;
 } {
     return !!metadata.channelToken && !!metadata.orderCode && !!metadata.orderId;
 }

+ 5 - 5
packages/payments-plugin/src/stripe/stripe.controller.ts

@@ -71,9 +71,9 @@ export class StripeController {
             );
         }
 
-        const { channelToken, orderCode, orderId } = metadata;
+        const { channelToken, orderCode, orderId, languageCode } = metadata;
 
-        const outerCtx = await this.createContext(channelToken, request);
+        const outerCtx = await this.createContext(channelToken, languageCode, request);
 
         await this.connection.withTransaction(outerCtx, async (ctx: RequestContext) => {
             const order = await this.orderService.findOneByCode(ctx, orderCode);
@@ -112,7 +112,7 @@ export class StripeController {
                 // channel to fail. Using a default channel avoids "entity-with-id-not-found" errors.
                 // See https://github.com/vendure-ecommerce/vendure/issues/3072
                 const defaultChannel = await this.channelService.getDefaultChannel(ctx);
-                const ctxWithDefaultChannel = await this.createContext(defaultChannel.token, request);
+                const ctxWithDefaultChannel = await this.createContext(defaultChannel.token, languageCode, request);
                 const transitionToStateResult = await this.orderService.transitionToState(
                     ctxWithDefaultChannel,
                     orderId,
@@ -159,12 +159,12 @@ export class StripeController {
         }
     }
 
-    private async createContext(channelToken: string, req: RequestWithRawBody): Promise<RequestContext> {
+    private async createContext(channelToken: string, languageCode: LanguageCode, req: RequestWithRawBody): Promise<RequestContext> {
         return this.requestContextService.create({
             apiType: 'admin',
             channelOrToken: channelToken,
             req,
-            languageCode: LanguageCode.en,
+            languageCode,
         });
     }
 

+ 1 - 0
packages/payments-plugin/src/stripe/stripe.service.ts

@@ -56,6 +56,7 @@ export class StripeService {
             channelToken: ctx.channel.token,
             orderId: order.id,
             orderCode: order.code,
+            languageCode: ctx.languageCode,
         });
 
         const allMetadata = {

+ 3 - 3
packages/sentry-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/sentry-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -22,7 +22,7 @@
     },
     "devDependencies": {
         "@sentry/node": "^7.106.1",
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1"
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2"
     }
 }

+ 3 - 3
packages/stellate-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/stellate-plugin",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "node-fetch": "^2.7.0"
     },
     "devDependencies": {
-        "@vendure/common": "^3.1.1",
-        "@vendure/core": "^3.1.1"
+        "@vendure/common": "^3.1.2",
+        "@vendure/core": "^3.1.2"
     }
 }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/testing",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "description": "End-to-end testing tools for Vendure projects",
     "keywords": [
         "vendure",
@@ -38,7 +38,7 @@
     },
     "dependencies": {
         "@graphql-typed-document-node/core": "^3.2.0",
-        "@vendure/common": "^3.1.1",
+        "@vendure/common": "^3.1.2",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "graphql": "~16.9.0",
@@ -51,7 +51,7 @@
         "@types/mysql": "^2.15.26",
         "@types/node-fetch": "^2.6.4",
         "@types/pg": "^8.11.2",
-        "@vendure/core": "^3.1.1",
+        "@vendure/core": "^3.1.2",
         "mysql": "^2.18.1",
         "pg": "^8.11.3",
         "rimraf": "^5.0.5",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/ui-devkit",
-    "version": "3.1.1",
+    "version": "3.1.2",
     "description": "A library for authoring Vendure Admin UI extensions",
     "keywords": [
         "vendure",
@@ -40,8 +40,8 @@
         "@angular/cli": "^17.2.3",
         "@angular/compiler": "^17.2.4",
         "@angular/compiler-cli": "^17.2.4",
-        "@vendure/admin-ui": "^3.1.1",
-        "@vendure/common": "^3.1.1",
+        "@vendure/admin-ui": "^3.1.2",
+        "@vendure/common": "^3.1.2",
         "chalk": "^4.1.0",
         "chokidar": "^3.6.0",
         "fs-extra": "^11.2.0",
@@ -52,7 +52,7 @@
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/core": "^3.1.1",
+        "@vendure/core": "^3.1.2",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "rimraf": "^5.0.5",

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff