Procházet zdrojové kódy

docs: Improve docs working with DB relations

Relates to #2299
Michael Bromley před 2 roky
rodič
revize
de8e3c054c

+ 80 - 0
docs/docs/guides/developer-guide/the-service-layer/index.mdx

@@ -229,3 +229,83 @@ export class ItemService {
 Further examples can be found in the [TypeORM QueryBuilder documentation](https://typeorm.io/select-query-builder).
 Further examples can be found in the [TypeORM QueryBuilder documentation](https://typeorm.io/select-query-builder).
 
 
 :::
 :::
+
+### Working with relations
+
+One limitation of TypeORM's typings is that we have no way of knowing at build-time whether a particular relation will be
+joined at runtime. For instance, the following code will build without issues, but will result in a runtime error:
+
+```ts
+const product = await this.connection.getRepository(ctx, Product).findOne({
+    where: { id: productId },
+});
+if (product) {
+    // highlight-start
+    console.log(product.featuredAsset.preview);
+    // ^ Error: Cannot read property 'preview' of undefined
+    // highlight-end
+}
+```
+
+This is because the `featuredAsset` relation is not joined by default. The simple fix for the above example is to use
+the `relations` option:
+
+```ts
+const product = await this.connection.getRepository(ctx, Product).findOne({
+    where: { id: productId },
+    // highlight-next-line
+    relations: { featuredAsset: true },
+});
+```
+or in the case of the QueryBuilder API, we can use the `leftJoinAndSelect()` method:
+
+```ts
+const product = await this.connection.getRepository(ctx, Product).createQueryBuilder('product')
+    // highlight-next-line
+    .leftJoinAndSelect('product.featuredAsset', 'featuredAsset')
+    .where('product.id = :id', { id: productId })
+    .getOne();
+```
+
+### Using the EntityHydrator
+
+But what about when we do not control the code which fetches the entity from the database? For instance, we might be implementing
+a function which gets an entity passed to it by Vendure. In this case, we can use the [`EntityHydrator`](/reference/typescript-api/data-access/entity-hydrator/)
+to ensure that a given relation is "hydrated" (i.e. joined) before we use it:
+
+```ts
+import { EntityHydrator, ShippingCalculator } from '@vendure/core';
+
+let entityHydrator: EntityHydrator;
+
+const myShippingCalculator = new ShippingCalculator({
+    // ... rest of config omitted for brevity
+    init(injector) {
+        entityHydrator = injector.get(EntityHydrator);
+    },
+    calculate: (ctx, order, args) => {
+      // highlight-start
+      // ensure that the customer and customer.groups relations are joined
+      await entityHydrator.hydrate(ctx, order, { relations: ['customer.groups' ]});
+      // highlight-end
+
+      if (order.customer?.groups?.some(g => g.name === 'VIP')) {
+        // ... do something special for VIP customers
+      } else {
+        // ... do something else
+      }
+    },
+});
+```
+
+### Joining relations in built-in service methods
+
+Many of the core services allow an optional `relations` argument in their `findOne()` and `findMany()` and related methods.
+This allows you to specify which relations should be joined when the query is executed. For instance, in the [`ProductService`](/reference/typescript-api/services/product-service)
+there is a `findOne()` method which allows you to specify which relations should be joined:
+
+```ts
+const productWithAssets = await this.productService
+    .findOne(ctx, productId, ['featuredAsset', 'assets']);
+```
+

+ 2 - 2
docs/docs/reference/admin-ui-api/custom-input-components/default-inputs.md

@@ -289,7 +289,7 @@ class CustomerGroupFormInputComponent implements FormInputComponent, OnInit {
     constructor(dataService: DataService)
     constructor(dataService: DataService)
     ngOnInit() => ;
     ngOnInit() => ;
     selectGroup(group: ItemOf<GetCustomerGroupsQuery, 'customerGroups'>) => ;
     selectGroup(group: ItemOf<GetCustomerGroupsQuery, 'customerGroups'>) => ;
-    compareWith(o1: ItemOf<GetCustomerGroupsQuery, 'customerGroups'>, o2: ItemOf<GetCustomerGroupsQuery, 'customerGroups'>) => ;
+    compareWith(o1: T, o2: T) => ;
 }
 }
 ```
 ```
 * Implements: <code><a href='/reference/admin-ui-api/custom-input-components/form-input-component#forminputcomponent'>FormInputComponent</a></code>, <code>OnInit</code>
 * Implements: <code><a href='/reference/admin-ui-api/custom-input-components/form-input-component#forminputcomponent'>FormInputComponent</a></code>, <code>OnInit</code>
@@ -340,7 +340,7 @@ class CustomerGroupFormInputComponent implements FormInputComponent, OnInit {
 
 
 ### compareWith
 ### compareWith
 
 
-<MemberInfo kind="method" type={`(o1: ItemOf&#60;GetCustomerGroupsQuery, 'customerGroups'&#62;, o2: ItemOf&#60;GetCustomerGroupsQuery, 'customerGroups'&#62;) => `}   />
+<MemberInfo kind="method" type={`(o1: T, o2: T) => `}   />
 
 
 
 
 
 

+ 1 - 1
docs/docs/reference/admin-ui-api/custom-table-components/custom-column-component.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## CustomColumnComponent
 ## CustomColumnComponent
 
 
-<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts" sourceLine="43" packageName="@vendure/admin-ui" />
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts" sourceLine="44" packageName="@vendure/admin-ui" />
 
 
 Components which are to be used to render custom cells in a data table should implement this interface.
 Components which are to be used to render custom cells in a data table should implement this interface.
 
 

+ 1 - 1
docs/docs/reference/admin-ui-api/custom-table-components/data-table-component-config.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DataTableComponentConfig
 ## DataTableComponentConfig
 
 
-<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts" sourceLine="53" packageName="@vendure/admin-ui" />
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts" sourceLine="54" packageName="@vendure/admin-ui" />
 
 
 Configures a <a href='/reference/admin-ui-api/custom-detail-components/custom-detail-component#customdetailcomponent'>CustomDetailComponent</a> to be placed in the given location.
 Configures a <a href='/reference/admin-ui-api/custom-detail-components/custom-detail-component#customdetailcomponent'>CustomDetailComponent</a> to be placed in the given location.
 
 

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

@@ -11,20 +11,42 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## EntityHydrator
 ## EntityHydrator
 
 
-<GenerationInfo sourceFile="packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts" sourceLine="53" packageName="@vendure/core" since="1.3.0" />
+<GenerationInfo sourceFile="packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts" sourceLine="75" packageName="@vendure/core" since="1.3.0" />
 
 
 This is a helper class which is used to "hydrate" entity instances, which means to populate them
 This is a helper class which is used to "hydrate" entity instances, which means to populate them
-with the specified relations. This is useful when writing plugin code which receives an entity
+with the specified relations. This is useful when writing plugin code which receives an entity,
 and you need to ensure that one or more relations are present.
 and you need to ensure that one or more relations are present.
 
 
 *Example*
 *Example*
 
 
 ```ts
 ```ts
-const product = await this.productVariantService
-  .getProductForVariant(ctx, variantId);
+import { Injectable } from '@nestjs/common';
+import { ID, RequestContext, EntityHydrator, ProductVariantService } from '@vendure/core';
 
 
-await this.entityHydrator
-  .hydrate(ctx, product, { relations: ['facetValues.facet' ]});
+@Injectable()
+export class MyService {
+
+  constructor(
+     // highlight-next-line
+     private entityHydrator: EntityHydrator,
+     private productVariantService: ProductVariantService,
+  ) {}
+
+  myMethod(ctx: RequestContext, variantId: ID) {
+    const product = await this.productVariantService
+      .getProductForVariant(ctx, variantId);
+
+    // at this stage, we don't know which of the Product relations
+    // will be joined at runtime.
+
+    // highlight-start
+    await this.entityHydrator
+      .hydrate(ctx, product, { relations: ['facetValues.facet' ]});
+
+    // You can be sure now that the `facetValues` & `facetValues.facet` relations are populated
+    // highlight-end
+  }
+}
 ```
 ```
 
 
 In this above example, the `product` instance will now have the `facetValues` relation
 In this above example, the `product` instance will now have the `facetValues` relation

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

@@ -31,8 +31,8 @@ class CustomerService {
     refreshVerificationToken(ctx: RequestContext, emailAddress: string) => Promise<void>;
     refreshVerificationToken(ctx: RequestContext, emailAddress: string) => Promise<void>;
     verifyCustomerEmailAddress(ctx: RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>>;
     verifyCustomerEmailAddress(ctx: RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>>;
     requestPasswordReset(ctx: RequestContext, emailAddress: string) => Promise<void>;
     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>;
     requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string) => Promise<boolean | EmailAddressConflictError>;
     updateEmailAddress(ctx: RequestContext, token: string) => Promise<boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError>;
     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;`}   />
 <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.
 to the current active Channel only.
 ### findAddressesByCustomerId
 ### 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;ErrorResultUnion&#60;CreateCustomerResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: CreateCustomerInput, password?: string) => Promise&#60;ErrorResultUnion&#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.
 This method is intended to be used in admin-created Customer flows.
 ### update
 ### 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;`}   />
 <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.
 This method is intended to be used in storefront Customer-creation flows.
 ### refreshVerificationToken
 ### refreshVerificationToken
 
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 <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>.
 publishing a <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
 ### verifyCustomerEmailAddress
 ### verifyCustomerEmailAddress
 
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, verificationToken: string, password?: string) => Promise&#60;ErrorResultUnion&#60;VerifyCustomerAccountResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, verificationToken: string, password?: string) => Promise&#60;ErrorResultUnion&#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.
 method is used to set the Customer as `verified` as part of the account registration flow.
 ### requestPasswordReset
 ### requestPasswordReset
 
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 <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.
 a token which can be used in the `resetPassword()` method.
 ### resetPassword
 ### 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.
 this method will change the Customer's password to that given as the `password` argument.
 ### requestUpdateEmailAddress
 ### 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;`}   />
 <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.
 Customer.
 ### updateEmailAddress
 ### 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;`}   />
 <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.
 will update the Customer & User email address.
 ### createOrUpdate
 ### 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>) => `}   />
 <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.
 addresses.
 ### addNoteToCustomer
 ### addNoteToCustomer
 
 

+ 4 - 4
docs/docs/reference/typescript-api/shipping/shipping-calculator.md

@@ -31,7 +31,7 @@ const flatRateCalculator = new ShippingCalculator({
       ui: { component: 'number-form-input', suffix: '%' },
       ui: { component: 'number-form-input', suffix: '%' },
     },
     },
   },
   },
-  calculate: (order, args) => {
+  calculate: (ctx, order, args) => {
     return {
     return {
       price: args.rate,
       price: args.rate,
       taxRate: args.taxRate,
       taxRate: args.taxRate,
@@ -64,7 +64,7 @@ class ShippingCalculator<T extends ConfigArgs = ConfigArgs> extends Configurable
 
 
 ## ShippingCalculationResult
 ## ShippingCalculationResult
 
 
-<GenerationInfo sourceFile="packages/core/src/config/shipping-method/shipping-calculator.ts" sourceLine="74" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/shipping-method/shipping-calculator.ts" sourceLine="79" packageName="@vendure/core" />
 
 
 The return value of the <a href='/reference/typescript-api/shipping/shipping-calculator#calculateshippingfn'>CalculateShippingFn</a>.
 The return value of the <a href='/reference/typescript-api/shipping/shipping-calculator#calculateshippingfn'>CalculateShippingFn</a>.
 
 
@@ -108,7 +108,7 @@ needed in the storefront application when listing eligible shipping methods.
 
 
 ## CalculateShippingFn
 ## CalculateShippingFn
 
 
-<GenerationInfo sourceFile="packages/core/src/config/shipping-method/shipping-calculator.ts" sourceLine="114" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/shipping-method/shipping-calculator.ts" sourceLine="119" packageName="@vendure/core" />
 
 
 A function which implements the specific shipping calculation logic. It takes an <a href='/reference/typescript-api/entities/order#order'>Order</a> and
 A function which implements the specific shipping calculation logic. It takes an <a href='/reference/typescript-api/entities/order#order'>Order</a> and
 an arguments object and should return the shipping price as an integer in cents.
 an arguments object and should return the shipping price as an integer in cents.
@@ -120,6 +120,6 @@ type CalculateShippingFn<T extends ConfigArgs> = (
     ctx: RequestContext,
     ctx: RequestContext,
     order: Order,
     order: Order,
     args: ConfigArgValues<T>,
     args: ConfigArgValues<T>,
-    method: ShippingMethod
+    method: ShippingMethod,
 ) => CalculateShippingFnResult
 ) => CalculateShippingFnResult
 ```
 ```

+ 27 - 5
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -16,16 +16,38 @@ import { HydrateOptions } from './entity-hydrator-types';
 /**
 /**
  * @description
  * @description
  * This is a helper class which is used to "hydrate" entity instances, which means to populate them
  * This is a helper class which is used to "hydrate" entity instances, which means to populate them
- * with the specified relations. This is useful when writing plugin code which receives an entity
+ * with the specified relations. This is useful when writing plugin code which receives an entity,
  * and you need to ensure that one or more relations are present.
  * and you need to ensure that one or more relations are present.
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
- * const product = await this.productVariantService
- *   .getProductForVariant(ctx, variantId);
+ * import { Injectable } from '\@nestjs/common';
+ * import { ID, RequestContext, EntityHydrator, ProductVariantService } from '\@vendure/core';
  *
  *
- * await this.entityHydrator
- *   .hydrate(ctx, product, { relations: ['facetValues.facet' ]});
+ * \@Injectable()
+ * export class MyService {
+ *
+ *   constructor(
+ *      // highlight-next-line
+ *      private entityHydrator: EntityHydrator,
+ *      private productVariantService: ProductVariantService,
+ *   ) {}
+ *
+ *   myMethod(ctx: RequestContext, variantId: ID) {
+ *     const product = await this.productVariantService
+ *       .getProductForVariant(ctx, variantId);
+ *
+ *     // at this stage, we don't know which of the Product relations
+ *     // will be joined at runtime.
+ *
+ *     // highlight-start
+ *     await this.entityHydrator
+ *       .hydrate(ctx, product, { relations: ['facetValues.facet' ]});
+ *
+ *     // You can be sure now that the `facetValues` & `facetValues.facet` relations are populated
+ *     // highlight-end
+ *   }
+ * }
  *```
  *```
  *
  *
  * In this above example, the `product` instance will now have the `facetValues` relation
  * In this above example, the `product` instance will now have the `facetValues` relation