Pārlūkot izejas kodu

Merge branch 'master' into minor

David Höck 1 gadu atpakaļ
vecāks
revīzija
60856ca103
100 mainītis faili ar 3893 papildinājumiem un 2862 dzēšanām
  1. 0 1
      .gitignore
  2. 5 0
      .vscode/extensions.json
  3. 22 0
      .vscode/settings.json
  4. 213 0
      CHANGELOG.md
  5. 2 2
      docs/docs/guides/developer-guide/events/index.mdx
  6. 1 1
      docs/docs/guides/developer-guide/plugins/index.mdx
  7. 2 2
      docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md
  8. 5 1
      docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md
  9. 37 0
      docs/docs/guides/extending-the-admin-ui/getting-started/index.md
  10. 13 13
      docs/docs/reference/admin-ui-api/alerts/alert-config.md
  11. 1 1
      docs/docs/reference/core-plugins/email-plugin/email-event-handler-with-async-data.md
  12. 9 4
      docs/docs/reference/core-plugins/email-plugin/email-event-handler.md
  13. 1 1
      docs/docs/reference/core-plugins/email-plugin/index.md
  14. 2 2
      docs/docs/reference/typescript-api/data-access/entity-hydrator.md
  15. 2 8
      docs/docs/reference/typescript-api/data-access/list-query-builder.md
  16. 5 5
      docs/docs/reference/typescript-api/data-access/transactional-connection.md
  17. 1 1
      docs/docs/reference/typescript-api/entities/authenticated-session.md
  18. 46 4
      docs/docs/reference/typescript-api/entities/channel.md
  19. 8 1
      docs/docs/reference/typescript-api/entities/customer-group.md
  20. 1 1
      docs/docs/reference/typescript-api/entities/customer.md
  21. 1 1
      docs/docs/reference/typescript-api/entities/facet.md
  22. 1 1
      docs/docs/reference/typescript-api/entities/order-line-reference.md
  23. 32 8
      docs/docs/reference/typescript-api/entities/order-line.md
  24. 1 1
      docs/docs/reference/typescript-api/entities/payment-method.md
  25. 8 1
      docs/docs/reference/typescript-api/entities/product-variant.md
  26. 1 1
      docs/docs/reference/typescript-api/entities/product.md
  27. 2 2
      docs/docs/reference/typescript-api/entities/promotion.md
  28. 1 1
      docs/docs/reference/typescript-api/entities/role.md
  29. 8 1
      docs/docs/reference/typescript-api/entities/seller.md
  30. 8 1
      docs/docs/reference/typescript-api/entities/shipping-line.md
  31. 1 1
      docs/docs/reference/typescript-api/entities/shipping-method.md
  32. 9 2
      docs/docs/reference/typescript-api/entities/stock-location.md
  33. 4 4
      docs/docs/reference/typescript-api/entities/stock-movement.md
  34. 8 1
      docs/docs/reference/typescript-api/entities/tax-category.md
  35. 3 3
      docs/docs/reference/typescript-api/entities/tax-rate.md
  36. 8 1
      docs/docs/reference/typescript-api/entities/user.md
  37. 22 1
      docs/docs/reference/typescript-api/entities/zone.md
  38. 2 2
      docs/docs/reference/typescript-api/job-queue/index.md
  39. 3 3
      docs/docs/reference/typescript-api/job-queue/job-queue-strategy.md
  40. 2 2
      docs/docs/reference/typescript-api/job-queue/sql-job-queue-strategy.md
  41. 3 3
      docs/docs/reference/typescript-api/job-queue/types.md
  42. 2 2
      docs/docs/reference/typescript-api/migration/generate-migration.md
  43. 1 1
      docs/docs/reference/typescript-api/migration/revert-last-migration.md
  44. 1 1
      docs/docs/reference/typescript-api/migration/run-migrations.md
  45. 1 1
      docs/docs/reference/typescript-api/orders/order-process.md
  46. 1 1
      docs/docs/reference/typescript-api/service-helpers/order-calculator.md
  47. 1 1
      docs/docs/reference/typescript-api/service-helpers/product-price-applicator.md
  48. 25 25
      docs/docs/reference/typescript-api/services/customer-service.md
  49. 11 11
      docs/docs/reference/typescript-api/services/fulfillment-service.md
  50. 1 1
      docs/docs/reference/typescript-api/services/global-settings-service.md
  51. 1 1
      docs/docs/reference/typescript-api/services/order-service.md
  52. 5 5
      docs/docs/reference/typescript-api/services/payment-method-service.md
  53. 3 3
      docs/docs/reference/typescript-api/services/tax-rate-service.md
  54. 1 1
      lerna.json
  55. 83 70
      package-lock.json
  56. 4 4
      packages/admin-ui-plugin/package.json
  57. 2 2
      packages/admin-ui/package.json
  58. 5 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  59. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  60. 6 2
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts
  61. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.html
  62. 7 1
      packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.ts
  63. 11 5
      packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts
  64. 2 12
      packages/admin-ui/src/lib/customer/src/customer.routes.ts
  65. 2 1
      packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.ts
  66. 6 3
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  67. 622 588
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  68. 3 3
      packages/asset-server-plugin/package.json
  69. 6 4
      packages/cli/package.json
  70. 1 1
      packages/cli/src/commands/add/add.ts
  71. 11 6
      packages/cli/src/commands/add/api-extension/add-api-extension.ts
  72. 13 6
      packages/cli/src/commands/add/codegen/add-codegen.ts
  73. 8 7
      packages/cli/src/commands/add/codegen/codegen-config-ref.ts
  74. 27 14
      packages/cli/src/commands/add/entity/add-entity.ts
  75. 5 2
      packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts
  76. 5 5
      packages/cli/src/commands/add/job-queue/add-job-queue.ts
  77. 88 37
      packages/cli/src/commands/add/plugin/create-new-plugin.ts
  78. 43 8
      packages/cli/src/commands/add/service/add-service.ts
  79. 21 7
      packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts
  80. 71 5
      packages/cli/src/commands/migrate/generate-migration/generate-migration.ts
  81. 26 3
      packages/cli/src/commands/migrate/load-vendure-config-file.ts
  82. 2 2
      packages/cli/src/commands/migrate/revert-migration/revert-migration.ts
  83. 2 2
      packages/cli/src/commands/migrate/run-migration/run-migration.ts
  84. 69 5
      packages/cli/src/shared/package-json-ref.ts
  85. 8 3
      packages/cli/src/shared/shared-prompts.ts
  86. 50 1
      packages/cli/src/shared/vendure-plugin-ref.ts
  87. 34 9
      packages/cli/src/utilities/ast-utils.ts
  88. 1 1
      packages/common/package.json
  89. 680 647
      packages/common/src/generated-shop-types.ts
  90. 5 0
      packages/common/src/generated-types.ts
  91. 16 2
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  92. 28 20
      packages/core/e2e/facet.e2e-spec.ts
  93. 622 588
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  94. 651 618
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  95. 3 2
      packages/core/package.json
  96. 1 1
      packages/core/src/api/config/configure-graphql-module.ts
  97. 12 1
      packages/core/src/api/config/generate-resolvers.ts
  98. 27 8
      packages/core/src/api/config/graphql-custom-fields.ts
  99. 11 9
      packages/core/src/bootstrap.ts
  100. 33 2
      packages/core/src/config/config.service.ts

+ 0 - 1
.gitignore

@@ -18,7 +18,6 @@ docs/static/intro.js*
 docs/static/intro.css*
 docs/public
 docs/data/build.json
-.vscode/
 yarn-error.log
 e2e-common/ports.json
 

+ 5 - 0
.vscode/extensions.json

@@ -0,0 +1,5 @@
+{
+    "recommendations": [
+        "vivaxy.vscode-conventional-commits"
+    ]
+}

+ 22 - 0
.vscode/settings.json

@@ -0,0 +1,22 @@
+{
+    "conventionalCommits.scopes": [
+        "admin-ui",
+        "admin-ui-plugin",
+        "asset-server-plugin",
+        "cli",
+        "common",
+        "core",
+        "create",
+        "dev-server",
+        "elasticsearch-plugin",
+        "email-plugin",
+        "harden-plugin",
+        "job-queue-plugin",
+        "sentry-plugin",
+        "stellate-plugin",
+        "testing",
+        "ui-devkit",
+        "repo"
+    ],
+    "conventionalCommits.gitmoji": false
+}

+ 213 - 0
CHANGELOG.md

@@ -1,3 +1,216 @@
+## <small>2.2.4 (2024-05-08)</small>
+
+
+#### Fixes
+
+* **cli** Fix error when adding non-translatable entity ([b6de420](https://github.com/vendure-ecommerce/vendure/commit/b6de420))
+* **cli** Fix plugin path detection when only 1 plugin exists ([4f31067](https://github.com/vendure-ecommerce/vendure/commit/4f31067))
+* **cli** Hide output when installing packages ([dea7ba7](https://github.com/vendure-ecommerce/vendure/commit/dea7ba7))
+* **core** Correctly generate customFields field for custom entity types ([58943e3](https://github.com/vendure-ecommerce/vendure/commit/58943e3))
+* **core** Fix query on translation filter ([5b42cde](https://github.com/vendure-ecommerce/vendure/commit/5b42cde)), closes [#2833](https://github.com/vendure-ecommerce/vendure/issues/2833)
+* **core** Handle edge case of existing customFields def on gql type ([a889320](https://github.com/vendure-ecommerce/vendure/commit/a889320))
+* **core** Optimize search index update queries (#2808) ([e83dfc6](https://github.com/vendure-ecommerce/vendure/commit/e83dfc6)), closes [#2808](https://github.com/vendure-ecommerce/vendure/issues/2808)
+* **core** Use the shop cookie name for default route (#2839) ([429f88d](https://github.com/vendure-ecommerce/vendure/commit/429f88d)), closes [#2839](https://github.com/vendure-ecommerce/vendure/issues/2839)
+
+#### Features
+
+* **core** Add French translations for API messages (#2837) ([e45e7b7](https://github.com/vendure-ecommerce/vendure/commit/e45e7b7)), closes [#2837](https://github.com/vendure-ecommerce/vendure/issues/2837)
+
+## <small>2.2.3 (2024-05-02)</small>
+
+
+#### Fixes
+
+* **admin-ui** Fix creating customer on draft order ([64b9c60](https://github.com/vendure-ecommerce/vendure/commit/64b9c60))
+* **cli** Fix api extension location detection ([a5fdd86](https://github.com/vendure-ecommerce/vendure/commit/a5fdd86))
+* **cli** Fix maximum call stack error ([464e68b](https://github.com/vendure-ecommerce/vendure/commit/464e68b)), closes [#2819](https://github.com/vendure-ecommerce/vendure/issues/2819)
+* **cli** Fix relative import path for parent dirs ([9379d73](https://github.com/vendure-ecommerce/vendure/commit/9379d73))
+* **cli** Fix translatable entity imports ([e6c9ba8](https://github.com/vendure-ecommerce/vendure/commit/e6c9ba8))
+* **cli** Improve detection of migration file location ([359b236](https://github.com/vendure-ecommerce/vendure/commit/359b236))
+* **cli** Improve plugin generation in monorepos ([40000a4](https://github.com/vendure-ecommerce/vendure/commit/40000a4))
+* **cli** Improve support for multiple tsconfig files ([d871eb7](https://github.com/vendure-ecommerce/vendure/commit/d871eb7))
+* **cli** Improve support for pnpm projects ([4eaf7ff](https://github.com/vendure-ecommerce/vendure/commit/4eaf7ff))
+* **cli** Include plugin options in service constructor ([a77251e](https://github.com/vendure-ecommerce/vendure/commit/a77251e))
+* **core** Improve message for custom field schema errors ([7ac4ac9](https://github.com/vendure-ecommerce/vendure/commit/7ac4ac9))
+* **core** Make featuredAsset optional on collection duplicator (#2824) ([bb10b4c](https://github.com/vendure-ecommerce/vendure/commit/bb10b4c)), closes [#2824](https://github.com/vendure-ecommerce/vendure/issues/2824)
+
+## <small>2.2.2 (2024-04-25)</small>
+
+This release contains no changes - it was published to fix a mistake 
+in the publishing of the `@vendure/admin-ui@2.2.1` package.
+
+## <small>2.2.1 (2024-04-25)</small>
+
+
+#### Fixes
+
+* **admin-ui** Fix code snippet for addNavMenuSection ([d1da9ae](https://github.com/vendure-ecommerce/vendure/commit/d1da9ae)), closes [#2807](https://github.com/vendure-ecommerce/vendure/issues/2807)
+* **admin-ui** Fix custom tabs in customer list ([482bca9](https://github.com/vendure-ecommerce/vendure/commit/482bca9)), closes [#2788](https://github.com/vendure-ecommerce/vendure/issues/2788)
+* **admin-ui** Fix default quantity when adding item to order ([277c17e](https://github.com/vendure-ecommerce/vendure/commit/277c17e))
+* **admin-ui** Fix error preventing f key usage in code editor ([6e68226](https://github.com/vendure-ecommerce/vendure/commit/6e68226)), closes [#2771](https://github.com/vendure-ecommerce/vendure/issues/2771)
+* **admin-ui** Fix facet value selection with duplicated labels ([3a9c317](https://github.com/vendure-ecommerce/vendure/commit/3a9c317))
+* **cli** Improve support for migrations in monorepo setups ([3fbf4e4](https://github.com/vendure-ecommerce/vendure/commit/3fbf4e4))
+* **cli** Load .env files automatically for migrations ([777a5a5](https://github.com/vendure-ecommerce/vendure/commit/777a5a5)), closes [#2802](https://github.com/vendure-ecommerce/vendure/issues/2802)
+* **core** Add missing semver dependency ([91484a2](https://github.com/vendure-ecommerce/vendure/commit/91484a2))
+* **core** Add surcharge taxLines to taxSummary (#2798) ([d0166a2](https://github.com/vendure-ecommerce/vendure/commit/d0166a2)), closes [#2798](https://github.com/vendure-ecommerce/vendure/issues/2798)
+* **core** Fix duplication of product without featured asset ([f5e866b](https://github.com/vendure-ecommerce/vendure/commit/f5e866b)), closes [#2803](https://github.com/vendure-ecommerce/vendure/issues/2803)
+* **core** Fix findOneInChannel with relations object ([b9eb7db](https://github.com/vendure-ecommerce/vendure/commit/b9eb7db)), closes [#2809](https://github.com/vendure-ecommerce/vendure/issues/2809)
+* **core** Fix importer asset channel handling (#2801) ([c7a28b7](https://github.com/vendure-ecommerce/vendure/commit/c7a28b7)), closes [#2801](https://github.com/vendure-ecommerce/vendure/issues/2801)
+* **core** Improved loading of eager-loaded custom field relations ([025a9c7](https://github.com/vendure-ecommerce/vendure/commit/025a9c7)), closes [#2775](https://github.com/vendure-ecommerce/vendure/issues/2775) [#2687](https://github.com/vendure-ecommerce/vendure/issues/2687)
+* **core** Publish OrderEvent when order is deleted ([55f68780](https://github.com/vendure-ecommerce/vendure/commit/55f68780))
+* **core** Remove original entityTable from channels sql request (#2791) ([9c1cb16](https://github.com/vendure-ecommerce/vendure/commit/9c1cb16)), closes [#2791](https://github.com/vendure-ecommerce/vendure/issues/2791)
+* **core** Update relations on Stock Location update (#2805) ([47b1116](https://github.com/vendure-ecommerce/vendure/commit/47b1116)), closes [#2805](https://github.com/vendure-ecommerce/vendure/issues/2805) [#2804](https://github.com/vendure-ecommerce/vendure/issues/2804)
+
+## 2.2.0 (2024-04-15)
+
+
+#### Fixes
+
+* **admin-ui** Add missing RTL compatibility to some admin-ui components (#2451) ([96eb96e](https://github.com/vendure-ecommerce/vendure/commit/96eb96e)), closes [#2451](https://github.com/vendure-ecommerce/vendure/issues/2451)
+* **admin-ui** Fix alerts service registration ([04dcaab](https://github.com/vendure-ecommerce/vendure/commit/04dcaab))
+* **admin-ui** Fix alignment of order modification history item ([e4a172c](https://github.com/vendure-ecommerce/vendure/commit/e4a172c))
+* **admin-ui** Fix dark mode layout ([893a913](https://github.com/vendure-ecommerce/vendure/commit/893a913)), closes [#2745](https://github.com/vendure-ecommerce/vendure/issues/2745)
+* **admin-ui** Fix display of alert on login screen ([7e66d85](https://github.com/vendure-ecommerce/vendure/commit/7e66d85))
+* **admin-ui** Fix header for promotion list (#2782) ([e46b36d](https://github.com/vendure-ecommerce/vendure/commit/e46b36d)), closes [#2782](https://github.com/vendure-ecommerce/vendure/issues/2782)
+* **admin-ui** Fix saving entities with custom field relations ([80f1f95](https://github.com/vendure-ecommerce/vendure/commit/80f1f95))
+* **admin-ui** Improve styling of form field wrapper ([5263c2d](https://github.com/vendure-ecommerce/vendure/commit/5263c2d))
+* **admin-ui** Improved support for modifying OrderLine custom fields ([0750fb1](https://github.com/vendure-ecommerce/vendure/commit/0750fb1)), closes [#2641](https://github.com/vendure-ecommerce/vendure/issues/2641)
+* **cli** Improve type generation for interfaces & gql types ([f26a0bf](https://github.com/vendure-ecommerce/vendure/commit/f26a0bf))
+* **cli** Removed channelId from generated findAll method ([5e3d831](https://github.com/vendure-ecommerce/vendure/commit/5e3d831))
+* **cli** Various fixes to CLI add commands ([4ea7711](https://github.com/vendure-ecommerce/vendure/commit/4ea7711))
+* **core** Add missing jobOptions to a strategy `jobQueueStrategy.add` function (#2770) ([f869a17](https://github.com/vendure-ecommerce/vendure/commit/f869a17)), closes [#2770](https://github.com/vendure-ecommerce/vendure/issues/2770)
+* **core** Add missing reverse side relations (#2781) ([bdf2329](https://github.com/vendure-ecommerce/vendure/commit/bdf2329)), closes [#2781](https://github.com/vendure-ecommerce/vendure/issues/2781)
+* **core** Correctly return custom field relation scalar fields ([1280cf3](https://github.com/vendure-ecommerce/vendure/commit/1280cf3))
+* **core** Fix amount being sent to payment handler refund method ([b6a5691](https://github.com/vendure-ecommerce/vendure/commit/b6a5691))
+* **core** Fix deleted product option groups can't be deleted again (#2706) ([16add4a](https://github.com/vendure-ecommerce/vendure/commit/16add4a)), closes [#2706](https://github.com/vendure-ecommerce/vendure/issues/2706)
+* **core** Fix edge case with cached tax zone ([e543e5e](https://github.com/vendure-ecommerce/vendure/commit/e543e5e))
+* **core** Fix error in joining list query relations ([33db45d](https://github.com/vendure-ecommerce/vendure/commit/33db45d))
+* **core** Fix floating promise & missed eventBus (#2779) ([603a36e](https://github.com/vendure-ecommerce/vendure/commit/603a36e)), closes [#2779](https://github.com/vendure-ecommerce/vendure/issues/2779)
+* **core** Fix hydration of product variant prices ([7adb115](https://github.com/vendure-ecommerce/vendure/commit/7adb115))
+* **core** Fix self-referencing relations `Not unique table/alias` (#2740) ([357ba49](https://github.com/vendure-ecommerce/vendure/commit/357ba49)), closes [#2740](https://github.com/vendure-ecommerce/vendure/issues/2740) [#2738](https://github.com/vendure-ecommerce/vendure/issues/2738)
+* **core** Fix typing on ProductOptionGroupService.create() method ([8fe24da](https://github.com/vendure-ecommerce/vendure/commit/8fe24da)), closes [#2577](https://github.com/vendure-ecommerce/vendure/issues/2577)
+* **core** Fix undefined reference error in product variant resolver ([5afa6bc](https://github.com/vendure-ecommerce/vendure/commit/5afa6bc))
+* **core** Persist custom field relations on TaxRate ([7eaa641](https://github.com/vendure-ecommerce/vendure/commit/7eaa641))
+* **core** Remove empty customFields relations from getMissingRelations in entity-hydrator (#2765) ([1c44113](https://github.com/vendure-ecommerce/vendure/commit/1c44113)), closes [#2765](https://github.com/vendure-ecommerce/vendure/issues/2765)
+* **core** Revert change to SQLJobQueueStrategy update mechanism ([a1e7730](https://github.com/vendure-ecommerce/vendure/commit/a1e7730))
+* **core** Wrap nextOrderStates in transaction ([ed9539d](https://github.com/vendure-ecommerce/vendure/commit/ed9539d))
+* **email-plugin** Remove unwanted currency symbols in template (#2536) ([639fa0f](https://github.com/vendure-ecommerce/vendure/commit/639fa0f)), closes [#2536](https://github.com/vendure-ecommerce/vendure/issues/2536)
+* **ui-devkit** Add call to exit in sigint handler (#2558) ([bfd9281](https://github.com/vendure-ecommerce/vendure/commit/bfd9281)), closes [#2558](https://github.com/vendure-ecommerce/vendure/issues/2558)
+
+#### Perf
+
+* **cli** Lazy load commands to improve startup time ([ec2f497](https://github.com/vendure-ecommerce/vendure/commit/ec2f497))
+* **core** Database access performance & edge case fixes  (#2744) ([48b239b](https://github.com/vendure-ecommerce/vendure/commit/48b239b)), closes [#2744](https://github.com/vendure-ecommerce/vendure/issues/2744)
+* **core** Optimization for assignToChannels method (#2743) ([c69e4ac](https://github.com/vendure-ecommerce/vendure/commit/c69e4ac)), closes [#2743](https://github.com/vendure-ecommerce/vendure/issues/2743)
+* **core** Upgrade EntityHydrator performance to any hydrate call (#2742) ([77233cd](https://github.com/vendure-ecommerce/vendure/commit/77233cd)), closes [#2742](https://github.com/vendure-ecommerce/vendure/issues/2742)
+* **core** Upgrade sql requests for more performant memory usage with big datasets (#2741) ([65888cb](https://github.com/vendure-ecommerce/vendure/commit/65888cb)), closes [#2741](https://github.com/vendure-ecommerce/vendure/issues/2741)
+
+#### Features
+
+* **admin-ui** Add bulk facet value editing to product variant list ([5ad41bf](https://github.com/vendure-ecommerce/vendure/commit/5ad41bf))
+* **admin-ui** Add React RichTextEditor component & hook (#2675) ([68e0fa5](https://github.com/vendure-ecommerce/vendure/commit/68e0fa5)), closes [#2675](https://github.com/vendure-ecommerce/vendure/issues/2675)
+* **admin-ui** Add React useLazyQuery hook (#2498) ([757635b](https://github.com/vendure-ecommerce/vendure/commit/757635b)), closes [#2498](https://github.com/vendure-ecommerce/vendure/issues/2498)
+* **admin-ui** Add support for custom action bar dropdown menus ([4d8bc74](https://github.com/vendure-ecommerce/vendure/commit/4d8bc74)), closes [#2678](https://github.com/vendure-ecommerce/vendure/issues/2678)
+* **admin-ui** Add support for Norwegian Bokmål (#2611) ([00d5315](https://github.com/vendure-ecommerce/vendure/commit/00d5315)), closes [#2611](https://github.com/vendure-ecommerce/vendure/issues/2611)
+* **admin-ui** Add support for permissions on custom fields ([94e0c42](https://github.com/vendure-ecommerce/vendure/commit/94e0c42)), closes [#2671](https://github.com/vendure-ecommerce/vendure/issues/2671)
+* **admin-ui** Allow configuration of available locales (#2550) ([dfddf0f](https://github.com/vendure-ecommerce/vendure/commit/dfddf0f)), closes [#2550](https://github.com/vendure-ecommerce/vendure/issues/2550)
+* **admin-ui** Allow customer to be reassigned to order ([a9a596e](https://github.com/vendure-ecommerce/vendure/commit/a9a596e)), closes [#2505](https://github.com/vendure-ecommerce/vendure/issues/2505)
+* **admin-ui** Allow order shipping method to be modified ([7f34329](https://github.com/vendure-ecommerce/vendure/commit/7f34329)), closes [#978](https://github.com/vendure-ecommerce/vendure/issues/978)
+* **admin-ui** Channel aware picker ([fd92b4c](https://github.com/vendure-ecommerce/vendure/commit/fd92b4c))
+* **admin-ui** Enable multiple refunds on an order modification ([9b3aa65](https://github.com/vendure-ecommerce/vendure/commit/9b3aa65)), closes [#2393](https://github.com/vendure-ecommerce/vendure/issues/2393)
+* **admin-ui** Expose `entity$` observable on action bar context ([3f07179](https://github.com/vendure-ecommerce/vendure/commit/3f07179))
+* **admin-ui** Expose `registerAlert` provider for custom UI alerts ([698ea0c](https://github.com/vendure-ecommerce/vendure/commit/698ea0c)), closes [#2503](https://github.com/vendure-ecommerce/vendure/issues/2503)
+* **admin-ui** Implement UI for entity duplication ([7aa0d16](https://github.com/vendure-ecommerce/vendure/commit/7aa0d16)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **admin-ui** Improve layout & styling of order payment cards ([4a8b91a](https://github.com/vendure-ecommerce/vendure/commit/4a8b91a))
+* **admin-ui** Improve styling of order/customer history timeline ([aeebbdd](https://github.com/vendure-ecommerce/vendure/commit/aeebbdd))
+* **admin-ui** Improved refund dialog ([ccbf9ec](https://github.com/vendure-ecommerce/vendure/commit/ccbf9ec)), closes [#2393](https://github.com/vendure-ecommerce/vendure/issues/2393)
+* **admin-ui** Product & variant lists can be filtered by name & sku ([74293cb](https://github.com/vendure-ecommerce/vendure/commit/74293cb)), closes [#2519](https://github.com/vendure-ecommerce/vendure/issues/2519)
+* **admin-ui** Support custom fields on custom entities ([74aeb86](https://github.com/vendure-ecommerce/vendure/commit/74aeb86)), closes [#1848](https://github.com/vendure-ecommerce/vendure/issues/1848)
+* **admin-ui** Update Angular to v17.2 ([6f6a7af](https://github.com/vendure-ecommerce/vendure/commit/6f6a7af))
+* **admin-ui** Update Clarity UI library to v17 ([44cfd95](https://github.com/vendure-ecommerce/vendure/commit/44cfd95))
+* **admin-ui** Updated order modification screen with improved UX ([ac4c762](https://github.com/vendure-ecommerce/vendure/commit/ac4c762))
+* **asset-server-plugin** Add `q` query param for dynamic quality ([b96289b](https://github.com/vendure-ecommerce/vendure/commit/b96289b))
+* **asset-server-plugin** Update Sharp to v0.33.2 ([f3d45a0](https://github.com/vendure-ecommerce/vendure/commit/f3d45a0))
+* **cli** Add API extension command ([41675a4](https://github.com/vendure-ecommerce/vendure/commit/41675a4))
+* **cli** Add codegen command ([de5544c](https://github.com/vendure-ecommerce/vendure/commit/de5544c))
+* **cli** Add job queue command ([2193a77](https://github.com/vendure-ecommerce/vendure/commit/2193a77))
+* **cli** Add service command ([e29accc](https://github.com/vendure-ecommerce/vendure/commit/e29accc))
+* **cli** Allow chaining features onto a newly-created plugin ([5b32c59](https://github.com/vendure-ecommerce/vendure/commit/5b32c59))
+* **cli** Allow new entity features to be selected ([74c69dd](https://github.com/vendure-ecommerce/vendure/commit/74c69dd))
+* **cli** Allow new plugin dir to be specified ([4ae12e7](https://github.com/vendure-ecommerce/vendure/commit/4ae12e7))
+* **cli** Implement "add entity" command ([ad87531](https://github.com/vendure-ecommerce/vendure/commit/ad87531))
+* **cli** Implement "add" command for ui extensions ([795b013](https://github.com/vendure-ecommerce/vendure/commit/795b013))
+* **cli** Implement migrations in CLI ([9860abd](https://github.com/vendure-ecommerce/vendure/commit/9860abd))
+* **core** A support for custom fields on ProductVariantPrice (#2654) ([e7f0fe2](https://github.com/vendure-ecommerce/vendure/commit/e7f0fe2)), closes [#2654](https://github.com/vendure-ecommerce/vendure/issues/2654)
+* **core** Accept `maxAge` and `expires` options in cookie config ([c903388](https://github.com/vendure-ecommerce/vendure/commit/c903388)), closes [#2518](https://github.com/vendure-ecommerce/vendure/issues/2518)
+* **core** Add `amount` field to `RefundOrderInput` ([fe43b4a](https://github.com/vendure-ecommerce/vendure/commit/fe43b4a)), closes [#2393](https://github.com/vendure-ecommerce/vendure/issues/2393)
+* **core** Add `gracefulShutdownTimeout` to DefaultJobQueuePlugin ([cba06e0](https://github.com/vendure-ecommerce/vendure/commit/cba06e0))
+* **core** Add `precision` property to MoneyStrategy ([c33ba63](https://github.com/vendure-ecommerce/vendure/commit/c33ba63))
+* **core** Add cancellation handling to built-in jobs ([c8022be](https://github.com/vendure-ecommerce/vendure/commit/c8022be)), closes [#1127](https://github.com/vendure-ecommerce/vendure/issues/1127) [#2650](https://github.com/vendure-ecommerce/vendure/issues/2650)
+* **core** Add Missing inherit filters field in collection import (#2534) ([ef64db7](https://github.com/vendure-ecommerce/vendure/commit/ef64db7)), closes [#2534](https://github.com/vendure-ecommerce/vendure/issues/2534) [#2484](https://github.com/vendure-ecommerce/vendure/issues/2484)
+* **core** Add SKU filtering to `products` list in Admin API ([876d1ec](https://github.com/vendure-ecommerce/vendure/commit/876d1ec)), closes [#2519](https://github.com/vendure-ecommerce/vendure/issues/2519)
+* **core** Add support for permissions on custom fields ([1c9f8f9](https://github.com/vendure-ecommerce/vendure/commit/1c9f8f9)), closes [#2671](https://github.com/vendure-ecommerce/vendure/issues/2671)
+* **core** Allow order shipping method to be modified ([400d78a](https://github.com/vendure-ecommerce/vendure/commit/400d78a)), closes [#978](https://github.com/vendure-ecommerce/vendure/issues/978)
+* **core** Enable multiple refunds on an order modification ([cf91a9e](https://github.com/vendure-ecommerce/vendure/commit/cf91a9e)), closes [#2393](https://github.com/vendure-ecommerce/vendure/issues/2393)
+* **core** Enable setting different cookie name for Shop & Admin API (#2482) ([ae91650](https://github.com/vendure-ecommerce/vendure/commit/ae91650)), closes [#2482](https://github.com/vendure-ecommerce/vendure/issues/2482)
+* **core** Export order state machine ([138d9ff](https://github.com/vendure-ecommerce/vendure/commit/138d9ff))
+* **core** Expose `enabled` field for Product in shop api (#2541) ([f6f2975](https://github.com/vendure-ecommerce/vendure/commit/f6f2975)), closes [#2541](https://github.com/vendure-ecommerce/vendure/issues/2541)
+* **core** Expose additional bootstrap options (#2568) ([3b6d6ab](https://github.com/vendure-ecommerce/vendure/commit/3b6d6ab)), closes [#2568](https://github.com/vendure-ecommerce/vendure/issues/2568)
+* **core** Expose entityCustomFields query ([01f9d44](https://github.com/vendure-ecommerce/vendure/commit/01f9d44)), closes [#1848](https://github.com/vendure-ecommerce/vendure/issues/1848)
+* **core** Implement `setOrderCustomer` mutation ([26e77d7](https://github.com/vendure-ecommerce/vendure/commit/26e77d7)), closes [#2505](https://github.com/vendure-ecommerce/vendure/issues/2505)
+* **core** Implement collection duplicator ([d457851](https://github.com/vendure-ecommerce/vendure/commit/d457851)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **core** Implement complex boolean list filtering for PaginatedLists ([c4bd484](https://github.com/vendure-ecommerce/vendure/commit/c4bd484)), closes [#2594](https://github.com/vendure-ecommerce/vendure/issues/2594)
+* **core** Implement facet duplicator ([8d20847](https://github.com/vendure-ecommerce/vendure/commit/8d20847)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **core** Implement internal support for entity duplication ([477fe93](https://github.com/vendure-ecommerce/vendure/commit/477fe93)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **core** Implement new blocking event handler API ([1c69499](https://github.com/vendure-ecommerce/vendure/commit/1c69499)), closes [#2735](https://github.com/vendure-ecommerce/vendure/issues/2735)
+* **core** Implement product duplicator ([6ac43d9](https://github.com/vendure-ecommerce/vendure/commit/6ac43d9)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **core** Implement promotion duplicator ([da58b0b](https://github.com/vendure-ecommerce/vendure/commit/da58b0b)), closes [#627](https://github.com/vendure-ecommerce/vendure/issues/627)
+* **core** Improve cancellation mechanism of DefaultJobQueuePlugin ([cba069b](https://github.com/vendure-ecommerce/vendure/commit/cba069b)), closes [#1127](https://github.com/vendure-ecommerce/vendure/issues/1127) [#2650](https://github.com/vendure-ecommerce/vendure/issues/2650)
+* **core** Introduce ErrorHandlerStrategy ([066e524](https://github.com/vendure-ecommerce/vendure/commit/066e524))
+* **core** Introduce new `ProductVariantPriceEvent` ([aa4eeb8](https://github.com/vendure-ecommerce/vendure/commit/aa4eeb8))
+* **core** Introduce new `ProductVariantPriceUpdateStrategy` ([9099f35](https://github.com/vendure-ecommerce/vendure/commit/9099f35)), closes [#2651](https://github.com/vendure-ecommerce/vendure/issues/2651)
+* **core** Pass ctx to job queue strategy add (#2759) ([3909251](https://github.com/vendure-ecommerce/vendure/commit/3909251)), closes [#2759](https://github.com/vendure-ecommerce/vendure/issues/2759) [#2758](https://github.com/vendure-ecommerce/vendure/issues/2758)
+* **core** Pass RequestContext to custom field validate function ([2314ff6](https://github.com/vendure-ecommerce/vendure/commit/2314ff6)), closes [#2408](https://github.com/vendure-ecommerce/vendure/issues/2408)
+* **core** Update NestJS to latest version (v10.3.3) ([573ae18](https://github.com/vendure-ecommerce/vendure/commit/573ae18))
+* **core** Update TypeORM to v0.3.20 ([0afc94e](https://github.com/vendure-ecommerce/vendure/commit/0afc94e))
+* **core** Update TypeScript version to v5.1.6 ([2f51929](https://github.com/vendure-ecommerce/vendure/commit/2f51929))
+* **create** Ship Vendure CLI with new projects ([faf69a9](https://github.com/vendure-ecommerce/vendure/commit/faf69a9))
+* **elasticsearch-plugin** Provide the ctx for custom mappings (#2547) ([c5d0ea2](https://github.com/vendure-ecommerce/vendure/commit/c5d0ea2)), closes [#2547](https://github.com/vendure-ecommerce/vendure/issues/2547)
+* **email-plugin** Multiple currency support in formatMoney helper (#2531) ([ccf17fb](https://github.com/vendure-ecommerce/vendure/commit/ccf17fb)), closes [#2531](https://github.com/vendure-ecommerce/vendure/issues/2531)
+* **email-plugin** Publish EmailSendEvent after send attempted ([e4175e7](https://github.com/vendure-ecommerce/vendure/commit/e4175e7))
+* **job-queue-plugin** Implement cancellation mechanism ([d0e97ca](https://github.com/vendure-ecommerce/vendure/commit/d0e97ca)), closes [#1127](https://github.com/vendure-ecommerce/vendure/issues/1127) [#2650](https://github.com/vendure-ecommerce/vendure/issues/2650)
+* **job-queue-plugin** Improve pub/sub message handling (#2561) ([3645819](https://github.com/vendure-ecommerce/vendure/commit/3645819)), closes [#2561](https://github.com/vendure-ecommerce/vendure/issues/2561)
+* **payments-plugin** Accepted states ([124f169](https://github.com/vendure-ecommerce/vendure/commit/124f169))
+* **payments-plugin** Admin ui dropdown button ([af80f26](https://github.com/vendure-ecommerce/vendure/commit/af80f26))
+* **payments-plugin** Backend admin api implemented ([5390a4c](https://github.com/vendure-ecommerce/vendure/commit/5390a4c))
+* **payments-plugin** Cleaner usage of type ([5a720bb](https://github.com/vendure-ecommerce/vendure/commit/5a720bb))
+* **payments-plugin** Correct test naming ([07d9b4d](https://github.com/vendure-ecommerce/vendure/commit/07d9b4d))
+* **payments-plugin** Docs ([f7dcc31](https://github.com/vendure-ecommerce/vendure/commit/f7dcc31))
+* **payments-plugin** E2e test fixed ([1e65032](https://github.com/vendure-ecommerce/vendure/commit/1e65032))
+* **payments-plugin** fixed e2e tests ([d842837](https://github.com/vendure-ecommerce/vendure/commit/d842837))
+* **payments-plugin** Forgot to add files ([9e51be2](https://github.com/vendure-ecommerce/vendure/commit/9e51be2))
+* **payments-plugin** Live testing of duplicate payments ([9c1df83](https://github.com/vendure-ecommerce/vendure/commit/9c1df83))
+* **payments-plugin** Mollie locale preservation #2270 and type fixes ([9e58097](https://github.com/vendure-ecommerce/vendure/commit/9e58097)), closes [#2270](https://github.com/vendure-ecommerce/vendure/issues/2270)
+* **payments-plugin** Mollie: support extra parameters for listing methods (#2516) ([cb9846b](https://github.com/vendure-ecommerce/vendure/commit/cb9846b)), closes [#2516](https://github.com/vendure-ecommerce/vendure/issues/2516) [#2510](https://github.com/vendure-ecommerce/vendure/issues/2510)
+* **payments-plugin** more manual merge fixes ([ddea675](https://github.com/vendure-ecommerce/vendure/commit/ddea675))
+* **payments-plugin** Plugin docs ([028dcf9](https://github.com/vendure-ecommerce/vendure/commit/028dcf9))
+* **payments-plugin** Prevent duplicate Mollie payments (#2691) ([34b61cd](https://github.com/vendure-ecommerce/vendure/commit/34b61cd)), closes [#2691](https://github.com/vendure-ecommerce/vendure/issues/2691)
+* **payments-plugin** Readded describe for more readable gh diff ([d7a38ab](https://github.com/vendure-ecommerce/vendure/commit/d7a38ab))
+* **payments-plugin** Removed copyfiles dependency ([fb519ed](https://github.com/vendure-ecommerce/vendure/commit/fb519ed))
+* **payments-plugin** Removed ui for now ([3dfd0ba](https://github.com/vendure-ecommerce/vendure/commit/3dfd0ba))
+* **payments-plugin** Reusing existing order if possible ([4642f9f](https://github.com/vendure-ecommerce/vendure/commit/4642f9f))
+* **payments-plugin** Setup of preventing duplicate payments ([0cb2df8](https://github.com/vendure-ecommerce/vendure/commit/0cb2df8))
+* **payments-plugin** Test case readability ([7009425](https://github.com/vendure-ecommerce/vendure/commit/7009425))
+* **payments-plugin** Ui extension removal ([e8ae5f6](https://github.com/vendure-ecommerce/vendure/commit/e8ae5f6))
+* **payments-plugin** Unstaged files ([1ef282a](https://github.com/vendure-ecommerce/vendure/commit/1ef282a))
+* **payments-plugin** wip ([fb4cace](https://github.com/vendure-ecommerce/vendure/commit/fb4cace))
+* **sentry-plugin** Use ErrorHandlerStrategy for better error coverage ([82ddf94](https://github.com/vendure-ecommerce/vendure/commit/82ddf94))
+* **ui-devkit** Add `prefix` option to route config to allow overrides ([babe4f4](https://github.com/vendure-ecommerce/vendure/commit/babe4f4)), closes [#2705](https://github.com/vendure-ecommerce/vendure/issues/2705)
+
+
+### BREAKING CHANGE
+
+* MolliePlugin - A new mollieOrderId has been added in order to prevent duplicate payments in Mollie. This will require a DB migration to add the custom field to your DB schema.
 ## <small>2.1.9 (2024-04-05)</small>
 
 

+ 2 - 2
docs/docs/guides/developer-guide/events/index.mdx

@@ -341,7 +341,7 @@ export class MyPluginPlugin implements OnModuleInit {
     onModuleInit() {
         // highlight-start
         this.eventBus.registerBlockingEventHandler({
-            type: CustomerEvent,
+            event: CustomerEvent,
             id: 'sync-customer-details-handler',
             handler: async event => {
                 // This hypothetical service method would do nothing
@@ -403,4 +403,4 @@ this.eventBus.registerBlockingEventHandler({
     // highlight-next-line
     before: 'sync-customer-details-handler',
 });
-```
+```

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

@@ -189,7 +189,7 @@ We'll start by creating a new directory to house our plugin, add create the main
             ├── wishlist.plugin.ts
 ```
 
-```ts title="src/plugins/reviews-plugin/wishlist.plugin.ts"
+```ts title="src/plugins/wishlist-plugin/wishlist.plugin.ts"
 import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 
 @VendurePlugin({

+ 2 - 2
docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md

@@ -28,7 +28,7 @@ This example assumes you have set up your project to use code generation as desc
 import { ResultOf } from '@graphql-typed-document-node/core';
 import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
 import { FormBuilder } from '@angular/forms';
-import { TypedBaseDetailComponent, LanguageCode, SharedModule } from '@vendure/admin-ui/core';
+import { TypedBaseDetailComponent, LanguageCode, NotificationService, SharedModule } from '@vendure/admin-ui/core';
 
 // This is the TypedDocumentNode & type generated by GraphQL Code Generator
 import { graphql } from '../../gql';
@@ -329,4 +329,4 @@ Then add a card for your custom fields to the template:
         </vdr-page-block>
     </vdr-page-detail-layout>
 </form>
-```
+```

+ 5 - 1
docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md

@@ -217,7 +217,7 @@ import { GET_REVIEWS_FOR_PRODUCT } from '../product-reviews-list/product-reviews
                 <clr-icon shape="link"></clr-icon>
             </a>
         </div>
-        <select [formControl]="formControl">
+        <select [formControl]="formControl" [compareWith]="compareFn">
             <option [ngValue]="null">Select a review...</option>
             <option *ngFor="let item of reviews$ | async" [ngValue]="item">
                 <b>{{ item.summary }}</b>
@@ -247,6 +247,10 @@ export class RelationReviewInputComponent implements OnInit, FormInputComponent<
             }),
         );
     }
+
+    compareFn(item1: { id: string } | null, item2: { id: string } | null) {
+        return item1 && item2 ? item1.id === item2.id : item1 === item2;
+    }
 }
 ```
 

+ 37 - 0
docs/docs/guides/extending-the-admin-ui/getting-started/index.md

@@ -371,6 +371,43 @@ yarn add copyfiles
 
 While the Admin UI natively supports extensions written with Angular or React, it is still possible to create extensions using other front-end frameworks such as Vue or Solid. Note that creating extensions in this way is much more limited, with only the ability to define new routes, and limited access to internal services such as data fetching and notifications. See [UI extensions in other frameworks](/guides/extending-the-admin-ui/using-other-frameworks/).
 
+## IDE Support
+
+### WebStorm
+
+If you are using Angular in your UI extensions and WebStorm is not recognizing the Angular templates, you can
+add an `angular.json` file to the `/src/plugins/<my-plugin>/ui` directory:
+
+```json title="angular.json"
+{
+    "$schema": "../../../../node_modules/@angular/cli/lib/config/schema.json",
+    "version": 1,
+    "newProjectRoot": "projects",
+    "projects": {
+        "ui-extensions": {
+            "root": "",
+            "sourceRoot": "src",
+            "projectType": "application"
+        }
+    }
+}
+```
+
+This allows WebStorm's built-in Angular support to recognize the Angular templates in your UI extensions. Note that depending
+on your folder structure, you may need to adjust the path to the schema file in the `$schema` property.
+
+### VS Code
+
+If you are using Angular in your UI extensions and VS Code is not recognizing the Angular templates, you can
+add an empty `tsconfig.json` file to the `/src/plugins/<my-plugin>/ui` directory:
+
+```json title="tsconfig.json"
+{}
+```
+
+This works around the fact that your main `tsconfig.json` file excludes the `src/plugins/**/ui` directory, 
+which would otherwise prevent the Angular Language Service from working correctly.
+
 ## Legacy API < v2.1.0
 
 Prior to Vendure v2.1.0, the API for extending the Admin UI was more verbose and less flexible (React components were not supported at all, for instance). This API is still supported, but from v2.1 is marked as deprecated and will be removed in a future major version. 

+ 13 - 13
docs/docs/reference/admin-ui-api/alerts/alert-config.md

@@ -22,9 +22,9 @@ interface AlertConfig<T = any> {
     recheck?: (context: AlertContext) => Observable<any>;
     isAlert: (data: T, context: AlertContext) => boolean;
     action: (data: T, context: AlertContext) => void;
-    label: (
-        data: T,
-        context: AlertContext,
+    label: (
+        data: T,
+        context: AlertContext,
     ) => { text: string; translationVars?: { [key: string]: string | number } };
     requiredPermissions?: Permission[];
 }
@@ -41,18 +41,18 @@ A unique identifier for the alert.
 
 <MemberInfo kind="property" type={`(context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; T | Promise&#60;T&#62; | Observable&#60;T&#62;`}   />
 
-A function which is gets the data used to determine whether the alert should be shown.
-Typically, this function will query the server or some other remote data source.
-
-This function will be called once when the Admin UI app bootstraps, and can be also
+A function which is gets the data used to determine whether the alert should be shown.
+Typically, this function will query the server or some other remote data source.
+
+This function will be called once when the Admin UI app bootstraps, and can be also
 set to run at regular intervals by setting the `recheckIntervalMs` property.
 ### recheck
 
 <MemberInfo kind="property" type={`(context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; Observable&#60;any&#62;`} default="undefined"   />
 
-A function which returns an Observable which is used to determine when to re-run the `check`
-function. Whenever the observable emits, the `check` function will be called again.
-
+A function which returns an Observable which is used to determine when to re-run the `check`
+function. Whenever the observable emits, the `check` function will be called again.
+
 A basic time-interval-based recheck can be achieved by using the `interval` function from RxJS.
 
 *Example*
@@ -69,7 +69,7 @@ If this is not set, the `check` function will only be called once when the Admin
 
 <MemberInfo kind="property" type={`(data: T, context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>) =&#62; boolean`}   />
 
-A function which determines whether the alert should be shown based on the data returned by the `check`
+A function which determines whether the alert should be shown based on the data returned by the `check`
 function.
 ### action
 
@@ -78,14 +78,14 @@ function.
 A function which is called when the alert is clicked in the Admin UI.
 ### label
 
-<MemberInfo kind="property" type={`(         data: T,         context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>,     ) =&#62; { text: string; translationVars?: { [key: string]: string | number } }`}   />
+<MemberInfo kind="property" type={`(
         data: T,
         context: <a href='/reference/admin-ui-api/alerts/alert-context#alertcontext'>AlertContext</a>,
     ) =&#62; { text: string; translationVars?: { [key: string]: string | number } }`}   />
 
 A function which returns the text used in the UI to describe the alert.
 ### requiredPermissions
 
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/common/permission#permission'>Permission</a>[]`}   />
 
-A list of permissions which the current Administrator must have in order. If the current
+A list of permissions which the current Administrator must have in order. If the current
 Administrator does not have these permissions, none of the other alert functions will be called.
 
 

+ 1 - 1
docs/docs/reference/core-plugins/email-plugin/email-event-handler-with-async-data.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## EmailEventHandlerWithAsyncData
 
-<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="438" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="446" packageName="@vendure/email-plugin" />
 
 Identical to the <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a> but with a `data` property added to the `event` based on the result
 of the `.loadData()` function.

+ 9 - 4
docs/docs/reference/core-plugins/email-plugin/email-event-handler.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## EmailEventHandler
 
-<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="131" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="135" packageName="@vendure/email-plugin" />
 
 The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 
@@ -25,6 +25,7 @@ const confirmationHandler = new EmailEventListener('order-confirmation')
   .on(OrderStateTransitionEvent)
   .filter(event => event.toState === 'PaymentSettled')
   .setRecipient(event => event.order.customer.emailAddress)
+  .setFrom('{{ fromAddress }}')
   .setSubject(`Order confirmation for #{{ order.code }}`)
   .setTemplateVars(event => ({ order: event.order }));
 ```
@@ -74,13 +75,16 @@ const quoteRequestedHandler = new EmailEventListener('quote-requested')
   .on(QuoteRequestedEvent)
   .setRecipient(event => event.customer.emailAddress)
   .setSubject(`Here's the quote you requested`)
+  .setFrom('{{ fromAddress }}')
   .setTemplateVars(event => ({ details: event.details }));
 ```
 
 ### 2. Create the email template
 
-Next you need to make sure there is a template defined at `<app root>/static/email/templates/quote-requested/body.hbs`. The template
-would look something like this:
+Next you need to make sure there is a template defined at `<app root>/static/email/templates/quote-requested/body.hbs`. The path
+segment `quote-requested` must match the string passed to the `EmailEventListener` constructor.
+
+The template would look something like this:
 
 ```handlebars
 {{> header title="Here's the quote you requested" }}
@@ -246,7 +250,8 @@ new EmailEventListener('order-confirmation')
   .setTemplateVars(event => ({
     order: event.order,
     payments: event.data,
-  }));
+  }))
+  // ...
 ```
 ### setMockEvent
 

+ 1 - 1
docs/docs/reference/core-plugins/email-plugin/index.md

@@ -117,7 +117,7 @@ etc. These defaults can be extended by adding custom templates for languages oth
 which respond to any of the available [VendureEvents](/reference/typescript-api/events/).
 
 A good way to learn how to create your own email handler is to take a look at the
-[source code of the default handler](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/default-email-handlers.ts).
+[source code of the default handler](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/handler/default-email-handlers.ts).
 New handler are defined in exactly the same way.
 
 It is also possible to modify the default handler:

+ 2 - 2
docs/docs/reference/typescript-api/data-access/entity-hydrator.md

@@ -71,7 +71,7 @@ await this.entityHydrator
 
 ```ts title="Signature"
 class EntityHydrator {
-    constructor(connection: TransactionalConnection, productPriceApplicator: ProductPriceApplicator, translator: TranslatorService, listQueryBuilder: ListQueryBuilder)
+    constructor(connection: TransactionalConnection, productPriceApplicator: ProductPriceApplicator, translator: TranslatorService)
     hydrate(ctx: RequestContext, target: Entity, options: HydrateOptions<Entity>) => Promise<Entity>;
 }
 ```
@@ -80,7 +80,7 @@ class EntityHydrator {
 
 ### constructor
 
-<MemberInfo kind="method" type={`(connection: <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a>, productPriceApplicator: <a href='/reference/typescript-api/service-helpers/product-price-applicator#productpriceapplicator'>ProductPriceApplicator</a>, translator: <a href='/reference/typescript-api/service-helpers/translator-service#translatorservice'>TranslatorService</a>, listQueryBuilder: <a href='/reference/typescript-api/data-access/list-query-builder#listquerybuilder'>ListQueryBuilder</a>) => EntityHydrator`}   />
+<MemberInfo kind="method" type={`(connection: <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a>, productPriceApplicator: <a href='/reference/typescript-api/service-helpers/product-price-applicator#productpriceapplicator'>ProductPriceApplicator</a>, translator: <a href='/reference/typescript-api/service-helpers/translator-service#translatorservice'>TranslatorService</a>) => EntityHydrator`}   />
 
 
 ### hydrate

+ 2 - 8
docs/docs/reference/typescript-api/data-access/list-query-builder.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ListQueryBuilder
 
-<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="201" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="200" packageName="@vendure/core" />
 
 This helper class is used when fetching entities the database from queries which return a <a href='/reference/typescript-api/common/paginated-list#paginatedlist'>PaginatedList</a> type.
 These queries all follow the same format:
@@ -84,7 +84,6 @@ class ListQueryBuilder implements OnApplicationBootstrap {
     constructor(connection: TransactionalConnection, configService: ConfigService)
     filterObjectHasProperty(filterObject: FP | NullOptionals<FP> | null | undefined, property: keyof FP) => boolean;
     build(entity: Type<T>, options: ListQueryOptions<T> = {}, extendedOptions: ExtendedListQueryOptions<T> = {}) => SelectQueryBuilder<T>;
-    joinTreeRelationsDynamically(qb: SelectQueryBuilder<T>, entity: EntityTarget<T>, requestedRelations: string[] = []) => Set<string>;
 }
 ```
 * Implements: <code>OnApplicationBootstrap</code>
@@ -115,11 +114,6 @@ to join that relation.
 <MemberInfo kind="method" type={`(entity: Type&#60;T&#62;, options: ListQueryOptions&#60;T&#62; = {}, extendedOptions: <a href='/reference/typescript-api/data-access/list-query-builder#extendedlistqueryoptions'>ExtendedListQueryOptions</a>&#60;T&#62; = {}) => SelectQueryBuilder&#60;T&#62;`}   />
 
 
-### joinTreeRelationsDynamically
-
-<MemberInfo kind="method" type={`(qb: SelectQueryBuilder&#60;T&#62;, entity: EntityTarget&#60;T&#62;, requestedRelations: string[] = []) => Set&#60;string&#62;`}   />
-
-
 
 
 </div>
@@ -127,7 +121,7 @@ to join that relation.
 
 ## ExtendedListQueryOptions
 
-<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="42" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/helpers/list-query-builder/list-query-builder.ts" sourceLine="41" packageName="@vendure/core" />
 
 Options which can be passed to the ListQueryBuilder's `build()` method.
 

+ 5 - 5
docs/docs/reference/typescript-api/data-access/transactional-connection.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## TransactionalConnection
 
-<GenerationInfo sourceFile="packages/core/src/connection/transactional-connection.ts" sourceLine="38" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/connection/transactional-connection.ts" sourceLine="40" packageName="@vendure/core" />
 
 The TransactionalConnection is a wrapper around the TypeORM `Connection` object which works in conjunction
 with the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator to implement per-request transactions. All services which access the
@@ -23,8 +23,8 @@ API by the use of the `Transaction` decorator.
 
 ```ts title="Signature"
 class TransactionalConnection {
-    constructor(connection: Connection, transactionWrapper: TransactionWrapper)
-    rawConnection: Connection
+    constructor(dataSource: DataSource, transactionWrapper: TransactionWrapper)
+    rawConnection: DataSource
     getRepository(target: ObjectType<Entity> | EntitySchema<Entity> | string) => Repository<Entity>;
     getRepository(ctx: RequestContext | undefined, target: ObjectType<Entity> | EntitySchema<Entity> | string) => Repository<Entity>;
     getRepository(ctxOrTarget: RequestContext | ObjectType<Entity> | EntitySchema<Entity> | string | undefined, maybeTarget?: ObjectType<Entity> | EntitySchema<Entity> | string) => Repository<Entity>;
@@ -44,12 +44,12 @@ class TransactionalConnection {
 
 ### constructor
 
-<MemberInfo kind="method" type={`(connection: Connection, transactionWrapper: TransactionWrapper) => TransactionalConnection`}   />
+<MemberInfo kind="method" type={`(dataSource: DataSource, transactionWrapper: TransactionWrapper) => TransactionalConnection`}   />
 
 
 ### rawConnection
 
-<MemberInfo kind="property" type={`Connection`}   />
+<MemberInfo kind="property" type={`DataSource`}   />
 
 The plain TypeORM Connection object. Should be used carefully as any operations
 performed with this connection will not be performed within any outer

+ 1 - 1
docs/docs/reference/typescript-api/entities/authenticated-session.md

@@ -19,7 +19,7 @@ An AuthenticatedSession is created upon successful authentication.
 class AuthenticatedSession extends Session {
     constructor(input: DeepPartial<AuthenticatedSession>)
     @Index()
-    @ManyToOne(type => User)
+    @ManyToOne(type => User, user => user.sessions)
     user: User;
     @Column()
     authenticationStrategy: string;

+ 46 - 4
docs/docs/reference/typescript-api/entities/channel.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Channel
 
-<GenerationInfo sourceFile="packages/core/src/entity/channel/channel.entity.ts" sourceLine="36" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/channel/channel.entity.ts" sourceLine="37" packageName="@vendure/core" />
 
 A Channel represents a distinct sales channel and configures defaults for that
 channel.
@@ -39,7 +39,7 @@ class Channel extends VendureEntity {
     @Column({ default: '', nullable: true })
     description: string;
     @Index()
-    @ManyToOne(type => Seller)
+    @ManyToOne(type => Seller, seller => seller.channels)
     seller?: Seller;
     @EntityId({ nullable: true })
     sellerId?: ID;
@@ -47,10 +47,10 @@ class Channel extends VendureEntity {
     @Column({ type: 'simple-array', nullable: true })
     availableLanguageCodes: LanguageCode[];
     @Index()
-    @ManyToOne(type => Zone)
+    @ManyToOne(type => Zone, zone => zone.defaultTaxZoneChannels)
     defaultTaxZone: Zone;
     @Index()
-    @ManyToOne(type => Zone)
+    @ManyToOne(type => Zone, zone => zone.defaultShippingZoneChannels)
     defaultShippingZone: Zone;
     @Column('varchar')
     defaultCurrencyCode: CurrencyCode;
@@ -73,6 +73,18 @@ class Channel extends VendureEntity {
     facets: Facet[];
     @ManyToMany(type => Collection, collection => collection.channels, { onDelete: 'CASCADE' })
     collections: Collection[];
+    @ManyToMany(type => Promotion, promotion => promotion.channels, { onDelete: 'CASCADE' })
+    promotions: Promotion[];
+    @ManyToMany(type => PaymentMethod, paymentMethod => paymentMethod.channels, { onDelete: 'CASCADE' })
+    paymentMethods: PaymentMethod[];
+    @ManyToMany(type => ShippingMethod, shippingMethod => shippingMethod.channels, { onDelete: 'CASCADE' })
+    shippingMethods: ShippingMethod[];
+    @ManyToMany(type => Customer, customer => customer.channels, { onDelete: 'CASCADE' })
+    customers: Customer[];
+    @ManyToMany(type => Role, role => role.channels, { onDelete: 'CASCADE' })
+    roles: Role[];
+    @ManyToMany(type => StockLocation, stockLocation => stockLocation.channels, { onDelete: 'CASCADE' })
+    stockLocations: StockLocation[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -190,6 +202,36 @@ out of stock.
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/collection#collection'>Collection</a>[]`}   />
 
 
+### promotions
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a>[]`}   />
+
+
+### paymentMethods
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a>[]`}   />
+
+
+### shippingMethods
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/shipping-method#shippingmethod'>ShippingMethod</a>[]`}   />
+
+
+### customers
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/customer#customer'>Customer</a>[]`}   />
+
+
+### roles
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/role#role'>Role</a>[]`}   />
+
+
+### stockLocations
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-location#stocklocation'>StockLocation</a>[]`}   />
+
+
 
 
 </div>

+ 8 - 1
docs/docs/reference/typescript-api/entities/customer-group.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## CustomerGroup
 
-<GenerationInfo sourceFile="packages/core/src/entity/customer-group/customer-group.entity.ts" sourceLine="16" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/customer-group/customer-group.entity.ts" sourceLine="17" packageName="@vendure/core" />
 
 A grouping of <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>s which enables features such as group-based promotions
 or tax rules.
@@ -24,6 +24,8 @@ class CustomerGroup extends VendureEntity implements HasCustomFields {
     customers: Customer[];
     @Column(type => CustomCustomerGroupFields)
     customFields: CustomCustomerGroupFields;
+    @OneToMany(type => TaxRate, taxRate => taxRate.zone)
+    taxRates: TaxRate[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -55,6 +57,11 @@ class CustomerGroup extends VendureEntity implements HasCustomFields {
 <MemberInfo kind="property" type={`CustomCustomerGroupFields`}   />
 
 
+### taxRates
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a>[]`}   />
+
+
 
 
 </div>

+ 1 - 1
docs/docs/reference/typescript-api/entities/customer.md

@@ -42,7 +42,7 @@ class Customer extends VendureEntity implements ChannelAware, HasCustomFields, S
     user?: User;
     @Column(type => CustomCustomerFields)
     customFields: CustomCustomerFields;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.customers)
     @JoinTable()
     channels: Channel[];
 }

+ 1 - 1
docs/docs/reference/typescript-api/entities/facet.md

@@ -34,7 +34,7 @@ class Facet extends VendureEntity implements Translatable, HasCustomFields, Chan
     values: FacetValue[];
     @Column(type => CustomFacetFields)
     customFields: CustomFacetFields;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.facets)
     @JoinTable()
     channels: Channel[];
 }

+ 1 - 1
docs/docs/reference/typescript-api/entities/order-line-reference.md

@@ -62,7 +62,7 @@ class OrderLineReference extends VendureEntity {
     @Column()
     quantity: number;
     @Index()
-    @ManyToOne(type => OrderLine, { onDelete: 'CASCADE' })
+    @ManyToOne(type => OrderLine, line => line.linesReferences, { onDelete: 'CASCADE' })
     orderLine: OrderLine;
     @EntityId()
     orderLineId: ID;

+ 32 - 8
docs/docs/reference/typescript-api/entities/order-line.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## OrderLine
 
-<GenerationInfo sourceFile="packages/core/src/entity/order-line/order-line.entity.ts" sourceLine="29" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/order-line/order-line.entity.ts" sourceLine="32" packageName="@vendure/core" />
 
 A single line on an <a href='/reference/typescript-api/entities/order#order'>Order</a> which contains information about the <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a> and
 quantity ordered, as well as the price and tax information.
@@ -25,12 +25,15 @@ class OrderLine extends VendureEntity implements HasCustomFields {
     @EntityId({ nullable: true })
     sellerChannelId?: ID;
     @Index()
-    @ManyToOne(type => ShippingLine, { nullable: true, onDelete: 'SET NULL' })
+    @ManyToOne(type => ShippingLine, shippingLine => shippingLine.orderLines, {
+        nullable: true,
+        onDelete: 'SET NULL',
+    })
     shippingLine?: ShippingLine;
     @EntityId({ nullable: true })
     shippingLineId?: ID;
     @Index()
-    @ManyToOne(type => ProductVariant)
+    @ManyToOne(type => ProductVariant, productVariant => productVariant.lines, { onDelete: 'CASCADE' })
     productVariant: ProductVariant;
     @EntityId()
     productVariantId: ID;
@@ -38,11 +41,15 @@ class OrderLine extends VendureEntity implements HasCustomFields {
     @ManyToOne(type => TaxCategory)
     taxCategory: TaxCategory;
     @Index()
-    @ManyToOne(type => Asset)
+    @ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
     @Index()
     @ManyToOne(type => Order, order => order.lines, { onDelete: 'CASCADE' })
     order: Order;
+    @OneToMany(type => OrderLineReference, lineRef => lineRef.orderLine)
+    linesReferences: OrderLineReference[];
+    @OneToMany(type => Sale, sale => sale.orderLine)
+    sales: Sale[];
     @Column()
     quantity: number;
     @Column({ default: 0 })
@@ -57,8 +64,10 @@ class OrderLine extends VendureEntity implements HasCustomFields {
     adjustments: Adjustment[];
     @Column('simple-json')
     taxLines: TaxLine[];
-    @OneToOne(type => Cancellation, cancellation => cancellation.orderLine)
-    cancellation: Cancellation;
+    @OneToMany(type => Cancellation, cancellation => cancellation.orderLine)
+    cancellations: Cancellation[];
+    @OneToMany(type => Allocation, allocation => allocation.orderLine)
+    allocations: Allocation[];
     @Column(type => CustomOrderLineFields)
     customFields: CustomOrderLineFields;
     unitPrice: number
@@ -145,6 +154,16 @@ The <a href='/reference/typescript-api/entities/product-variant#productvariant'>
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/order#order'>Order</a>`}   />
 
 
+### linesReferences
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/order-line-reference#orderlinereference'>OrderLineReference</a>[]`}   />
+
+
+### sales
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-movement#sale'>Sale</a>[]`}   />
+
+
 ### quantity
 
 <MemberInfo kind="property" type={`number`}   />
@@ -183,9 +202,14 @@ Whether the listPrice includes tax, which depends on the settings of the current
 <MemberInfo kind="property" type={`TaxLine[]`}   />
 
 
-### cancellation
+### cancellations
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-movement#cancellation'>Cancellation</a>[]`}   />
+
+
+### allocations
 
-<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-movement#cancellation'>Cancellation</a>`}   />
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-movement#allocation'>Allocation</a>[]`}   />
 
 
 ### customFields

+ 1 - 1
docs/docs/reference/typescript-api/entities/payment-method.md

@@ -27,7 +27,7 @@ class PaymentMethod extends VendureEntity implements Translatable, ChannelAware,
     @Column() enabled: boolean;
     @Column('simple-json', { nullable: true }) checker: ConfigurableOperation | null;
     @Column('simple-json') handler: ConfigurableOperation;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.paymentMethods)
     @JoinTable()
     channels: Channel[];
     @Column(type => CustomPaymentMethodFields)

+ 8 - 1
docs/docs/reference/typescript-api/entities/product-variant.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ProductVariant
 
-<GenerationInfo sourceFile="packages/core/src/entity/product-variant/product-variant.entity.ts" sourceLine="37" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/product-variant/product-variant.entity.ts" sourceLine="38" packageName="@vendure/core" />
 
 A ProductVariant represents a single stock keeping unit (SKU) in the store's inventory.
 Whereas a <a href='/reference/typescript-api/entities/product#product'>Product</a> is a "container" of variants, the variant itself holds the
@@ -76,6 +76,8 @@ class ProductVariant extends VendureEntity implements Translatable, HasCustomFie
     @ManyToMany(type => Channel, channel => channel.productVariants)
     @JoinTable()
     channels: Channel[];
+    @OneToMany(type => OrderLine, orderLine => orderLine.productVariant)
+    lines: OrderLine[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -229,6 +231,11 @@ value set on this ProductVariant will be ignored.
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/channel#channel'>Channel</a>[]`}   />
 
 
+### lines
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a>[]`}   />
+
+
 
 
 </div>

+ 1 - 1
docs/docs/reference/typescript-api/entities/product.md

@@ -40,7 +40,7 @@ class Product extends VendureEntity implements Translatable, HasCustomFields, Ch
     @ManyToMany(type => FacetValue, facetValue => facetValue.products)
     @JoinTable()
     facetValues: FacetValue[];
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.products)
     @JoinTable()
     channels: Channel[];
     @Column(type => CustomProductFields)

+ 2 - 2
docs/docs/reference/typescript-api/entities/promotion.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Promotion
 
-<GenerationInfo sourceFile="packages/core/src/entity/promotion/promotion.entity.ts" sourceLine="57" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/promotion/promotion.entity.ts" sourceLine="56" packageName="@vendure/core" />
 
 A Promotion is used to define a set of conditions under which promotions actions (typically discounts)
 will be applied to an Order.
@@ -43,7 +43,7 @@ class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable,
     @OneToMany(type => PromotionTranslation, translation => translation.base, { eager: true })
     translations: Array<Translation<Promotion>>;
     @Column() enabled: boolean;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.promotions)
     @JoinTable()
     channels: Channel[];
     @ManyToMany(type => Order, order => order.promotions)

+ 1 - 1
docs/docs/reference/typescript-api/entities/role.md

@@ -22,7 +22,7 @@ class Role extends VendureEntity implements ChannelAware {
     @Column() code: string;
     @Column() description: string;
     @Column('simple-array') permissions: Permission[];
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.roles)
     @JoinTable()
     channels: Channel[];
 }

+ 8 - 1
docs/docs/reference/typescript-api/entities/seller.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Seller
 
-<GenerationInfo sourceFile="packages/core/src/entity/seller/seller.entity.ts" sourceLine="16" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/seller/seller.entity.ts" sourceLine="17" packageName="@vendure/core" />
 
 A Seller represents the person or organization who is selling the goods on a given <a href='/reference/typescript-api/entities/channel#channel'>Channel</a>.
 By default, a single-channel Vendure installation will have a single default Seller.
@@ -24,6 +24,8 @@ class Seller extends VendureEntity implements SoftDeletable, HasCustomFields {
     @Column() name: string;
     @Column(type => CustomSellerFields)
     customFields: CustomSellerFields;
+    @OneToMany(type => Channel, channel => channel.seller)
+    channels: Channel[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -55,6 +57,11 @@ class Seller extends VendureEntity implements SoftDeletable, HasCustomFields {
 <MemberInfo kind="property" type={`CustomSellerFields`}   />
 
 
+### channels
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/channel#channel'>Channel</a>[]`}   />
+
+
 
 
 </div>

+ 8 - 1
docs/docs/reference/typescript-api/entities/shipping-line.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ShippingLine
 
-<GenerationInfo sourceFile="packages/core/src/entity/shipping-line/shipping-line.entity.ts" sourceLine="23" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/shipping-line/shipping-line.entity.ts" sourceLine="24" packageName="@vendure/core" />
 
 A ShippingLine is created when a <a href='/reference/typescript-api/entities/shipping-method#shippingmethod'>ShippingMethod</a> is applied to an <a href='/reference/typescript-api/entities/order#order'>Order</a>.
 It contains information about the price of the shipping method, any discounts that were
@@ -36,6 +36,8 @@ class ShippingLine extends VendureEntity {
     adjustments: Adjustment[];
     @Column('simple-json')
     taxLines: TaxLine[];
+    @OneToMany(type => OrderLine, orderLine => orderLine.shippingLine)
+    orderLines: OrderLine[];
     price: number
     priceWithTax: number
     discountedPrice: number
@@ -92,6 +94,11 @@ class ShippingLine extends VendureEntity {
 <MemberInfo kind="property" type={`TaxLine[]`}   />
 
 
+### orderLines
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a>[]`}   />
+
+
 ### price
 
 <MemberInfo kind="property" type={`number`}   />

+ 1 - 1
docs/docs/reference/typescript-api/entities/shipping-method.md

@@ -31,7 +31,7 @@ class ShippingMethod extends VendureEntity implements ChannelAware, SoftDeletabl
     @Column('simple-json') calculator: ConfigurableOperation;
     @Column()
     fulfillmentHandlerCode: string;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.shippingMethods)
     @JoinTable()
     channels: Channel[];
     @OneToMany(type => ShippingMethodTranslation, translation => translation.base, { eager: true })

+ 9 - 2
docs/docs/reference/typescript-api/entities/stock-location.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## StockLocation
 
-<GenerationInfo sourceFile="packages/core/src/entity/stock-location/stock-location.entity.ts" sourceLine="21" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/stock-location/stock-location.entity.ts" sourceLine="22" packageName="@vendure/core" />
 
 A StockLocation represents a physical location where stock is held. For example, a warehouse or a shop.
 
@@ -29,9 +29,11 @@ class StockLocation extends VendureEntity implements HasCustomFields, ChannelAwa
     description: string;
     @Column(type => CustomStockLocationFields)
     customFields: CustomStockLocationFields;
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.stockLocations)
     @JoinTable()
     channels: Channel[];
+    @OneToMany(type => StockMovement, movement => movement.stockLocation)
+    stockMovements: StockMovement[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -68,6 +70,11 @@ class StockLocation extends VendureEntity implements HasCustomFields, ChannelAwa
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/channel#channel'>Channel</a>[]`}   />
 
 
+### stockMovements
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/stock-movement#stockmovement'>StockMovement</a>[]`}   />
+
+
 
 
 </div>

+ 4 - 4
docs/docs/reference/typescript-api/entities/stock-movement.md

@@ -24,7 +24,7 @@ class StockMovement extends VendureEntity {
     @ManyToOne(type => ProductVariant, variant => variant.stockMovements)
     productVariant: ProductVariant;
     @Index()
-    @ManyToOne(type => StockLocation, { onDelete: 'CASCADE' })
+    @ManyToOne(type => StockLocation, stockLocation => stockLocation.stockMovements, { onDelete: 'CASCADE' })
     stockLocation: StockLocation;
     @EntityId()
     stockLocationId: ID;
@@ -80,7 +80,7 @@ class Allocation extends StockMovement {
     readonly type = StockMovementType.ALLOCATION;
     constructor(input: DeepPartial<Allocation>)
     @Index()
-    @ManyToOne(type => OrderLine)
+    @ManyToOne(type => OrderLine, orderLine => orderLine.allocations)
     orderLine: OrderLine;
 }
 ```
@@ -120,7 +120,7 @@ A Cancellation is created when OrderItems from a fulfilled Order are cancelled.
 class Cancellation extends StockMovement {
     readonly type = StockMovementType.CANCELLATION;
     constructor(input: DeepPartial<Cancellation>)
-    @ManyToOne(type => OrderLine)
+    @ManyToOne(type => OrderLine, orderLine => orderLine.cancellations)
     orderLine: OrderLine;
 }
 ```
@@ -201,7 +201,7 @@ A Sale is created when OrderItems are fulfilled.
 class Sale extends StockMovement {
     readonly type = StockMovementType.SALE;
     constructor(input: DeepPartial<Sale>)
-    @ManyToOne(type => OrderLine)
+    @ManyToOne(type => OrderLine, line => line.sales)
     orderLine: OrderLine;
 }
 ```

+ 8 - 1
docs/docs/reference/typescript-api/entities/tax-category.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## TaxCategory
 
-<GenerationInfo sourceFile="packages/core/src/entity/tax-category/tax-category.entity.ts" sourceLine="15" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/tax-category/tax-category.entity.ts" sourceLine="16" packageName="@vendure/core" />
 
 A TaxCategory defines what type of taxes to apply to a <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>.
 
@@ -24,6 +24,8 @@ class TaxCategory extends VendureEntity implements HasCustomFields {
     customFields: CustomTaxCategoryFields;
     @OneToMany(type => ProductVariant, productVariant => productVariant.taxCategory)
     productVariants: ProductVariant[];
+    @OneToMany(type => TaxRate, taxRate => taxRate.category)
+    taxRates: TaxRate[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -60,6 +62,11 @@ class TaxCategory extends VendureEntity implements HasCustomFields {
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>[]`}   />
 
 
+### taxRates
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a>[]`}   />
+
+
 
 
 </div>

+ 3 - 3
docs/docs/reference/typescript-api/entities/tax-rate.md

@@ -26,13 +26,13 @@ class TaxRate extends VendureEntity implements HasCustomFields {
     @Column() enabled: boolean;
     @Column({ type: 'decimal', precision: 5, scale: 2, transformer: new DecimalTransformer() }) value: number;
     @Index()
-    @ManyToOne(type => TaxCategory)
+    @ManyToOne(type => TaxCategory, taxCategory => taxCategory.taxRates)
     category: TaxCategory;
     @Index()
-    @ManyToOne(type => Zone)
+    @ManyToOne(type => Zone, zone => zone.taxRates)
     zone: Zone;
     @Index()
-    @ManyToOne(type => CustomerGroup, { nullable: true })
+    @ManyToOne(type => CustomerGroup, customerGroup => customerGroup.taxRates, { nullable: true })
     customerGroup?: CustomerGroup;
     @Column(type => CustomTaxRateFields)
     customFields: CustomTaxRateFields;

+ 8 - 1
docs/docs/reference/typescript-api/entities/user.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## User
 
-<GenerationInfo sourceFile="packages/core/src/entity/user/user.entity.ts" sourceLine="20" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/user/user.entity.ts" sourceLine="21" packageName="@vendure/core" />
 
 A User represents any authenticated user of the Vendure API. This includes both
 <a href='/reference/typescript-api/entities/administrator#administrator'>Administrator</a>s as well as registered <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>s.
@@ -34,6 +34,8 @@ class User extends VendureEntity implements HasCustomFields, SoftDeletable {
     lastLogin: Date | null;
     @Column(type => CustomUserFields)
     customFields: CustomUserFields;
+    @OneToMany(type => AuthenticatedSession, session => session.user)
+    sessions: AuthenticatedSession[];
     getNativeAuthenticationMethod() => NativeAuthenticationMethod;
     getNativeAuthenticationMethod(strict?: boolean) => NativeAuthenticationMethod | undefined;
     getNativeAuthenticationMethod(strict?: boolean) => NativeAuthenticationMethod | undefined;
@@ -88,6 +90,11 @@ class User extends VendureEntity implements HasCustomFields, SoftDeletable {
 <MemberInfo kind="property" type={`CustomUserFields`}   />
 
 
+### sessions
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/authenticated-session#authenticatedsession'>AuthenticatedSession</a>[]`}   />
+
+
 ### getNativeAuthenticationMethod
 
 <MemberInfo kind="method" type={`() => <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a>`}   />

+ 22 - 1
docs/docs/reference/typescript-api/entities/zone.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Zone
 
-<GenerationInfo sourceFile="packages/core/src/entity/zone/zone.entity.ts" sourceLine="17" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/entity/zone/zone.entity.ts" sourceLine="19" packageName="@vendure/core" />
 
 A Zone is a grouping of one or more <a href='/reference/typescript-api/entities/country#country'>Country</a> entities. It is used for
 calculating applicable shipping and taxes.
@@ -25,6 +25,12 @@ class Zone extends VendureEntity implements HasCustomFields {
     members: Region[];
     @Column(type => CustomZoneFields)
     customFields: CustomZoneFields;
+    @OneToMany(type => Channel, country => country.defaultShippingZone)
+    defaultShippingZoneChannels: Channel[];
+    @OneToMany(type => Channel, country => country.defaultTaxZone)
+    defaultTaxZoneChannels: Channel[];
+    @OneToMany(type => TaxRate, taxRate => taxRate.zone)
+    taxRates: TaxRate[];
 }
 ```
 * Extends: <code><a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a></code>
@@ -56,6 +62,21 @@ class Zone extends VendureEntity implements HasCustomFields {
 <MemberInfo kind="property" type={`CustomZoneFields`}   />
 
 
+### defaultShippingZoneChannels
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/channel#channel'>Channel</a>[]`}   />
+
+
+### defaultTaxZoneChannels
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/channel#channel'>Channel</a>[]`}   />
+
+
+### taxRates
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a>[]`}   />
+
+
 
 
 </div>

+ 2 - 2
docs/docs/reference/typescript-api/job-queue/index.md

@@ -26,7 +26,7 @@ class JobQueue<Data extends JobData<Data> = object> {
     name: string
     started: boolean
     constructor(options: CreateQueueOptions<Data>, jobQueueStrategy: JobQueueStrategy, jobBufferService: JobBufferService)
-    add(data: Data, options?: JobOptions) => Promise<SubscribableJob<Data>>;
+    add(data: Data, options?: JobOptions<Data>) => Promise<SubscribableJob<Data>>;
 }
 ```
 
@@ -49,7 +49,7 @@ class JobQueue<Data extends JobData<Data> = object> {
 
 ### add
 
-<MemberInfo kind="method" type={`(data: Data, options?: Pick&#60;<a href='/reference/typescript-api/job-queue/types#jobconfig'>JobConfig</a>&#60;Data&#62;, 'retries'&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/subscribable-job#subscribablejob'>SubscribableJob</a>&#60;Data&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(data: Data, options?: JobOptions&#60;Data&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/subscribable-job#subscribablejob'>SubscribableJob</a>&#60;Data&#62;&#62;`}   />
 
 Adds a new <a href='/reference/typescript-api/job-queue/job#job'>Job</a> to the queue. The resolved <a href='/reference/typescript-api/job-queue/subscribable-job#subscribablejob'>SubscribableJob</a> allows the
 calling code to subscribe to updates to the Job:

+ 3 - 3
docs/docs/reference/typescript-api/job-queue/job-queue-strategy.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## JobQueueStrategy
 
-<GenerationInfo sourceFile="packages/core/src/config/job-queue/job-queue-strategy.ts" sourceLine="23" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/job-queue/job-queue-strategy.ts" sourceLine="24" packageName="@vendure/core" />
 
 Defines how the jobs in the <a href='/reference/typescript-api/job-queue/job-queue-service#jobqueueservice'>JobQueueService</a> are persisted and
 accessed. Custom strategies can be defined to make use of external
@@ -26,7 +26,7 @@ your VendureConfig.
 
 ```ts title="Signature"
 interface JobQueueStrategy extends InjectableStrategy {
-    add<Data extends JobData<Data> = object>(job: Job<Data>): Promise<Job<Data>>;
+    add<Data extends JobData<Data> = object>(job: Job<Data>, jobOptions?: JobQueueStrategyJobOptions<Data>): Promise<Job<Data>>;
     start<Data extends JobData<Data> = object>(
         queueName: string,
         process: (job: Job<Data>) => Promise<any>,
@@ -45,7 +45,7 @@ interface JobQueueStrategy extends InjectableStrategy {
 
 ### add
 
-<MemberInfo kind="method" type={`(job: <a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(job: <a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;, jobOptions?: JobQueueStrategyJobOptions&#60;Data&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;&#62;`}   />
 
 Add a new job to the queue.
 ### start

+ 2 - 2
docs/docs/reference/typescript-api/job-queue/sql-job-queue-strategy.md

@@ -20,7 +20,7 @@ This strategy is used by the <a href='/reference/typescript-api/job-queue/defaul
 class SqlJobQueueStrategy extends PollingJobQueueStrategy implements InspectableJobQueueStrategy {
     init(injector: Injector) => ;
     destroy() => ;
-    add(job: Job<Data>) => Promise<Job<Data>>;
+    add(job: Job<Data>, jobOptions?: JobQueueStrategyJobOptions<Data>) => Promise<Job<Data>>;
     next(queueName: string) => Promise<Job | undefined>;
     update(job: Job<any>) => Promise<void>;
     findMany(options?: JobListOptions) => Promise<PaginatedList<Job>>;
@@ -50,7 +50,7 @@ class SqlJobQueueStrategy extends PollingJobQueueStrategy implements Inspectable
 
 ### add
 
-<MemberInfo kind="method" type={`(job: <a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(job: <a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;, jobOptions?: JobQueueStrategyJobOptions&#60;Data&#62;) => Promise&#60;<a href='/reference/typescript-api/job-queue/job#job'>Job</a>&#60;Data&#62;&#62;`}   />
 
 
 ### next

+ 3 - 3
docs/docs/reference/typescript-api/job-queue/types.md

@@ -73,7 +73,7 @@ type JobUpdateOptions = {
 
 ## CreateQueueOptions
 
-<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="13" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="15" packageName="@vendure/core" />
 
 Used to configure a new <a href='/reference/typescript-api/job-queue/#jobqueue'>JobQueue</a> instance.
 
@@ -104,7 +104,7 @@ should resolve when the job is complete, or be rejected in case of an error.
 
 ## JobData
 
-<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="35" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="37" packageName="@vendure/core" />
 
 A JSON-serializable data type which provides a <a href='/reference/typescript-api/job-queue/job#job'>Job</a>
 with the data it needs to be processed.
@@ -116,7 +116,7 @@ type JobData<T> = JsonCompatible<T>
 
 ## JobConfig
 
-<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="44" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="46" packageName="@vendure/core" />
 
 Used to instantiate a new <a href='/reference/typescript-api/job-queue/job#job'>Job</a>
 

+ 2 - 2
docs/docs/reference/typescript-api/migration/generate-migration.md

@@ -11,14 +11,14 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## generateMigration
 
-<GenerationInfo sourceFile="packages/core/src/migrate.ts" sourceLine="107" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/migrate.ts" sourceLine="118" packageName="@vendure/core" />
 
 Generates a new migration file based on any schema changes (e.g. adding or removing CustomFields).
 See [TypeORM migration docs](https://typeorm.io/#/migrations) for more information about the
 underlying migration mechanism.
 
 ```ts title="Signature"
-function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions): void
+function generateMigration(userConfig: Partial<VendureConfig>, options: MigrationOptions): Promise<string | undefined>
 ```
 Parameters
 

+ 1 - 1
docs/docs/reference/typescript-api/migration/revert-last-migration.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## revertLastMigration
 
-<GenerationInfo sourceFile="packages/core/src/migrate.ts" sourceLine="82" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/migrate.ts" sourceLine="89" packageName="@vendure/core" />
 
 Reverts the last applied database migration. See [TypeORM migration docs](https://typeorm.io/#/migrations)
 for more information about the underlying migration mechanism.

+ 1 - 1
docs/docs/reference/typescript-api/migration/run-migrations.md

@@ -17,7 +17,7 @@ Runs any pending database migrations. See [TypeORM migration docs](https://typeo
 for more information about the underlying migration mechanism.
 
 ```ts title="Signature"
-function runMigrations(userConfig: Partial<VendureConfig>): void
+function runMigrations(userConfig: Partial<VendureConfig>): Promise<string[]>
 ```
 Parameters
 

+ 1 - 1
docs/docs/reference/typescript-api/orders/order-process.md

@@ -196,7 +196,7 @@ Parameters
 
 ## defaultOrderProcess
 
-<GenerationInfo sourceFile="packages/core/src/config/order/default-order-process.ts" sourceLine="476" packageName="@vendure/core" since="2.0.0" />
+<GenerationInfo sourceFile="packages/core/src/config/order/default-order-process.ts" sourceLine="475" packageName="@vendure/core" since="2.0.0" />
 
 This is the built-in <a href='/reference/typescript-api/orders/order-process#orderprocess'>OrderProcess</a> that ships with Vendure. A customized version of this process
 can be created using the <a href='/reference/typescript-api/orders/order-process#configuredefaultorderprocess'>configureDefaultOrderProcess</a> function, which allows you to pass in an object

+ 1 - 1
docs/docs/reference/typescript-api/service-helpers/order-calculator.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## OrderCalculator
 
-<GenerationInfo sourceFile="packages/core/src/service/helpers/order-calculator/order-calculator.ts" sourceLine="32" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/helpers/order-calculator/order-calculator.ts" sourceLine="33" packageName="@vendure/core" />
 
 This helper is used when making changes to an Order, to apply all applicable price adjustments to that Order,
 including:

+ 1 - 1
docs/docs/reference/typescript-api/service-helpers/product-price-applicator.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ProductPriceApplicator
 
-<GenerationInfo sourceFile="packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts" sourceLine="40" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts" sourceLine="41" packageName="@vendure/core" />
 
 This helper is used to apply the correct price to a ProductVariant based on the current context
 including active Channel, any current Order, etc. If you use the <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a> to

+ 25 - 25
docs/docs/reference/typescript-api/services/customer-service.md

@@ -31,8 +31,8 @@ class CustomerService {
     refreshVerificationToken(ctx: RequestContext, emailAddress: string) => Promise<void>;
     verifyCustomerEmailAddress(ctx: RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>>;
     requestPasswordReset(ctx: RequestContext, emailAddress: string) => Promise<void>;
-    resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
-        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
+    resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
+        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     >;
     requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string) => Promise<boolean | EmailAddressConflictError>;
     updateEmailAddress(ctx: RequestContext, token: string) => Promise<boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError>;
@@ -69,8 +69,8 @@ class CustomerService {
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, userId: <a href='/reference/typescript-api/common/id#id'>ID</a>, filterOnChannel:  = true) => Promise&#60;<a href='/reference/typescript-api/entities/customer#customer'>Customer</a> | undefined&#62;`}   />
 
-Returns the Customer entity associated with the given userId, if one exists.
-Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned
+Returns the Customer entity associated with the given userId, if one exists.
+Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned
 to the current active Channel only.
 ### findAddressesByCustomerId
 
@@ -86,13 +86,13 @@ Returns a list of all <a href='/reference/typescript-api/entities/customer-group
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: CreateCustomerInput, password?: string) => Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;CreateCustomerResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 
-Creates a new Customer, including creation of a new User with the special `customer` Role.
-
-If the `password` argument is specified, the Customer will be immediately verified. If not,
-then an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a> is published, so that the customer can have their
-email address verified and set their password in a later step using the `verifyCustomerEmailAddress()`
-method.
-
+Creates a new Customer, including creation of a new User with the special `customer` Role.
+
+If the `password` argument is specified, the Customer will be immediately verified. If not,
+then an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a> is published, so that the customer can have their
+email address verified and set their password in a later step using the `verifyCustomerEmailAddress()`
+method.
+
 This method is intended to be used in admin-created Customer flows.
 ### update
 
@@ -113,47 +113,47 @@ This method is intended to be used in admin-created Customer flows.
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: RegisterCustomerInput) => Promise&#60;RegisterCustomerAccountResult | EmailAddressConflictError | PasswordValidationError&#62;`}   />
 
-Registers a new Customer account with the <a href='/reference/typescript-api/auth/native-authentication-strategy#nativeauthenticationstrategy'>NativeAuthenticationStrategy</a> and starts
-the email verification flow (unless <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification` is set to `false`)
-by publishing an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
-
+Registers a new Customer account with the <a href='/reference/typescript-api/auth/native-authentication-strategy#nativeauthenticationstrategy'>NativeAuthenticationStrategy</a> and starts
+the email verification flow (unless <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification` is set to `false`)
+by publishing an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
+
 This method is intended to be used in storefront Customer-creation flows.
 ### refreshVerificationToken
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 
-Refreshes a stale email address verification token by generating a new one and
+Refreshes a stale email address verification token by generating a new one and
 publishing a <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
 ### verifyCustomerEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, verificationToken: string, password?: string) => Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;VerifyCustomerAccountResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 
-Given a valid verification token which has been published in an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>, this
+Given a valid verification token which has been published in an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>, this
 method is used to set the Customer as `verified` as part of the account registration flow.
 ### requestPasswordReset
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 
-Publishes a new <a href='/reference/typescript-api/events/event-types#passwordresetevent'>PasswordResetEvent</a> for the given email address. This event creates
+Publishes a new <a href='/reference/typescript-api/events/event-types#passwordresetevent'>PasswordResetEvent</a> for the given email address. This event creates
 a token which can be used in the `resetPassword()` method.
 ### resetPassword
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError     &#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;
         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     &#62;`}   />
 
-Given a valid password reset token created by a call to the `requestPasswordReset()` method,
+Given a valid password reset token created by a call to the `requestPasswordReset()` method,
 this method will change the Customer's password to that given as the `password` argument.
 ### requestUpdateEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, userId: <a href='/reference/typescript-api/common/id#id'>ID</a>, newEmailAddress: string) => Promise&#60;boolean | EmailAddressConflictError&#62;`}   />
 
-Publishes a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a> for the given User. This event contains a token
-which is then used in the `updateEmailAddress()` method to change the email address of the User &
+Publishes a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a> for the given User. This event contains a token
+which is then used in the `updateEmailAddress()` method to change the email address of the User &
 Customer.
 ### updateEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, token: string) => Promise&#60;boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError&#62;`}   />
 
-Given a valid email update token published in a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a>, this method
+Given a valid email update token published in a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a>, this method
 will update the Customer & User email address.
 ### createOrUpdate
 
@@ -184,8 +184,8 @@ Creates a new <a href='/reference/typescript-api/entities/address#address'>Addre
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>) => `}   />
 
-If the Customer associated with the given Order does not yet have any Addresses,
-this method will create new Address(es) based on the Order's shipping & billing
+If the Customer associated with the given Order does not yet have any Addresses,
+this method will create new Address(es) based on the Order's shipping & billing
 addresses.
 ### addNoteToCustomer
 

+ 11 - 11
docs/docs/reference/typescript-api/services/fulfillment-service.md

@@ -21,14 +21,14 @@ class FulfillmentService {
     create(ctx: RequestContext, orders: Order[], lines: OrderLineInput[], handler: ConfigurableOperationInput) => Promise<Fulfillment | InvalidFulfillmentHandlerError | CreateFulfillmentError>;
     getFulfillmentLines(ctx: RequestContext, id: ID) => Promise<FulfillmentLine[]>;
     getFulfillmentsLinesForOrderLine(ctx: RequestContext, orderLineId: ID, relations: RelationPaths<FulfillmentLine> = []) => Promise<FulfillmentLine[]>;
-    transitionToState(ctx: RequestContext, fulfillmentId: ID, state: FulfillmentState) => Promise<
-        | {
-              fulfillment: Fulfillment;
-              orders: Order[];
-              fromState: FulfillmentState;
-              toState: FulfillmentState;
-          }
-        | FulfillmentStateTransitionError
+    transitionToState(ctx: RequestContext, fulfillmentId: ID, state: FulfillmentState) => Promise<
+        | {
+              fulfillment: Fulfillment;
+              orders: Order[];
+              fromState: FulfillmentState;
+              toState: FulfillmentState;
+          }
+        | FulfillmentStateTransitionError
     >;
     getNextStates(fulfillment: Fulfillment) => readonly FulfillmentState[];
 }
@@ -45,7 +45,7 @@ class FulfillmentService {
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[], lines: OrderLineInput[], handler: ConfigurableOperationInput) => Promise&#60;<a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a> | InvalidFulfillmentHandlerError | CreateFulfillmentError&#62;`}   />
 
-Creates a new Fulfillment for the given Orders and OrderItems, using the specified
+Creates a new Fulfillment for the given Orders and OrderItems, using the specified
 <a href='/reference/typescript-api/fulfillment/fulfillment-handler#fulfillmenthandler'>FulfillmentHandler</a>.
 ### getFulfillmentLines
 
@@ -59,9 +59,9 @@ Creates a new Fulfillment for the given Orders and OrderItems, using the specifi
 
 ### transitionToState
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, fulfillmentId: <a href='/reference/typescript-api/common/id#id'>ID</a>, state: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>) => Promise&#60;         | {               fulfillment: <a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a>;               orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[];               fromState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;               toState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;           }         | FulfillmentStateTransitionError     &#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, fulfillmentId: <a href='/reference/typescript-api/common/id#id'>ID</a>, state: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>) => Promise&#60;
         | {
               fulfillment: <a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a>;
               orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[];
               fromState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;
               toState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;
           }
         | FulfillmentStateTransitionError
     &#62;`}   />
 
-Transitions the specified Fulfillment to a new state and upon successful transition
+Transitions the specified Fulfillment to a new state and upon successful transition
 publishes a <a href='/reference/typescript-api/events/event-types#fulfillmentstatetransitionevent'>FulfillmentStateTransitionEvent</a>.
 ### getNextStates
 

+ 1 - 1
docs/docs/reference/typescript-api/services/global-settings-service.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## GlobalSettingsService
 
-<GenerationInfo sourceFile="packages/core/src/service/services/global-settings.service.ts" sourceLine="21" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/global-settings.service.ts" sourceLine="22" packageName="@vendure/core" />
 
 Contains methods relating to the {@link GlobalSettings} entity.
 

+ 1 - 1
docs/docs/reference/typescript-api/services/order-service.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## OrderService
 
-<GenerationInfo sourceFile="packages/core/src/service/services/order.service.ts" sourceLine="133" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/order.service.ts" sourceLine="134" packageName="@vendure/core" />
 
 Contains methods relating to <a href='/reference/typescript-api/entities/order#order'>Order</a> entities.
 

+ 5 - 5
docs/docs/reference/typescript-api/services/payment-method-service.md

@@ -28,10 +28,10 @@ class PaymentMethodService {
     getPaymentMethodEligibilityCheckers(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getPaymentMethodHandlers(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getEligiblePaymentMethods(ctx: RequestContext, order: Order) => Promise<PaymentMethodQuote[]>;
-    getMethodAndOperations(ctx: RequestContext, method: string) => Promise<{
-        paymentMethod: PaymentMethod;
-        handler: PaymentMethodHandler;
-        checker: PaymentMethodEligibilityChecker | null;
+    getMethodAndOperations(ctx: RequestContext, method: string) => Promise<{
+        paymentMethod: PaymentMethod;
+        handler: PaymentMethodHandler;
+        checker: PaymentMethodEligibilityChecker | null;
     }>;
 }
 ```
@@ -95,7 +95,7 @@ class PaymentMethodService {
 
 ### getMethodAndOperations
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, method: string) => Promise&#60;{         paymentMethod: <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a>;         handler: <a href='/reference/typescript-api/payment/payment-method-handler#paymentmethodhandler'>PaymentMethodHandler</a>;         checker: <a href='/reference/typescript-api/payment/payment-method-eligibility-checker#paymentmethodeligibilitychecker'>PaymentMethodEligibilityChecker</a> | null;     }&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, method: string) => Promise&#60;{
         paymentMethod: <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a>;
         handler: <a href='/reference/typescript-api/payment/payment-method-handler#paymentmethodhandler'>PaymentMethodHandler</a>;
         checker: <a href='/reference/typescript-api/payment/payment-method-eligibility-checker#paymentmethodeligibilitychecker'>PaymentMethodEligibilityChecker</a> | null;
     }&#62;`}   />
 
 
 

+ 3 - 3
docs/docs/reference/typescript-api/services/tax-rate-service.md

@@ -11,13 +11,13 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## TaxRateService
 
-<GenerationInfo sourceFile="packages/core/src/service/services/tax-rate.service.ts" sourceLine="34" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/tax-rate.service.ts" sourceLine="35" packageName="@vendure/core" />
 
 Contains methods relating to <a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a> entities.
 
 ```ts title="Signature"
 class TaxRateService {
-    constructor(connection: TransactionalConnection, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, configService: ConfigService)
+    constructor(connection: TransactionalConnection, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, configService: ConfigService, customFieldRelationService: CustomFieldRelationService)
     findAll(ctx: RequestContext, options?: ListQueryOptions<TaxRate>, relations?: RelationPaths<TaxRate>) => Promise<PaginatedList<TaxRate>>;
     findOne(ctx: RequestContext, taxRateId: ID, relations?: RelationPaths<TaxRate>) => Promise<TaxRate | undefined>;
     create(ctx: RequestContext, input: CreateTaxRateInput) => Promise<TaxRate>;
@@ -31,7 +31,7 @@ class TaxRateService {
 
 ### constructor
 
-<MemberInfo kind="method" type={`(connection: <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a>, eventBus: <a href='/reference/typescript-api/events/event-bus#eventbus'>EventBus</a>, listQueryBuilder: <a href='/reference/typescript-api/data-access/list-query-builder#listquerybuilder'>ListQueryBuilder</a>, configService: ConfigService) => TaxRateService`}   />
+<MemberInfo kind="method" type={`(connection: <a href='/reference/typescript-api/data-access/transactional-connection#transactionalconnection'>TransactionalConnection</a>, eventBus: <a href='/reference/typescript-api/events/event-bus#eventbus'>EventBus</a>, listQueryBuilder: <a href='/reference/typescript-api/data-access/list-query-builder#listquerybuilder'>ListQueryBuilder</a>, configService: ConfigService, customFieldRelationService: CustomFieldRelationService) => TaxRateService`}   />
 
 
 ### findAll

+ 1 - 1
lerna.json

@@ -1,6 +1,6 @@
 {
     "packages": ["packages/*"],
-    "version": "2.2.0-next.8",
+    "version": "2.2.4",
     "npmClient": "npm",
     "command": {
         "version": {

+ 83 - 70
package-lock.json

@@ -3630,18 +3630,6 @@
                 "sisteransi": "^1.0.5"
             }
         },
-        "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
-            "version": "1.3.0",
-            "extraneous": true,
-            "inBundle": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=12"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/@clr/angular": {
             "version": "17.0.1",
             "license": "MIT",
@@ -14994,7 +14982,8 @@
         },
         "node_modules/dotenv": {
             "version": "16.4.5",
-            "license": "BSD-2-Clause",
+            "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+            "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
             "engines": {
                 "node": ">=12"
             },
@@ -31746,7 +31735,7 @@
         },
         "packages/admin-ui": {
             "name": "@vendure/admin-ui",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@angular/animations": "^17.2.4",
@@ -31769,7 +31758,7 @@
                 "@ng-select/ng-select": "^12.0.7",
                 "@ngx-translate/core": "^15.0.0",
                 "@ngx-translate/http-loader": "^8.0.0",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "apollo-angular": "^6.0.0",
                 "apollo-upload-client": "^18.0.1",
@@ -31840,7 +31829,7 @@
         },
         "packages/admin-ui-plugin": {
             "name": "@vendure/admin-ui-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "date-fns": "^2.30.0",
@@ -31849,9 +31838,9 @@
             "devDependencies": {
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/admin-ui": "2.2.0-next.8",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/admin-ui": "^2.2.4",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
                 "express": "^4.18.3",
                 "rimraf": "^5.0.5",
                 "typescript": "5.4.2"
@@ -31882,7 +31871,7 @@
         },
         "packages/asset-server-plugin": {
             "name": "@vendure/asset-server-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "file-type": "^19.0.0",
@@ -31895,8 +31884,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/node-fetch": "^2.6.11",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
                 "express": "^4.18.3",
                 "node-fetch": "^2.7.0",
                 "rimraf": "^5.0.5",
@@ -31908,23 +31897,25 @@
         },
         "packages/cli": {
             "name": "@vendure/cli",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
                 "change-case": "^4.1.2",
                 "commander": "^11.0.0",
+                "dotenv": "^16.4.5",
                 "fs-extra": "^11.2.0",
                 "picocolors": "^1.0.0",
                 "ts-morph": "^21.0.1",
-                "ts-node": "^10.9.2"
+                "ts-node": "^10.9.2",
+                "tsconfig-paths": "^4.2.0"
             },
             "bin": {
                 "vendure": "dist/cli.js"
             },
             "devDependencies": {
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/core": "^2.2.4",
                 "typescript": "5.3.3"
             },
             "funding": {
@@ -31938,9 +31929,30 @@
                 "node": ">=16"
             }
         },
+        "packages/cli/node_modules/strip-bom": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+            "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "packages/cli/node_modules/tsconfig-paths": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+            "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+            "dependencies": {
+                "json5": "^2.2.2",
+                "minimist": "^1.2.6",
+                "strip-bom": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
         "packages/common": {
             "name": "@vendure/common",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "devDependencies": {
                 "rimraf": "^5.0.5",
@@ -31952,7 +31964,7 @@
         },
         "packages/core": {
             "name": "@vendure/core",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@apollo/server": "^4.10.1",
@@ -31966,7 +31978,7 @@
                 "@nestjs/testing": "10.3.3",
                 "@nestjs/typeorm": "10.0.2",
                 "@types/fs-extra": "^9.0.1",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
                 "bcrypt": "^5.1.1",
                 "body-parser": "^1.20.2",
                 "cookie-session": "^2.1.0",
@@ -31992,6 +32004,7 @@
                 "progress": "^2.0.3",
                 "reflect-metadata": "^0.2.1",
                 "rxjs": "^7.8.1",
+                "semver": "^7.6.0",
                 "typeorm": "0.3.20"
             },
             "devDependencies": {
@@ -32101,11 +32114,11 @@
         },
         "packages/create": {
             "name": "@vendure/create",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "detect-port": "^1.5.1",
@@ -32124,7 +32137,7 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/core": "^2.2.4",
                 "rimraf": "^5.0.5",
                 "ts-node": "^10.9.2",
                 "typescript": "5.3.3"
@@ -32141,21 +32154,21 @@
             }
         },
         "packages/dev-server": {
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@nestjs/axios": "^3.0.2",
-                "@vendure/admin-ui-plugin": "2.2.0-next.8",
-                "@vendure/asset-server-plugin": "2.2.0-next.8",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
-                "@vendure/elasticsearch-plugin": "2.2.0-next.8",
-                "@vendure/email-plugin": "2.2.0-next.8",
+                "@vendure/admin-ui-plugin": "^2.2.4",
+                "@vendure/asset-server-plugin": "^2.2.4",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
+                "@vendure/elasticsearch-plugin": "^2.2.4",
+                "@vendure/email-plugin": "^2.2.4",
                 "typescript": "5.3.3"
             },
             "devDependencies": {
-                "@vendure/testing": "2.2.0-next.8",
-                "@vendure/ui-devkit": "2.2.0-next.8",
+                "@vendure/testing": "^2.2.4",
+                "@vendure/ui-devkit": "^2.2.4",
                 "commander": "^12.0.0",
                 "concurrently": "^8.2.2",
                 "csv-stringify": "^6.4.6",
@@ -32173,7 +32186,7 @@
         },
         "packages/elasticsearch-plugin": {
             "name": "@vendure/elasticsearch-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@elastic/elasticsearch": "~7.9.1",
@@ -32181,8 +32194,8 @@
                 "fast-deep-equal": "^3.1.3"
             },
             "devDependencies": {
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32192,7 +32205,7 @@
         },
         "packages/email-plugin": {
             "name": "@vendure/email-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@types/nodemailer": "^6.4.9",
@@ -32208,8 +32221,8 @@
                 "@types/express": "^4.17.21",
                 "@types/fs-extra": "^11.0.4",
                 "@types/mjml": "^4.7.4",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
                 "rimraf": "^5.0.5",
                 "typescript": "5.3.3"
             },
@@ -32219,14 +32232,14 @@
         },
         "packages/harden-plugin": {
             "name": "@vendure/harden-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "graphql-query-complexity": "^0.12.0"
             },
             "devDependencies": {
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8"
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32234,12 +32247,12 @@
         },
         "packages/job-queue-plugin": {
             "name": "@vendure/job-queue-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "devDependencies": {
                 "@google-cloud/pubsub": "^2.8.0",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
                 "bullmq": "^5.4.2",
                 "ioredis": "^5.3.2",
                 "rimraf": "^5.0.5",
@@ -32251,7 +32264,7 @@
         },
         "packages/payments-plugin": {
             "name": "@vendure/payments-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "currency.js": "2.0.4"
@@ -32260,9 +32273,9 @@
                 "@mollie/api-client": "^3.7.0",
                 "@types/braintree": "^3.3.11",
                 "@types/localtunnel": "2.0.4",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8",
-                "@vendure/testing": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4",
+                "@vendure/testing": "^2.2.4",
                 "braintree": "^3.22.0",
                 "localtunnel": "2.0.2",
                 "nock": "^13.1.4",
@@ -32318,12 +32331,12 @@
         },
         "packages/sentry-plugin": {
             "name": "@vendure/sentry-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "devDependencies": {
                 "@sentry/node": "^7.106.1",
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8"
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32334,14 +32347,14 @@
         },
         "packages/stellate-plugin": {
             "name": "@vendure/stellate-plugin",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "node-fetch": "^2.7.0"
             },
             "devDependencies": {
-                "@vendure/common": "2.2.0-next.8",
-                "@vendure/core": "2.2.0-next.8"
+                "@vendure/common": "^2.2.4",
+                "@vendure/core": "^2.2.4"
             },
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -32349,11 +32362,11 @@
         },
         "packages/testing": {
             "name": "@vendure/testing",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "dependencies": {
                 "@graphql-typed-document-node/core": "^3.2.0",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/common": "^2.2.4",
                 "faker": "^4.1.0",
                 "form-data": "^4.0.0",
                 "graphql": "16.8.1",
@@ -32366,7 +32379,7 @@
                 "@types/mysql": "^2.15.26",
                 "@types/node-fetch": "^2.6.4",
                 "@types/pg": "^8.11.2",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/core": "^2.2.4",
                 "mysql": "^2.18.1",
                 "pg": "^8.11.3",
                 "rimraf": "^5.0.5",
@@ -32382,15 +32395,15 @@
         },
         "packages/ui-devkit": {
             "name": "@vendure/ui-devkit",
-            "version": "2.2.0-next.8",
+            "version": "2.2.4",
             "license": "MIT",
             "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": "2.2.0-next.8",
-                "@vendure/common": "2.2.0-next.8",
+                "@vendure/admin-ui": "^2.2.4",
+                "@vendure/common": "^2.2.4",
                 "chalk": "^4.1.0",
                 "chokidar": "^3.6.0",
                 "fs-extra": "^11.2.0",
@@ -32401,7 +32414,7 @@
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/core": "2.2.0-next.8",
+                "@vendure/core": "^2.2.4",
                 "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": "2.2.0-next.8",
+    "version": "2.2.4",
     "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": "2.2.0-next.8",
-        "@vendure/common": "2.2.0-next.8",
-        "@vendure/core": "2.2.0-next.8",
+        "@vendure/admin-ui": "^2.2.4",
+        "@vendure/common": "^2.2.4",
+        "@vendure/core": "^2.2.4",
         "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": "2.2.0-next.8",
+    "version": "2.2.4",
     "license": "MIT",
     "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": "2.2.0-next.8",
+        "@vendure/common": "^2.2.4",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^6.0.0",
         "apollo-upload-client": "^18.0.1",

+ 5 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2858,6 +2858,10 @@ export type Mutation = {
   deleteZone: DeletionResponse;
   /** Delete a Zone */
   deleteZones: Array<DeletionResponse>;
+  /**
+   * Duplicate an existing entity using a specific EntityDuplicator.
+   * Since v2.2.0.
+   */
   duplicateEntity: DuplicateEntityResult;
   flushBufferedJobs: Success;
   importProducts?: Maybe<ImportInfo>;
@@ -5079,6 +5083,7 @@ export type Query = {
   customers: CustomerList;
   /** Returns a list of eligible shipping methods for the draft Order */
   eligibleShippingMethodsForDraftOrder: Array<ShippingMethodQuote>;
+  /** Returns all configured EntityDuplicators. */
   entityDuplicators: Array<EntityDuplicatorDefinition>;
   facet?: Maybe<Facet>;
   facetValues: FacetValueList;

+ 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 = '2.2.0-next.8';
+export const ADMIN_UI_VERSION = '2.2.4';

+ 6 - 2
packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts

@@ -47,7 +47,8 @@ export class DataTableFiltersComponent implements AfterViewInit {
             if (
                 event.target.tagName === 'INPUT' ||
                 event.target.tagName === 'TEXTAREA' ||
-                event.target.classList.contains('vdr-prosemirror')
+                event.target.classList.contains('vdr-prosemirror') ||
+                event.target.classList.contains('code-editor')
             ) {
                 return;
             }
@@ -57,7 +58,10 @@ export class DataTableFiltersComponent implements AfterViewInit {
         }
     }
 
-    constructor(private i18nService: I18nService, private changeDetectorRef: ChangeDetectorRef) {}
+    constructor(
+        private i18nService: I18nService,
+        private changeDetectorRef: ChangeDetectorRef,
+    ) {}
 
     ngAfterViewInit() {
         this.dropdown.onOpenChange(isOpen => {

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.html

@@ -6,7 +6,7 @@
     [typeahead]="searchInput$"
     multiple="true"
     appendTo="body"
-    bindLabel="name"
+    bindLabel="id"
     [disabled]="disabled || readonly"
     [ngModel]="value"
     (change)="onChange($event)"

+ 7 - 1
packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.ts

@@ -68,7 +68,10 @@ export class FacetValueSelectorComponent implements OnInit, OnDestroy, ControlVa
     disabled = false;
     value: Array<string | FacetValueFragment>;
     private subscription: Subscription;
-    constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}
+    constructor(
+        private dataService: DataService,
+        private changeDetectorRef: ChangeDetectorRef,
+    ) {}
 
     ngOnInit(): void {
         this.initSearchResults();
@@ -116,6 +119,9 @@ export class FacetValueSelectorComponent implements OnInit, OnDestroy, ControlVa
         if (this.readonly) {
             return;
         }
+        for (const sel of selected) {
+            console.log(`selected: ${sel.facet.name}:${sel.code}`);
+        }
         this.selectedValuesChange.emit(selected);
         if (this.onChangeFn) {
             const transformedValue = this.transformControlValueAccessorValue(selected);

+ 11 - 5
packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts

@@ -1,5 +1,4 @@
 import {
-    AfterViewInit,
     ChangeDetectionStrategy,
     Component,
     ElementRef,
@@ -9,8 +8,8 @@ import {
     OnInit,
     ViewChild,
 } from '@angular/core';
-import { Observable } from 'rxjs';
 import { CodeJar } from 'codejar';
+import { Observable } from 'rxjs';
 import { tap } from 'rxjs/operators';
 
 import { UIExtensionLocationId } from '../../../common/component-registry-types';
@@ -27,7 +26,7 @@ type UiExtensionType = 'actionBar' | 'actionBarDropdown' | 'navMenu' | 'detailCo
 })
 export class UiExtensionPointComponent implements OnInit {
     @Input() locationId: UIExtensionLocationId;
-    @Input() metadata?: any;
+    @Input() metadata?: string;
     @Input() topPx: number;
     @Input() leftPx: number;
     @HostBinding('style.display')
@@ -72,6 +71,7 @@ function highlightTsCode(tsCode: string) {
     tsCode = tsCode.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 
     return tsCode.replace(
+        // eslint-disable-next-line max-len
         /\b(abstract|any|as|boolean|break|case|catch|class|const|continue|default|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|never|new|null|number|object|of|private|protected|public|readonly|require|return|set|static|string|super|switch|symbol|this|throw|true|try|type|typeof|undefined|var|void|while|with|yield)\b|\/\/.*|\/\*[\s\S]*?\*\/|"(?:\\[\s\S]|[^\\"])*"|'[^']*'/g,
         (match, ...args) => {
             if (/^"/.test(match) || /^'/.test(match)) {
@@ -79,6 +79,7 @@ function highlightTsCode(tsCode: string) {
             } else if (/\/\/.*|\/\*[\s\S]*?\*\//.test(match)) {
                 return '<span class="ts-comment">' + match + '</span>';
             } else if (
+                // eslint-disable-next-line max-len
                 /\b(abstract|any|as|boolean|break|case|catch|class|const|continue|default|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|never|new|null|number|object|of|private|protected|public|readonly|require|return|set|static|string|super|switch|symbol|this|throw|true|try|type|typeof|undefined|var|void|while|with|yield)\b/.test(
                     match,
                 )
@@ -91,7 +92,10 @@ function highlightTsCode(tsCode: string) {
     );
 }
 
-const codeTemplates: Record<UiExtensionType, (locationId: UIExtensionLocationId, metadata: any) => string> = {
+const codeTemplates: Record<
+    UiExtensionType,
+    (locationId: UIExtensionLocationId, metadata?: string) => string
+> = {
     actionBar: locationId => `
 import { addActionBarItem } from '@vendure/admin-ui/core';
 
@@ -119,7 +123,9 @@ export default [
   addNavMenuSection({
       id: 'my-menu-item',
       label: 'My Menu Item',
-      routerLink: ['/extensions/my-plugin'],
+      items: [{
+          // ...
+      }],
     },
     '${locationId}',
   ),

+ 2 - 12
packages/admin-ui/src/lib/customer/src/customer.routes.ts

@@ -1,23 +1,13 @@
 import { Route } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import {
-    CanDeactivateDetailGuard,
-    createResolveData,
-    CustomerFragment,
-    detailBreadcrumb,
-    PageComponent,
-    PageService,
-} from '@vendure/admin-ui/core';
-
-import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
-import { CustomerGroupListComponent } from './components/customer-group-list/customer-group-list.component';
-import { CustomerListComponent } from './components/customer-list/customer-list.component';
+import { CustomerFragment, detailBreadcrumb, PageComponent, PageService } from '@vendure/admin-ui/core';
 
 export const createRoutes = (pageService: PageService): Route[] => [
     {
         path: 'customers',
         component: PageComponent,
         data: {
+            locationId: 'customer-list',
             breadcrumb: _('breadcrumb.customers'),
         },
         children: pageService.getPageTabRoutes('customer-list'),

+ 2 - 1
packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.ts

@@ -110,7 +110,8 @@ export class DraftOrderDetailComponent
                     .setCustomerForDraftOrder(this.id, { customerId: result.id })
                     .subscribe();
             } else if (result) {
-                this.dataService.order.setCustomerForDraftOrder(this.id, { input: result }).subscribe();
+                const { note, ...input } = result;
+                this.dataService.order.setCustomerForDraftOrder(this.id, { input }).subscribe();
             }
         });
     }

+ 6 - 3
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts

@@ -260,10 +260,13 @@ export class OrderEditorComponent
         const adjustedLine = this.modifyOrderInput.adjustOrderLines?.find(l => l.orderLineId === lineId);
         if (adjustedLine) {
             return adjustedLine.quantity;
-        } else {
-            const line = this.orderSnapshot.lines.find(l => l.id === lineId);
-            return line ? line.quantity : 0;
         }
+        const addedLine = this.modifyOrderInput.addItems?.find(l => this.getIdForAddedItem(l) === lineId);
+        if (addedLine) {
+            return addedLine.quantity ?? 1;
+        }
+        const line = this.orderSnapshot.lines.find(l => l.id === lineId);
+        return line ? line.quantity : 1;
     }
 
     updateLineQuantity(line: OrderDetailFragment['lines'][number] | AddedLine, quantity: string) {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 622 - 588
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": "2.2.0-next.8",
+    "version": "2.2.4",
     "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": "2.2.0-next.8",
-        "@vendure/core": "2.2.0-next.8",
+        "@vendure/common": "^2.2.4",
+        "@vendure/core": "^2.2.4",
         "express": "^4.18.3",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",

+ 6 - 4
packages/cli/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/cli",
-    "version": "2.2.0-next.8",
+    "version": "2.2.4",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -35,16 +35,18 @@
     ],
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "2.2.0-next.8",
+        "@vendure/common": "^2.2.4",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
+        "dotenv": "^16.4.5",
         "fs-extra": "^11.2.0",
         "picocolors": "^1.0.0",
         "ts-morph": "^21.0.1",
-        "ts-node": "^10.9.2"
+        "ts-node": "^10.9.2",
+        "tsconfig-paths": "^4.2.0"
     },
     "devDependencies": {
-        "@vendure/core": "2.2.0-next.8",
+        "@vendure/core": "^2.2.4",
         "typescript": "5.3.3"
     }
 }

+ 1 - 1
packages/cli/src/commands/add/add.ts

@@ -32,7 +32,7 @@ export async function addCommand() {
         message: 'Which feature would you like to add?',
         options: addCommands.map(c => ({
             value: c.id,
-            label: `[${c.category}] ${c.description}`,
+            label: `${pc.blue(`${c.category}`)} ${c.description}`,
         })),
     });
     if (isCancel(featureType)) {

+ 11 - 6
packages/cli/src/commands/add/api-extension/add-api-extension.ts

@@ -43,7 +43,7 @@ async function addApiExtension(
     options?: AddApiExtensionOptions,
 ): Promise<CliCommandReturnVal<{ serviceRef: ServiceRef }>> {
     const providedVendurePlugin = options?.plugin;
-    const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
+    const { project } = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const plugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
     const serviceRef = await selectServiceRef(project, plugin, false);
     const serviceEntityRef = serviceRef.crudEntityRef;
@@ -186,8 +186,8 @@ function createSimpleResolver(
     const resolverSourceFile = createFile(
         project,
         path.join(__dirname, 'templates/simple-resolver.template.ts'),
+        path.join(plugin.getPluginDir().getPath(), 'api', resolverFileName),
     );
-    resolverSourceFile.move(path.join(plugin.getPluginDir().getPath(), 'api', resolverFileName));
 
     const resolverClassDeclaration = resolverSourceFile
         .getClasses()
@@ -245,8 +245,6 @@ function createCrudResolver(
     const resolverSourceFile = createFile(
         project,
         path.join(__dirname, 'templates/crud-resolver.template.ts'),
-    );
-    resolverSourceFile.move(
         path.join(
             plugin.getPluginDir().getPath(),
             'api',
@@ -628,12 +626,19 @@ function getGraphQLType(type: Type): string | undefined {
 
 function getOrCreateApiExtensionsFile(project: Project, plugin: VendurePluginRef): SourceFile {
     const existingApiExtensionsFile = project.getSourceFiles().find(sf => {
-        return sf.getBaseName() === 'api-extensions.ts' && sf.getDirectory().getPath().endsWith('/api');
+        const filePath = sf.getDirectory().getPath();
+        return (
+            sf.getBaseName() === 'api-extensions.ts' &&
+            filePath.includes(plugin.getPluginDir().getPath()) &&
+            filePath.endsWith('/api')
+        );
     });
     if (existingApiExtensionsFile) {
         return existingApiExtensionsFile;
     }
-    return createFile(project, path.join(__dirname, 'templates/api-extensions.template.ts')).move(
+    return createFile(
+        project,
+        path.join(__dirname, 'templates/api-extensions.template.ts'),
         path.join(plugin.getPluginDir().getPath(), 'api', 'api-extensions.ts'),
     );
 }

+ 13 - 6
packages/cli/src/commands/add/codegen/add-codegen.ts

@@ -1,4 +1,4 @@
-import { log, note, outro, spinner } from '@clack/prompts';
+import { cancel, log, note, outro, spinner } from '@clack/prompts';
 import path from 'path';
 import { StructureKind } from 'ts-morph';
 
@@ -24,7 +24,7 @@ export const addCodegenCommand = new CliCommand({
 
 async function addCodegen(options?: AddCodegenOptions): Promise<CliCommandReturnVal> {
     const providedVendurePlugin = options?.plugin;
-    const project = await analyzeProject({
+    const { project } = await analyzeProject({
         providedVendurePlugin,
         cancelledMessage: 'Add codegen cancelled',
     });
@@ -51,6 +51,14 @@ async function addCodegen(options?: AddCodegenOptions): Promise<CliCommandReturn
             isDevDependency: true,
         });
     }
+    const packageManager = packageJson.determinePackageManager();
+    const packageJsonFile = packageJson.locatePackageJsonWithVendureDependency();
+    log.info(`Detected package manager: ${packageManager}`);
+    if (!packageJsonFile) {
+        cancel(`Could not locate package.json file with a dependency on Vendure.`);
+        process.exit(1);
+    }
+    log.info(`Detected package.json: ${packageJsonFile}`);
     try {
         await packageJson.installPackages(packagesToInstall);
     } catch (e: any) {
@@ -62,7 +70,7 @@ async function addCodegen(options?: AddCodegenOptions): Promise<CliCommandReturn
     configSpinner.start('Configuring codegen file...');
     await pauseForPromptDisplay();
 
-    const codegenFile = new CodegenConfigRef(packageJson.getPackageRootDir());
+    const codegenFile = new CodegenConfigRef(project, packageJson.getPackageRootDir());
 
     const rootDir = project.getDirectory('.');
     if (!rootDir) {
@@ -85,9 +93,9 @@ async function addCodegen(options?: AddCodegenOptions): Promise<CliCommandReturn
             codegenFile.addEntryToGeneratesObject({
                 name: `'${uiExtensionsPath}/gql/'`,
                 kind: StructureKind.PropertyAssignment,
-                initializer: `{ 
+                initializer: `{
                         preset: 'client',
-                        documents: '${uiExtensionsPath}/**/*.ts', 
+                        documents: '${uiExtensionsPath}/**/*.ts',
                         presetConfig: {
                             fragmentMasking: false,
                         },
@@ -101,7 +109,6 @@ async function addCodegen(options?: AddCodegenOptions): Promise<CliCommandReturn
     configSpinner.stop('Configured codegen file');
 
     await project.save();
-    await codegenFile.save();
 
     const nextSteps = [
         `You can run codegen by doing the following:`,

+ 8 - 7
packages/cli/src/commands/add/codegen/codegen-config-ref.ts

@@ -13,20 +13,21 @@ import {
 import { createFile, getTsMorphProject } from '../../../utilities/ast-utils';
 
 export class CodegenConfigRef {
-    private readonly tempProject: Project;
     public readonly sourceFile: SourceFile;
     private configObject: ObjectLiteralExpression | undefined;
-    constructor(rootDir: Directory) {
-        this.tempProject = getTsMorphProject({ skipAddingFilesFromTsConfig: true });
+    constructor(
+        private readonly project: Project,
+        rootDir: Directory,
+    ) {
         const codegenFilePath = path.join(rootDir.getPath(), 'codegen.ts');
         if (fs.existsSync(codegenFilePath)) {
-            this.sourceFile = this.tempProject.addSourceFileAtPath(codegenFilePath);
+            this.sourceFile = this.project.addSourceFileAtPath(codegenFilePath);
         } else {
             this.sourceFile = createFile(
-                this.tempProject,
+                this.project,
                 path.join(__dirname, 'templates/codegen.template.ts'),
+                path.join(rootDir.getPath(), 'codegen.ts'),
             );
-            this.sourceFile.move(path.join(rootDir.getPath(), 'codegen.ts'));
         }
     }
 
@@ -58,6 +59,6 @@ export class CodegenConfigRef {
     }
 
     save() {
-        return this.tempProject.save();
+        return this.project.save();
     }
 }

+ 27 - 14
packages/cli/src/commands/add/entity/add-entity.ts

@@ -37,7 +37,7 @@ async function addEntity(
     options?: Partial<AddEntityOptions>,
 ): Promise<CliCommandReturnVal<{ entityRef: EntityRef }>> {
     const providedVendurePlugin = options?.plugin;
-    const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
+    const { project } = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
     const modifiedSourceFiles: SourceFile[] = [];
 
@@ -109,24 +109,18 @@ function createEntity(plugin: VendurePluginRef, options: AddEntityOptions) {
     const entityFile = createFile(
         plugin.getSourceFile().getProject(),
         path.join(__dirname, 'templates/entity.template.ts'),
+        path.join(entitiesDir, `${options.fileName}.ts`),
     );
     const translationFile = createFile(
         plugin.getSourceFile().getProject(),
         path.join(__dirname, 'templates/entity-translation.template.ts'),
+        path.join(entitiesDir, `${options.translationFileName}.ts`),
     );
-    entityFile.move(path.join(entitiesDir, `${options.fileName}.ts`));
-    translationFile.move(path.join(entitiesDir, `${options.translationFileName}.ts`));
-
-    const entityClass = entityFile.getClass('ScaffoldEntity')?.rename(options.className);
-    const customFieldsClass = entityFile
-        .getClass('ScaffoldEntityCustomFields')
-        ?.rename(`${options.className}CustomFields`);
-    const translationClass = translationFile
-        .getClass('ScaffoldTranslation')
-        ?.rename(`${options.className}Translation`);
-    const translationCustomFieldsClass = translationFile
-        .getClass('ScaffoldEntityCustomFieldsTranslation')
-        ?.rename(`${options.className}CustomFieldsTranslation`);
+
+    const entityClass = entityFile.getClass('ScaffoldEntity');
+    const customFieldsClass = entityFile.getClass('ScaffoldEntityCustomFields');
+    const translationClass = translationFile.getClass('ScaffoldTranslation');
+    const translationCustomFieldsClass = translationFile.getClass('ScaffoldEntityCustomFieldsTranslation');
 
     if (!options.features.customFields) {
         // Remove custom fields from entity
@@ -142,6 +136,25 @@ function createEntity(plugin: VendurePluginRef, options: AddEntityOptions) {
         entityClass?.getProperty('translations')?.remove();
         removeImplementsFromClass('Translatable', entityClass);
         translationFile.delete();
+    } else {
+        entityFile
+            .getImportDeclaration('./entity-translation.template')
+            ?.setModuleSpecifier(`./${options.translationFileName}`);
+        translationFile
+            .getImportDeclaration('./entity.template')
+            ?.setModuleSpecifier(`./${options.fileName}`);
+    }
+
+    // Rename the entity classes
+    entityClass?.rename(options.className);
+    if (!customFieldsClass?.wasForgotten()) {
+        customFieldsClass?.rename(`${options.className}CustomFields`);
+    }
+    if (!translationClass?.wasForgotten()) {
+        translationClass?.rename(`${options.className}Translation`);
+    }
+    if (!translationCustomFieldsClass?.wasForgotten()) {
+        translationCustomFieldsClass?.rename(`${options.className}CustomFieldsTranslation`);
     }
 
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

+ 5 - 2
packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.spec.ts

@@ -19,8 +19,11 @@ describe('addEntityToPlugin', () => {
         const pluginClasses = getPluginClasses(project);
         expect(pluginClasses.length).toBe(1);
         const entityTemplatePath = path.join(__dirname, '../../templates/entity.template.ts');
-        const entityFile = createFile(project, entityTemplatePath);
-        entityFile.move(path.join(__dirname, 'fixtures', 'entity.ts'));
+        const entityFile = createFile(
+            project,
+            entityTemplatePath,
+            path.join(__dirname, 'fixtures', 'entity.ts'),
+        );
         const entityClass = entityFile.getClass('ScaffoldEntity');
         addEntityToPlugin(new VendurePluginRef(pluginClasses[0]), entityClass!);
 

+ 5 - 5
packages/cli/src/commands/add/job-queue/add-job-queue.ts

@@ -25,7 +25,7 @@ async function addJobQueue(
     options?: AddJobQueueOptions,
 ): Promise<CliCommandReturnVal<{ serviceRef: ServiceRef }>> {
     const providedVendurePlugin = options?.plugin;
-    const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
+    const { project } = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const plugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
     const serviceRef = await selectServiceRef(project, plugin);
 
@@ -107,19 +107,19 @@ async function addJobQueue(
                     const totalItems = 10;
                     for (let i = 0; i < totalItems; i++) {
                         await new Promise(resolve => setTimeout(resolve, 500));
-                        
+
                         // You can optionally respond to the job being cancelled
                         // during processing. This can be useful for very long-running
                         // tasks which can be cancelled by the user.
                         if (job.state === JobState.CANCELLED) {
                             throw new Error('Job was cancelled');
                         }
-                        
+
                         // Progress can be reported as a percentage like this
                         job.setProgress(Math.floor(i / totalItems * 100));
                     }
-                  
-                    // The value returned from the \`process\` function is stored 
+
+                    // The value returned from the \`process\` function is stored
                     // as the "result" field of the job
                     return {
                         processedCount: totalItems,

+ 88 - 37
packages/cli/src/commands/add/plugin/create-new-plugin.ts

@@ -2,11 +2,13 @@ import { cancel, intro, isCancel, log, select, spinner, text } from '@clack/prom
 import { constantCase, paramCase, pascalCase } from 'change-case';
 import * as fs from 'fs-extra';
 import path from 'path';
+import { Project, SourceFile } from 'ts-morph';
 
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
+import { analyzeProject } from '../../../shared/shared-prompts';
 import { VendureConfigRef } from '../../../shared/vendure-config-ref';
 import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
-import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils';
+import { addImportsToFile, createFile, getPluginClasses } from '../../../utilities/ast-utils';
 import { pauseForPromptDisplay } from '../../../utilities/utils';
 import { addApiExtensionCommand } from '../api-extension/add-api-extension';
 import { addCodegenCommand } from '../codegen/add-codegen';
@@ -29,6 +31,7 @@ const cancelledMessage = 'Plugin setup cancelled.';
 export async function createNewPlugin(): Promise<CliCommandReturnVal> {
     const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
     intro('Adding a new Vendure plugin!');
+    const { project } = await analyzeProject({ cancelledMessage });
     if (!options.name) {
         const name = await text({
             message: 'What is the name of the plugin?',
@@ -47,7 +50,8 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
             options.name = name;
         }
     }
-    const pluginDir = getPluginDirName(options.name);
+    const existingPluginDir = findExistingPluginsDir(project);
+    const pluginDir = getPluginDirName(options.name, existingPluginDir);
     const confirmation = await text({
         message: 'Plugin location',
         initialValue: pluginDir,
@@ -65,7 +69,7 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
     }
 
     options.pluginDir = confirmation;
-    const { plugin, project, modifiedSourceFiles } = await generatePlugin(options);
+    const { plugin, modifiedSourceFiles } = await generatePlugin(project, options);
 
     const configSpinner = spinner();
     configSpinner.start('Updating VendureConfig...');
@@ -89,9 +93,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
         addCodegenCommand,
     ];
     let allModifiedSourceFiles = [...modifiedSourceFiles];
-    const pluginClassName = plugin.name;
-    let workingPlugin = plugin;
-    let workingProject = project;
     while (!done) {
         const featureType = await select({
             message: `Add features to ${options.name}?`,
@@ -109,20 +110,11 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
         if (featureType === 'no') {
             done = true;
         } else {
-            const newProject = getTsMorphProject();
-            workingProject = newProject;
-            const newPlugin = newProject
-                .getSourceFile(workingPlugin.getSourceFile().getFilePath())
-                ?.getClass(pluginClassName);
-            if (!newPlugin) {
-                throw new Error(`Could not find class "${pluginClassName}" in the new project`);
-            }
-            workingPlugin = new VendurePluginRef(newPlugin);
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             const command = followUpCommands.find(c => c.id === featureType)!;
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             try {
-                const result = await command.run({ plugin: new VendurePluginRef(newPlugin) });
+                const result = await command.run({ plugin });
                 allModifiedSourceFiles = result.modifiedSourceFiles;
                 // We format all modified source files and re-load the
                 // project to avoid issues with the project state
@@ -133,8 +125,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
                 log.error(`Error adding feature "${command.id}"`);
                 log.error(e.stack);
             }
-
-            await workingProject.save();
         }
     }
 
@@ -145,8 +135,9 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
 }
 
 export async function generatePlugin(
+    project: Project,
     options: GeneratePluginOptions,
-): Promise<CliCommandReturnVal<{ plugin: VendurePluginRef }>> {
+): Promise<{ plugin: VendurePluginRef; modifiedSourceFiles: SourceFile[] }> {
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const normalizedName = nameWithoutPlugin + '-plugin';
     const templateContext: NewPluginTemplateContext = {
@@ -158,18 +149,31 @@ export async function generatePlugin(
     const projectSpinner = spinner();
     projectSpinner.start('Generating plugin scaffold...');
     await pauseForPromptDisplay();
-    const project = getTsMorphProject({ skipAddingFilesFromTsConfig: true });
 
-    const pluginFile = createFile(project, path.join(__dirname, 'templates/plugin.template.ts'));
+    const pluginFile = createFile(
+        project,
+        path.join(__dirname, 'templates/plugin.template.ts'),
+        path.join(options.pluginDir, paramCase(nameWithoutPlugin) + '.plugin.ts'),
+    );
     const pluginClass = pluginFile.getClass('TemplatePlugin');
     if (!pluginClass) {
         throw new Error('Could not find the plugin class in the generated file');
     }
+    pluginFile.getImportDeclaration('./constants.template')?.setModuleSpecifier('./constants');
+    pluginFile.getImportDeclaration('./types.template')?.setModuleSpecifier('./types');
     pluginClass.rename(templateContext.pluginName);
 
-    const typesFile = createFile(project, path.join(__dirname, 'templates/types.template.ts'));
+    const typesFile = createFile(
+        project,
+        path.join(__dirname, 'templates/types.template.ts'),
+        path.join(options.pluginDir, 'types.ts'),
+    );
 
-    const constantsFile = createFile(project, path.join(__dirname, 'templates/constants.template.ts'));
+    const constantsFile = createFile(
+        project,
+        path.join(__dirname, 'templates/constants.template.ts'),
+        path.join(options.pluginDir, 'constants.ts'),
+    );
     constantsFile
         .getVariableDeclaration('TEMPLATE_PLUGIN_OPTIONS')
         ?.rename(templateContext.pluginInitOptionsName)
@@ -178,31 +182,78 @@ export async function generatePlugin(
         .getVariableDeclaration('loggerCtx')
         ?.set({ initializer: `'${templateContext.pluginName}'` });
 
-    typesFile.move(path.join(options.pluginDir, 'types.ts'));
-    pluginFile.move(path.join(options.pluginDir, paramCase(nameWithoutPlugin) + '.plugin.ts'));
-    constantsFile.move(path.join(options.pluginDir, 'constants.ts'));
-
     projectSpinner.stop('Generated plugin scaffold');
     await project.save();
     return {
-        project,
         modifiedSourceFiles: [pluginFile, typesFile, constantsFile],
         plugin: new VendurePluginRef(pluginClass),
     };
 }
 
-function getPluginDirName(name: string) {
+function findExistingPluginsDir(project: Project): { prefix: string; suffix: string } | undefined {
+    const pluginClasses = getPluginClasses(project);
+    if (pluginClasses.length === 0) {
+        return;
+    }
+    if (pluginClasses.length === 1) {
+        return { prefix: path.dirname(pluginClasses[0].getSourceFile().getDirectoryPath()), suffix: '' };
+    }
+    const pluginDirs = pluginClasses.map(c => {
+        return c.getSourceFile().getDirectoryPath();
+    });
+    const prefix = findCommonPath(pluginDirs);
+    const suffixStartIndex = prefix.length;
+    const rest = pluginDirs[0].substring(suffixStartIndex).replace(/^\//, '').split('/');
+    const suffix = rest.length > 1 ? rest.slice(1).join('/') : '';
+    return { prefix, suffix };
+}
+
+function getPluginDirName(
+    name: string,
+    existingPluginDirPattern: { prefix: string; suffix: string } | undefined,
+) {
     const cwd = process.cwd();
-    const pathParts = cwd.split(path.sep);
-    const currentlyInPluginsDir = pathParts[pathParts.length - 1] === 'plugins';
-    const currentlyInRootDir = fs.pathExistsSync(path.join(cwd, 'package.json'));
     const nameWithoutPlugin = name.replace(/-?plugin$/i, '');
+    if (existingPluginDirPattern) {
+        return path.join(
+            existingPluginDirPattern.prefix,
+            paramCase(nameWithoutPlugin),
+            existingPluginDirPattern.suffix,
+        );
+    } else {
+        return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
+    }
+}
 
-    if (currentlyInPluginsDir) {
-        return path.join(cwd, paramCase(nameWithoutPlugin));
+function findCommonPath(paths: string[]): string {
+    if (paths.length === 0) {
+        return ''; // If no paths provided, return empty string
     }
-    if (currentlyInRootDir) {
-        return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
+
+    // Split each path into segments
+    const pathSegmentsList = paths.map(p => p.split('/'));
+
+    // Find the minimum length of path segments (to avoid out of bounds)
+    const minLength = Math.min(...pathSegmentsList.map(segments => segments.length));
+
+    // Initialize the common path
+    const commonPath: string[] = [];
+
+    // Loop through each segment index up to the minimum length
+    for (let i = 0; i < minLength; i++) {
+        // Get the segment at the current index from the first path
+        const currentSegment = pathSegmentsList[0][i];
+        // Check if this segment is common across all paths
+        const isCommon = pathSegmentsList.every(segments => segments[i] === currentSegment);
+        if (isCommon) {
+            // If it's common, add this segment to the common path
+            commonPath.push(currentSegment);
+        } else {
+            // If it's not common, break out of the loop
+            break;
+        }
     }
-    return path.join(cwd, paramCase(nameWithoutPlugin));
+
+    // Join the common path segments back into a string
+    return commonPath.join('/');
 }

+ 43 - 8
packages/cli/src/commands/add/service/add-service.ts

@@ -1,7 +1,7 @@
 import { cancel, isCancel, log, select, spinner, text } from '@clack/prompts';
 import { paramCase } from 'change-case';
 import path from 'path';
-import { ClassDeclaration, SourceFile } from 'ts-morph';
+import { ClassDeclaration, Scope, SourceFile } from 'ts-morph';
 
 import { Messages, pascalCaseRegex } from '../../../constants';
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
@@ -37,7 +37,7 @@ async function addService(
     providedOptions?: Partial<AddServiceOptions>,
 ): Promise<CliCommandReturnVal<{ serviceRef: ServiceRef }>> {
     const providedVendurePlugin = providedOptions?.plugin;
-    const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
+    const { project } = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
     const modifiedSourceFiles: SourceFile[] = [];
     const type =
@@ -101,8 +101,13 @@ async function addService(
 
         options.serviceName = name;
         serviceSpinner.start(`Creating ${options.serviceName}...`);
+        const serviceSourceFilePath = getServiceFilePath(vendurePlugin, options.serviceName);
         await pauseForPromptDisplay();
-        serviceSourceFile = createFile(project, path.join(__dirname, 'templates/basic-service.template.ts'));
+        serviceSourceFile = createFile(
+            project,
+            path.join(__dirname, 'templates/basic-service.template.ts'),
+            serviceSourceFilePath,
+        );
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         serviceClassDeclaration = serviceSourceFile
             .getClass('BasicServiceTemplate')!
@@ -110,7 +115,12 @@ async function addService(
     } else {
         serviceSpinner.start(`Creating ${options.serviceName}...`);
         await pauseForPromptDisplay();
-        serviceSourceFile = createFile(project, path.join(__dirname, 'templates/entity-service.template.ts'));
+        const serviceSourceFilePath = getServiceFilePath(vendurePlugin, options.serviceName);
+        serviceSourceFile = createFile(
+            project,
+            path.join(__dirname, 'templates/entity-service.template.ts'),
+            serviceSourceFilePath,
+        );
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         serviceClassDeclaration = serviceSourceFile
             .getClass('EntityServiceTemplate')!
@@ -150,11 +160,31 @@ async function addService(
         customizeUpdateMethod(serviceClassDeclaration, entityRef);
         removedUnusedConstructorArgs(serviceClassDeclaration, entityRef);
     }
+    const pluginOptions = vendurePlugin.getPluginOptions();
+    if (pluginOptions) {
+        addImportsToFile(serviceSourceFile, {
+            moduleSpecifier: pluginOptions.constantDeclaration.getSourceFile(),
+            namedImports: [pluginOptions.constantDeclaration.getName()],
+        });
+        addImportsToFile(serviceSourceFile, {
+            moduleSpecifier: pluginOptions.typeDeclaration.getSourceFile(),
+            namedImports: [pluginOptions.typeDeclaration.getName()],
+        });
+        addImportsToFile(serviceSourceFile, {
+            moduleSpecifier: '@nestjs/common',
+            namedImports: ['Inject'],
+        });
+        serviceClassDeclaration
+            .getConstructors()[0]
+            ?.addParameter({
+                scope: Scope.Private,
+                name: 'options',
+                type: pluginOptions.typeDeclaration.getName(),
+                decorators: [{ name: 'Inject', arguments: [pluginOptions.constantDeclaration.getName()] }],
+            })
+            .formatText();
+    }
     modifiedSourceFiles.push(serviceSourceFile);
-    const serviceFileName = paramCase(options.serviceName).replace(/-service$/, '.service');
-    serviceSourceFile?.move(
-        path.join(vendurePlugin.getPluginDir().getPath(), 'services', `${serviceFileName}.ts`),
-    );
 
     serviceSpinner.message(`Registering service with plugin...`);
 
@@ -175,6 +205,11 @@ async function addService(
     };
 }
 
+function getServiceFilePath(plugin: VendurePluginRef, serviceName: string) {
+    const serviceFileName = paramCase(serviceName).replace(/-service$/, '.service');
+    return path.join(plugin.getPluginDir().getPath(), 'services', `${serviceFileName}.ts`);
+}
+
 function customizeFindOneMethod(serviceClassDeclaration: ClassDeclaration, entityRef: EntityRef) {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const findOneMethod = serviceClassDeclaration.getMethod('findOne')!;

+ 21 - 7
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts

@@ -1,4 +1,5 @@
-import { log, note, outro, spinner } from '@clack/prompts';
+import { cancel, log, note, outro, spinner } from '@clack/prompts';
+import fs from 'fs-extra';
 import path from 'path';
 
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
@@ -24,7 +25,7 @@ export const addUiExtensionsCommand = new CliCommand<AddUiExtensionsOptions>({
 
 async function addUiExtensions(options?: AddUiExtensionsOptions): Promise<CliCommandReturnVal> {
     const providedVendurePlugin = options?.plugin;
-    const project = await analyzeProject({ providedVendurePlugin });
+    const { project } = await analyzeProject({ providedVendurePlugin });
     const vendurePlugin =
         providedVendurePlugin ?? (await selectPlugin(project, 'Add UI extensions cancelled'));
     const packageJson = new PackageJson(project);
@@ -37,7 +38,15 @@ async function addUiExtensions(options?: AddUiExtensionsOptions): Promise<CliCom
 
     log.success('Updated the plugin class');
     const installSpinner = spinner();
-    installSpinner.start(`Installing dependencies...`);
+    const packageManager = packageJson.determinePackageManager();
+    const packageJsonFile = packageJson.locatePackageJsonWithVendureDependency();
+    log.info(`Detected package manager: ${packageManager}`);
+    if (!packageJsonFile) {
+        cancel(`Could not locate package.json file with a dependency on Vendure.`);
+        process.exit(1);
+    }
+    log.info(`Detected package.json: ${packageJsonFile}`);
+    installSpinner.start(`Installing dependencies using ${packageManager}...`);
     try {
         const version = packageJson.determineVendureVersion();
         await packageJson.installPackages([
@@ -53,10 +62,15 @@ async function addUiExtensions(options?: AddUiExtensionsOptions): Promise<CliCom
     installSpinner.stop('Dependencies installed');
 
     const pluginDir = vendurePlugin.getPluginDir().getPath();
-    const providersFile = createFile(project, path.join(__dirname, 'templates/providers.template.ts'));
-    providersFile.move(path.join(pluginDir, 'ui', 'providers.ts'));
-    const routesFile = createFile(project, path.join(__dirname, 'templates/routes.template.ts'));
-    routesFile.move(path.join(pluginDir, 'ui', 'routes.ts'));
+
+    const providersFileDest = path.join(pluginDir, 'ui', 'providers.ts');
+    if (!fs.existsSync(providersFileDest)) {
+        createFile(project, path.join(__dirname, 'templates/providers.template.ts'), providersFileDest);
+    }
+    const routesFileDest = path.join(pluginDir, 'ui', 'routes.ts');
+    if (!fs.existsSync(routesFileDest)) {
+        createFile(project, path.join(__dirname, 'templates/routes.template.ts'), routesFileDest);
+    }
 
     log.success('Created UI extension scaffold');
 

+ 71 - 5
packages/cli/src/commands/migrate/generate-migration/generate-migration.ts

@@ -1,5 +1,8 @@
-import { cancel, isCancel, log, spinner, text } from '@clack/prompts';
-import { generateMigration } from '@vendure/core';
+import { cancel, isCancel, log, multiselect, select, spinner, text } from '@clack/prompts';
+import { unique } from '@vendure/common/lib/unique';
+import { generateMigration, VendureConfig } from '@vendure/core';
+import * as fs from 'fs-extra';
+import path from 'path';
 
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
 import { analyzeProject } from '../../../shared/shared-prompts';
@@ -16,7 +19,7 @@ export const generateMigrationCommand = new CliCommand({
 });
 
 async function runGenerateMigration(): Promise<CliCommandReturnVal> {
-    const project = await analyzeProject({ cancelledMessage });
+    const { project, tsConfigPath } = await analyzeProject({ cancelledMessage });
     const vendureConfig = new VendureConfigRef(project);
     log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
 
@@ -34,10 +37,47 @@ async function runGenerateMigration(): Promise<CliCommandReturnVal> {
         cancel(cancelledMessage);
         process.exit(0);
     }
-    const config = loadVendureConfigFile(vendureConfig);
+    const config = await loadVendureConfigFile(vendureConfig, tsConfigPath);
+
+    const migrationsDirs = getMigrationsDir(vendureConfig, config);
+    let migrationDir = migrationsDirs[0];
+
+    if (migrationsDirs.length > 1) {
+        const migrationDirSelect = await select({
+            message: 'Migration file location',
+            options: migrationsDirs
+                .map(c => ({
+                    value: c,
+                    label: c,
+                }))
+                .concat({
+                    value: 'other',
+                    label: 'Other',
+                }),
+        });
+        if (isCancel(migrationDirSelect)) {
+            cancel(cancelledMessage);
+            process.exit(0);
+        }
+        migrationDir = migrationDirSelect as string;
+    }
+
+    if (migrationsDirs.length === 1 || migrationDir === 'other') {
+        const confirmation = await text({
+            message: 'Migration file location',
+            initialValue: migrationsDirs[0],
+            placeholder: '',
+        });
+        if (isCancel(confirmation)) {
+            cancel(cancelledMessage);
+            process.exit(0);
+        }
+        migrationDir = confirmation;
+    }
+
     const migrationSpinner = spinner();
     migrationSpinner.start('Generating migration...');
-    const migrationName = await generateMigration(config, { name, outputDir: './src/migrations' });
+    const migrationName = await generateMigration(config, { name, outputDir: migrationDir });
     const report =
         typeof migrationName === 'string'
             ? `New migration generated: ${migrationName}`
@@ -48,3 +88,29 @@ async function runGenerateMigration(): Promise<CliCommandReturnVal> {
         modifiedSourceFiles: [],
     };
 }
+
+function getMigrationsDir(vendureConfigRef: VendureConfigRef, config: VendureConfig): string[] {
+    const options: string[] = [];
+    if (
+        Array.isArray(config.dbConnectionOptions.migrations) &&
+        config.dbConnectionOptions.migrations.length
+    ) {
+        const firstEntry = config.dbConnectionOptions.migrations[0];
+        if (typeof firstEntry === 'string') {
+            options.push(path.dirname(firstEntry));
+        }
+    }
+    const migrationFile = vendureConfigRef.sourceFile
+        .getProject()
+        .getSourceFiles()
+        .find(sf => {
+            return sf
+                .getClasses()
+                .find(c => c.getImplements().find(i => i.getText() === 'MigrationInterface'));
+        });
+    if (migrationFile) {
+        options.push(migrationFile.getDirectory().getPath());
+    }
+    options.push(path.join(vendureConfigRef.sourceFile.getDirectory().getPath(), '../migrations'));
+    return unique(options.map(p => path.normalize(p)));
+}

+ 26 - 3
packages/cli/src/commands/migrate/load-vendure-config-file.ts

@@ -1,15 +1,38 @@
+import { VendureConfig } from '@vendure/core';
 import path from 'node:path';
 import { register } from 'ts-node';
 
 import { VendureConfigRef } from '../../shared/vendure-config-ref';
+import { selectTsConfigFile } from '../../utilities/ast-utils';
 import { isRunningInTsNode } from '../../utilities/utils';
 
-export function loadVendureConfigFile(vendureConfig: VendureConfigRef) {
+export async function loadVendureConfigFile(
+    vendureConfig: VendureConfigRef,
+    providedTsConfigPath?: string,
+): Promise<VendureConfig> {
+    await import('dotenv/config');
     if (!isRunningInTsNode()) {
-        const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
+        let tsConfigPath: string;
+        if (providedTsConfigPath) {
+            tsConfigPath = providedTsConfigPath;
+        } else {
+            const tsConfigFile = await selectTsConfigFile();
+            tsConfigPath = path.join(process.cwd(), tsConfigFile);
+        }
         // eslint-disable-next-line @typescript-eslint/no-var-requires
         const compilerOptions = require(tsConfigPath).compilerOptions;
-        register({ compilerOptions, transpileOnly: true });
+        register({
+            compilerOptions: { ...compilerOptions, moduleResolution: 'NodeNext', module: 'NodeNext' },
+            transpileOnly: true,
+        });
+        if (compilerOptions.paths) {
+            // eslint-disable-next-line @typescript-eslint/no-var-requires
+            const tsConfigPaths = require('tsconfig-paths');
+            tsConfigPaths.register({
+                baseUrl: './',
+                paths: compilerOptions.paths,
+            });
+        }
     }
     const exportedVarName = vendureConfig.getConfigObjectVariableName();
     if (!exportedVarName) {

+ 2 - 2
packages/cli/src/commands/migrate/revert-migration/revert-migration.ts

@@ -16,10 +16,10 @@ export const revertMigrationCommand = new CliCommand({
 });
 
 async function runRevertMigration(): Promise<CliCommandReturnVal> {
-    const project = await analyzeProject({ cancelledMessage });
+    const { project } = await analyzeProject({ cancelledMessage });
     const vendureConfig = new VendureConfigRef(project);
     log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
-    const config = loadVendureConfigFile(vendureConfig);
+    const config = await loadVendureConfigFile(vendureConfig);
 
     const runSpinner = spinner();
     runSpinner.start('Reverting last migration...');

+ 2 - 2
packages/cli/src/commands/migrate/run-migration/run-migration.ts

@@ -16,10 +16,10 @@ export const runMigrationCommand = new CliCommand({
 });
 
 async function runRunMigration(): Promise<CliCommandReturnVal> {
-    const project = await analyzeProject({ cancelledMessage });
+    const { project } = await analyzeProject({ cancelledMessage });
     const vendureConfig = new VendureConfigRef(project);
     log.info('Using VendureConfig from ' + vendureConfig.getPathRelativeToProjectRoot());
-    const config = loadVendureConfigFile(vendureConfig);
+    const config = await loadVendureConfigFile(vendureConfig);
 
     const runSpinner = spinner();
     runSpinner.start('Running migrations...');

+ 69 - 5
packages/cli/src/shared/package-json-ref.ts

@@ -8,11 +8,22 @@ export interface PackageToInstall {
     pkg: string;
     version?: string;
     isDevDependency?: boolean;
+    installInRoot?: boolean;
 }
 
 export class PackageJson {
+    private _vendurePackageJsonPath: string | undefined;
+    private _rootPackageJsonPath: string | undefined;
     constructor(private readonly project: Project) {}
 
+    get vendurePackageJsonPath() {
+        return this.locatePackageJsonWithVendureDependency();
+    }
+
+    get rootPackageJsonPath() {
+        return this.locateRootPackageJson();
+    }
+
     determineVendureVersion(): string | undefined {
         const packageJson = this.getPackageJsonContent();
         return packageJson.dependencies['@vendure/core'];
@@ -42,8 +53,8 @@ export class PackageJson {
     }
 
     getPackageJsonContent() {
-        const packageJsonPath = path.join(this.getPackageRootDir().getPath(), 'package.json');
-        if (!fs.existsSync(packageJsonPath)) {
+        const packageJsonPath = this.locatePackageJsonWithVendureDependency();
+        if (!packageJsonPath || !fs.existsSync(packageJsonPath)) {
             note(
                 `Could not find a package.json in the current directory. Please run this command from the root of a Vendure project.`,
             );
@@ -73,9 +84,10 @@ export class PackageJson {
         const packageJson = this.getPackageJsonContent();
         packageJson.scripts = packageJson.scripts || {};
         packageJson.scripts[scriptName] = script;
-        const rootDir = this.getPackageRootDir();
-        const packageJsonPath = path.join(rootDir.getPath(), 'package.json');
-        fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
+        const packageJsonPath = this.vendurePackageJsonPath;
+        if (packageJsonPath) {
+            fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
+        }
     }
 
     getPackageRootDir() {
@@ -86,6 +98,52 @@ export class PackageJson {
         return rootDir;
     }
 
+    locateRootPackageJson() {
+        if (this._rootPackageJsonPath) {
+            return this._rootPackageJsonPath;
+        }
+        const rootDir = this.getPackageRootDir().getPath();
+        const rootPackageJsonPath = path.join(rootDir, 'package.json');
+        if (fs.existsSync(rootPackageJsonPath)) {
+            this._rootPackageJsonPath = rootPackageJsonPath;
+            return rootPackageJsonPath;
+        }
+        return null;
+    }
+
+    locatePackageJsonWithVendureDependency() {
+        if (this._vendurePackageJsonPath) {
+            return this._vendurePackageJsonPath;
+        }
+        const rootDir = this.getPackageRootDir().getPath();
+        const potentialMonorepoDirs = ['packages', 'apps', 'libs'];
+
+        const rootPackageJsonPath = path.join(this.getPackageRootDir().getPath(), 'package.json');
+        if (this.hasVendureDependency(rootPackageJsonPath)) {
+            return rootPackageJsonPath;
+        }
+        for (const dir of potentialMonorepoDirs) {
+            const monorepoDir = path.join(rootDir, dir);
+            // Check for a package.json in all subdirs
+            for (const subDir of fs.readdirSync(monorepoDir)) {
+                const packageJsonPath = path.join(monorepoDir, subDir, 'package.json');
+                if (this.hasVendureDependency(packageJsonPath)) {
+                    this._vendurePackageJsonPath = packageJsonPath;
+                    return packageJsonPath;
+                }
+            }
+        }
+        return null;
+    }
+
+    private hasVendureDependency(packageJsonPath: string) {
+        if (!fs.existsSync(packageJsonPath)) {
+            return false;
+        }
+        const packageJson = fs.readJsonSync(packageJsonPath);
+        return !!packageJson.dependencies?.['@vendure/core'];
+    }
+
     private async runPackageManagerInstall(dependencies: string[], isDev: boolean) {
         return new Promise<void>((resolve, reject) => {
             const packageManager = this.determinePackageManager();
@@ -99,6 +157,12 @@ export class PackageJson {
                 }
 
                 args = args.concat(dependencies);
+            } else if (packageManager === 'pnpm') {
+                command = 'pnpm';
+                args = ['add', '--save-exact'].concat(dependencies);
+                if (isDev) {
+                    args.push('--save-dev', '--workspace-root');
+                }
             } else {
                 command = 'npm';
                 args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);

+ 8 - 3
packages/cli/src/shared/shared-prompts.ts

@@ -3,7 +3,7 @@ import { ClassDeclaration, Project } from 'ts-morph';
 
 import { addServiceCommand } from '../commands/add/service/add-service';
 import { Messages } from '../constants';
-import { getPluginClasses, getTsMorphProject } from '../utilities/ast-utils';
+import { getPluginClasses, getTsMorphProject, selectTsConfigFile } from '../utilities/ast-utils';
 import { pauseForPromptDisplay } from '../utilities/utils';
 
 import { EntityRef } from './entity-ref';
@@ -16,14 +16,19 @@ export async function analyzeProject(options: {
 }) {
     const providedVendurePlugin = options.providedVendurePlugin;
     let project = providedVendurePlugin?.classDeclaration.getProject();
+    let tsConfigPath: string | undefined;
+
     if (!providedVendurePlugin) {
         const projectSpinner = spinner();
+        const tsConfigFile = await selectTsConfigFile();
         projectSpinner.start('Analyzing project...');
         await pauseForPromptDisplay();
-        project = getTsMorphProject();
+        const { project: _project, tsConfigPath: _tsConfigPath } = await getTsMorphProject({}, tsConfigFile);
+        project = _project;
+        tsConfigPath = _tsConfigPath;
         projectSpinner.stop('Project analyzed');
     }
-    return project as Project;
+    return { project: project as Project, tsConfigPath };
 }
 
 export async function selectPlugin(project: Project, cancelledMessage: string): Promise<VendurePluginRef> {

+ 50 - 1
packages/cli/src/shared/vendure-plugin-ref.ts

@@ -1,4 +1,13 @@
-import { ClassDeclaration, Node, StructureKind, SyntaxKind, VariableDeclaration } from 'ts-morph';
+import {
+    ClassDeclaration,
+    InterfaceDeclaration,
+    Node,
+    PropertyAssignment,
+    StructureKind,
+    SyntaxKind,
+    Type,
+    VariableDeclaration,
+} from 'ts-morph';
 
 import { AdminUiExtensionTypeName } from '../constants';
 
@@ -31,6 +40,46 @@ export class VendurePluginRef {
         return pluginOptions;
     }
 
+    getPluginOptions():
+        | { typeDeclaration: InterfaceDeclaration; constantDeclaration: VariableDeclaration }
+        | undefined {
+        const metadataOptions = this.getMetadataOptions();
+        const staticOptions = this.classDeclaration.getStaticProperty('options');
+        const typeDeclaration = staticOptions
+            ?.getType()
+            .getSymbolOrThrow()
+            .getDeclarations()
+            .find(d => Node.isInterfaceDeclaration(d));
+        if (!typeDeclaration || !Node.isInterfaceDeclaration(typeDeclaration)) {
+            return;
+        }
+        const providersArray = metadataOptions
+            .getProperty('providers')
+            ?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
+        if (!providersArray) {
+            return;
+        }
+        const elements = providersArray.getElements();
+        const optionsProviders = elements
+            .filter(Node.isObjectLiteralExpression)
+            .filter(el => el.getProperty('useFactory')?.getText().includes(`${this.name}.options`));
+
+        if (!optionsProviders.length) {
+            return;
+        }
+        const optionsSymbol = optionsProviders[0].getProperty('provide') as PropertyAssignment;
+        const initializer = optionsSymbol?.getInitializer();
+        if (!initializer || !Node.isIdentifier(initializer)) {
+            return;
+        }
+        const constantDeclaration = initializer.getDefinitions()[0]?.getDeclarationNode();
+        if (!constantDeclaration || !Node.isVariableDeclaration(constantDeclaration)) {
+            return;
+        }
+
+        return { typeDeclaration, constantDeclaration };
+    }
+
     addEntity(entityClassName: string) {
         const pluginOptions = this.getMetadataOptions();
         const entityProperty = pluginOptions.getProperty('entities');

+ 34 - 9
packages/cli/src/utilities/ast-utils.ts

@@ -1,4 +1,4 @@
-import { log } from '@clack/prompts';
+import { cancel, isCancel, log, select } from '@clack/prompts';
 import fs from 'fs-extra';
 import path from 'node:path';
 import { Directory, Node, Project, ProjectOptions, ScriptKind, SourceFile } from 'ts-morph';
@@ -6,22 +6,45 @@ import { Directory, Node, Project, ProjectOptions, ScriptKind, SourceFile } from
 import { defaultManipulationSettings } from '../constants';
 import { EntityRef } from '../shared/entity-ref';
 
-export function getTsMorphProject(options: ProjectOptions = {}) {
-    const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
+export async function selectTsConfigFile() {
+    const tsConfigFiles = fs.readdirSync(process.cwd()).filter(f => /^tsconfig.*\.json$/.test(f));
+    if (tsConfigFiles.length === 0) {
+        throw new Error('No tsconfig files found in current directory');
+    }
+    if (tsConfigFiles.length === 1) {
+        return tsConfigFiles[0];
+    }
+    const selectedConfigFile = await select({
+        message: 'Multiple tsconfig files found. Select one:',
+        options: tsConfigFiles.map(c => ({
+            value: c,
+            label: path.basename(c),
+        })),
+        maxItems: 10,
+    });
+    if (isCancel(selectedConfigFile)) {
+        cancel();
+        process.exit(0);
+    }
+    return selectedConfigFile as string;
+}
+
+export async function getTsMorphProject(options: ProjectOptions = {}, providedTsConfigPath?: string) {
+    const tsConfigFile = providedTsConfigPath ?? (await selectTsConfigFile());
+    const tsConfigPath = path.join(process.cwd(), tsConfigFile);
     if (!fs.existsSync(tsConfigPath)) {
         throw new Error('No tsconfig.json found in current directory');
     }
     const project = new Project({
         tsConfigFilePath: tsConfigPath,
         manipulationSettings: defaultManipulationSettings,
-        skipFileDependencyResolution: true,
         compilerOptions: {
             skipLibCheck: true,
         },
         ...options,
     });
     project.enableLogging(false);
-    return project;
+    return { project, tsConfigPath };
 }
 
 export function getPluginClasses(project: Project) {
@@ -99,14 +122,15 @@ export function getRelativeImportPath(locations: {
     return convertPathToRelativeImport(path.relative(fromDir, toPath));
 }
 
-export function createFile(project: Project, templatePath: string) {
+export function createFile(project: Project, templatePath: string, filePath: string) {
     const template = fs.readFileSync(templatePath, 'utf-8');
-    const tempFilePath = path.join('/.vendure-cli-temp/', path.basename(templatePath));
     try {
-        return project.createSourceFile(path.join('/.vendure-cli-temp/', tempFilePath), template, {
+        const file = project.createSourceFile(filePath, template, {
             overwrite: true,
             scriptKind: ScriptKind.TS,
         });
+        project.resolveSourceFileDependencies();
+        return file;
     } catch (e: any) {
         log.error(e.message);
         process.exit(1);
@@ -119,7 +143,8 @@ function convertPathToRelativeImport(filePath: string): string {
 
     // Remove the file extension
     const parsedPath = path.parse(normalizedPath);
-    return `./${parsedPath.dir}/${parsedPath.name}`.replace(/\/\//g, '/');
+    const prefix = parsedPath.dir.startsWith('..') ? '' : './';
+    return `${prefix}${parsedPath.dir}/${parsedPath.name}`.replace(/\/\//g, '/');
 }
 
 export function customizeCreateUpdateInputInterfaces(sourceFile: SourceFile, entityRef: EntityRef) {

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/common",
-    "version": "2.2.0-next.8",
+    "version": "2.2.4",
     "main": "index.js",
     "license": "MIT",
     "scripts": {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 680 - 647
packages/common/src/generated-shop-types.ts


+ 5 - 0
packages/common/src/generated-types.ts

@@ -2846,6 +2846,10 @@ export type Mutation = {
   deleteZone: DeletionResponse;
   /** Delete a Zone */
   deleteZones: Array<DeletionResponse>;
+  /**
+   * Duplicate an existing entity using a specific EntityDuplicator.
+   * Since v2.2.0.
+   */
   duplicateEntity: DuplicateEntityResult;
   flushBufferedJobs: Success;
   importProducts?: Maybe<ImportInfo>;
@@ -5004,6 +5008,7 @@ export type Query = {
   customers: CustomerList;
   /** Returns a list of eligible shipping methods for the draft Order */
   eligibleShippingMethodsForDraftOrder: Array<ShippingMethodQuote>;
+  /** Returns all configured EntityDuplicators. */
   entityDuplicators: Array<EntityDuplicatorDefinition>;
   facet?: Maybe<Facet>;
   facetValues: FacetValueList;

+ 16 - 2
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -73,6 +73,8 @@ import {
     AssignProductVariantsToChannelMutationVariables,
     RemoveProductVariantsFromChannelMutation,
     RemoveProductVariantsFromChannelMutationVariables,
+    UpdateChannelMutation,
+    UpdateChannelMutationVariables,
 } from './graphql/generated-e2e-admin-types';
 import {
     LogicalOperator,
@@ -93,6 +95,7 @@ import {
     REMOVE_PRODUCTVARIANT_FROM_CHANNEL,
     REMOVE_PRODUCT_FROM_CHANNEL,
     UPDATE_ASSET,
+    UPDATE_CHANNEL,
     UPDATE_COLLECTION,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
@@ -1365,6 +1368,7 @@ describe('Default search plugin', () => {
                         code: 'second-channel',
                         token: SECOND_CHANNEL_TOKEN,
                         defaultLanguageCode: LanguageCode.en,
+                        availableLanguageCodes: [LanguageCode.en, LanguageCode.de, LanguageCode.zh],
                         currencyCode: CurrencyCode.GBP,
                         pricesIncludeTax: true,
                         defaultTaxZoneId: 'T_1',
@@ -1562,6 +1566,16 @@ describe('Default search plugin', () => {
             beforeAll(async () => {
                 adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
 
+                await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(
+                    UPDATE_CHANNEL,
+                    {
+                        input: {
+                            id: 'T_1',
+                            availableLanguageCodes: [LanguageCode.en, LanguageCode.de, LanguageCode.zh],
+                        },
+                    },
+                );
+
                 const { updateProduct } = await adminClient.query<
                     UpdateProductMutation,
                     UpdateProductMutationVariables
@@ -1702,7 +1716,7 @@ describe('Default search plugin', () => {
                     expect(laptopVariantT4?.description).toEqual('Laptop description en');
                 });
 
-                it('indexes non-default language de', async () => {
+                it('indexes non-default language de when it is available language of channel', async () => {
                     const { search } = await searchInLanguage(LanguageCode.de);
 
                     const laptopVariants = search.items.filter(i => i.productId === 'T_1');
@@ -1733,7 +1747,7 @@ describe('Default search plugin', () => {
                     expect(laptopVariantT4?.description).toEqual('Laptop description de');
                 });
 
-                it('indexes non-default language zh', async () => {
+                it('indexes non-default language zh when it is available language of channel', async () => {
                     const { search } = await searchInLanguage(LanguageCode.zh);
 
                     const laptopVariants = search.items.filter(i => i.productId === 'T_1');

+ 28 - 20
packages/core/e2e/facet.e2e-spec.ts

@@ -182,6 +182,20 @@ describe('Facet resolver', () => {
         expect(result.facet?.valueList.totalItems).toBe(3);
     });
 
+    it('facet with valueList with name filter', async () => {
+        const result = await adminClient.query(GetFacetWithValueListDocument, {
+            id: speakerTypeFacet.id,
+            options: {
+                filter: {
+                    name: {
+                        contains: 'spea',
+                    },
+                },
+            },
+        });
+        expect(result.facet?.valueList.totalItems).toBe(2);
+    });
+
     it('product.facetValues resolver omits private facets in shop-api', async () => {
         const publicFacetValue = brandFacet.values[0];
         const privateFacetValue = speakerTypeFacet.values[0];
@@ -661,9 +675,8 @@ describe('Facet resolver', () => {
 
         it('removing from channel with error', async () => {
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: before } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: before } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(before.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
 
             adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
@@ -689,17 +702,15 @@ describe('Facet resolver', () => {
             ]);
 
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: after } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: after } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(after.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
         });
 
         it('force removing from channel', async () => {
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: before } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: before } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(before.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
 
             adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
@@ -717,17 +728,15 @@ describe('Facet resolver', () => {
             expect(removeFacetsFromChannel).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
 
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: after } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: after } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(after.items).toEqual([]);
         });
 
         it('assigning to channel', async () => {
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: before } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: before } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(before.items).toEqual([]);
 
             adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
@@ -744,9 +753,8 @@ describe('Facet resolver', () => {
             expect(assignFacetsToChannel).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
 
             adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
-            const { facets: after } = await adminClient.query<Codegen.GetFacetListSimpleQuery>(
-                GET_FACET_LIST_SIMPLE,
-            );
+            const { facets: after } =
+                await adminClient.query<Codegen.GetFacetListSimpleQuery>(GET_FACET_LIST_SIMPLE);
             expect(after.items).toEqual([{ id: 'T_4', name: 'Channel Facet' }]);
         });
     });
@@ -811,14 +819,14 @@ describe('Facet resolver', () => {
 });
 
 export const GET_FACET_WITH_VALUE_LIST = gql`
-    query GetFacetWithValueList($id: ID!) {
+    query GetFacetWithValueList($id: ID!, $options: FacetValueListOptions) {
         facet(id: $id) {
             id
             languageCode
             isPrivate
             code
             name
-            valueList {
+            valueList(options: $options) {
                 items {
                     ...FacetValue
                 }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 622 - 588
packages/core/e2e/graphql/generated-e2e-admin-types.ts


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 651 - 618
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 3 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/core",
-    "version": "2.2.0-next.8",
+    "version": "2.2.4",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -51,7 +51,7 @@
         "@nestjs/testing": "10.3.3",
         "@nestjs/typeorm": "10.0.2",
         "@types/fs-extra": "^9.0.1",
-        "@vendure/common": "2.2.0-next.8",
+        "@vendure/common": "^2.2.4",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "cookie-session": "^2.1.0",
@@ -77,6 +77,7 @@
         "progress": "^2.0.3",
         "reflect-metadata": "^0.2.1",
         "rxjs": "^7.8.1",
+        "semver": "^7.6.0",
         "typeorm": "0.3.20"
     },
     "devDependencies": {

+ 1 - 1
packages/core/src/api/config/configure-graphql-module.ts

@@ -1,6 +1,6 @@
 import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
 import { DynamicModule } from '@nestjs/common';
-import { GqlModuleOptions, GraphQLModule, GraphQLTypesLoader } from '@nestjs/graphql';
+import { GraphQLModule, GraphQLTypesLoader } from '@nestjs/graphql';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { buildSchema, extendSchema, GraphQLSchema, printSchema, ValidationContext } from 'graphql';
 import path from 'path';

+ 12 - 1
packages/core/src/api/config/generate-resolvers.ts

@@ -13,6 +13,7 @@ import { shopErrorOperationTypeResolvers } from '../../common/error/generated-gr
 import { Translatable } from '../../common/types/locale-types';
 import { ConfigService } from '../../config/config.service';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
+import { Logger } from '../../config/logger/vendure-logger';
 import { Region } from '../../entity/region/region.entity';
 import { getPluginAPIExtensions } from '../../plugin/plugin-metadata';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
@@ -212,7 +213,17 @@ function generateCustomFieldRelationResolvers(
                     const eagerEntity = source[fieldDef.name];
                     // If the relation is eager-loaded, we can simply try to translate this relation entity if they have translations
                     if (eagerEntity != null) {
-                        return customFieldRelationResolverService.translateEntity(ctx, eagerEntity, fieldDef);
+                        try {
+                            return await customFieldRelationResolverService.translateEntity(
+                                ctx,
+                                eagerEntity,
+                                fieldDef,
+                            );
+                        } catch (e: any) {
+                            Logger.debug(
+                                `Error resolving eager-loaded custom field entity relation "${entityName}.${fieldDef.name}": ${e.message as string}`,
+                            );
+                        }
                     }
                     const entityId = source[ENTITY_ID_KEY];
                     return customFieldRelationResolverService.resolveRelation({

+ 27 - 8
packages/core/src/api/config/graphql-custom-fields.ts

@@ -5,11 +5,12 @@ import {
     GraphQLInputObjectType,
     GraphQLList,
     GraphQLSchema,
-    isInterfaceType,
+    isObjectType,
     parse,
 } from 'graphql';
 
 import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
+import { Logger } from '../../config/logger/vendure-logger';
 
 import { getCustomFieldsConfigWithoutInterfaces } from './get-custom-fields-config-without-interfaces';
 
@@ -41,18 +42,36 @@ export function addGraphQLCustomFields(
 
     const customFieldsConfig = getCustomFieldsConfigWithoutInterfaces(customFieldConfig, schema);
     for (const [entityName, customFields] of customFieldsConfig) {
+        const gqlType = schema.getType(entityName);
+        if (isObjectType(gqlType) && gqlType.getFields().customFields) {
+            Logger.warn(
+                `The entity type "${entityName}" already has a "customFields" field defined. Skipping automatic custom field extension.`,
+            );
+            continue;
+        }
         const customEntityFields = customFields.filter(config => {
             return !config.internal && (publicOnly === true ? config.public !== false : true);
         });
 
         for (const fieldDef of customEntityFields) {
             if (fieldDef.type === 'relation') {
-                if (!schema.getType(fieldDef.graphQLType || fieldDef.entity.name)) {
-                    throw new Error(
-                        `The GraphQL type "${
-                            fieldDef?.graphQLType ?? '(unknown)'
-                        }" specified by the ${entityName}.${fieldDef.name} custom field does not exist`,
-                    );
+                const graphQlTypeName = fieldDef.graphQLType || fieldDef.entity.name;
+                if (!schema.getType(graphQlTypeName)) {
+                    const customFieldPath = `${entityName}.${fieldDef.name}`;
+                    const errorMessage = `The GraphQL type "${
+                        graphQlTypeName ?? '(unknown)'
+                    }" specified by the ${customFieldPath} custom field does not exist in the ${publicOnly ? 'Shop API' : 'Admin API'} schema.`;
+                    Logger.warn(errorMessage);
+                    if (publicOnly) {
+                        Logger.warn(
+                            [
+                                `This can be resolved by either:`,
+                                `  - setting \`public: false\` in the ${customFieldPath} custom field config`,
+                                `  - defining the "${graphQlTypeName}" type in the Shop API schema`,
+                            ].join('\n'),
+                        );
+                    }
+                    throw new Error(errorMessage);
                 }
             }
         }
@@ -245,7 +264,7 @@ export function addServerConfigCustomFields(
                     '',
                 )}
             }
-            
+
             type EntityCustomFields {
                 entityName: String!
                 customFields: [CustomFieldConfig!]!

+ 11 - 9
packages/core/src/bootstrap.ts

@@ -204,7 +204,7 @@ export async function preBootstrapConfig(
         await setConfig(userConfig);
     }
 
-    const entities = await getAllEntities(userConfig);
+    const entities = getAllEntities(userConfig);
     const { coreSubscribersMap } = await import('./entity/subscribers.js');
     await setConfig({
         dbConnectionOptions: {
@@ -250,7 +250,7 @@ function checkPluginCompatibility(config: RuntimeVendureConfig): void {
             if (!satisfies(VENDURE_VERSION, compatibility, { loose: true, includePrerelease: true })) {
                 Logger.error(
                     `Plugin "${pluginName}" is not compatible with this version of Vendure. ` +
-                        `It specifies a semver range of "${compatibility}" but the current version is "${VENDURE_VERSION}".`,
+                    `It specifies a semver range of "${compatibility}" but the current version is "${VENDURE_VERSION}".`,
                 );
                 throw new InternalServerError(
                     `Plugin "${pluginName}" is not compatible with this version of Vendure.`,
@@ -276,7 +276,7 @@ async function runPluginConfigurations(config: RuntimeVendureConfig): Promise<Ru
 /**
  * Returns an array of core entities and any additional entities defined in plugins.
  */
-export async function getAllEntities(userConfig: Partial<VendureConfig>): Promise<Array<Type<any>>> {
+export function getAllEntities(userConfig: Partial<VendureConfig>): Array<Type<any>> {
     const coreEntities = Object.values(coreEntitiesMap) as Array<Type<any>>;
     const pluginEntities = getEntitiesFromPlugins(userConfig.plugins);
 
@@ -410,19 +410,21 @@ export function configureSessionCookies(
     userConfig: Readonly<RuntimeVendureConfig>,
 ): void {
     const { cookieOptions } = userConfig.authOptions;
-    app.use(
-        cookieSession({
-            ...cookieOptions,
-            name: typeof cookieOptions?.name === 'string' ? cookieOptions.name : DEFAULT_COOKIE_NAME,
-        }),
-    );
 
     // If the Admin API and Shop API should have specific cookies names
     if (typeof cookieOptions?.name === 'object') {
         const shopApiCookieName = cookieOptions.name.shop;
         const adminApiCookieName = cookieOptions.name.admin;
         const { shopApiPath, adminApiPath } = userConfig.apiOptions;
+        app.use(cookieSession({...cookieOptions, name: shopApiCookieName}));
         app.use(`/${shopApiPath}`, cookieSession({ ...cookieOptions, name: shopApiCookieName }));
         app.use(`/${adminApiPath}`, cookieSession({ ...cookieOptions, name: adminApiCookieName }));
+    } else {
+        app.use(
+            cookieSession({
+                ...cookieOptions,
+                name: cookieOptions?.name ?? DEFAULT_COOKIE_NAME,
+            }),
+        );
     }
 }

+ 33 - 2
packages/core/src/config/config.service.ts

@@ -1,6 +1,6 @@
 import { DynamicModule, Injectable, Type } from '@nestjs/common';
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { DataSourceOptions } from 'typeorm';
+import { DataSourceOptions, getMetadataArgsStorage } from 'typeorm';
 
 import { getConfig } from './config-helpers';
 import { CustomFields } from './custom-field/custom-field-types';
@@ -27,6 +27,7 @@ import {
 @Injectable()
 export class ConfigService implements VendureConfig {
     private activeConfig: RuntimeVendureConfig;
+    private allCustomFieldsConfig: Required<CustomFields> | undefined;
 
     constructor() {
         this.activeConfig = getConfig();
@@ -97,7 +98,10 @@ export class ConfigService implements VendureConfig {
     }
 
     get customFields(): Required<CustomFields> {
-        return this.activeConfig.customFields;
+        if (!this.allCustomFieldsConfig) {
+            this.allCustomFieldsConfig = this.getCustomFieldsForAllEntities();
+        }
+        return this.allCustomFieldsConfig;
     }
 
     get plugins(): Array<DynamicModule | Type<any>> {
@@ -115,4 +119,31 @@ export class ConfigService implements VendureConfig {
     get systemOptions(): Required<SystemOptions> {
         return this.activeConfig.systemOptions;
     }
+
+    private getCustomFieldsForAllEntities(): Required<CustomFields> {
+        const definedCustomFields = this.activeConfig.customFields;
+        const metadataArgsStorage = getMetadataArgsStorage();
+        // We need to check for any entities which have a "customFields" property but which are not
+        // explicitly defined in the customFields config. This is because the customFields object
+        // only includes the built-in entities. Any custom entities which have a "customFields"
+        // must be dynamically added to the customFields object.
+        if (Array.isArray(this.dbConnectionOptions.entities)) {
+            for (const entity of this.dbConnectionOptions.entities) {
+                if (typeof entity === 'function' && !definedCustomFields[entity.name]) {
+                    const hasCustomFields = !!metadataArgsStorage
+                        .filterEmbeddeds(entity)
+                        .find(c => c.propertyName === 'customFields');
+                    const isTranslationEntity =
+                        entity.name.endsWith('Translation') &&
+                        metadataArgsStorage
+                            .filterColumns(entity)
+                            .find(c => c.propertyName === 'languageCode');
+                    if (hasCustomFields && !isTranslationEntity) {
+                        definedCustomFields[entity.name] = [];
+                    }
+                }
+            }
+        }
+        return definedCustomFields;
+    }
 }

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels