Explorar o código

Merge branch 'master' into minor

Michael Bromley %!s(int64=3) %!d(string=hai) anos
pai
achega
c3411463b4

+ 2 - 0
packages/core/e2e/fixtures/test-plugins/with-config.ts

@@ -6,10 +6,12 @@ import { ConfigModule, VendurePlugin } from '@vendure/core';
     configuration: config => {
         // tslint:disable-next-line:no-non-null-assertion
         config.defaultLanguageCode = LanguageCode.zh;
+        TestPluginWithConfig.configSpy();
         return config;
     },
 })
 export class TestPluginWithConfig {
+    static configSpy = jest.fn();
     static setup() {
         return TestPluginWithConfig;
     }

+ 1 - 1
packages/core/e2e/list-query-builder.e2e-spec.ts

@@ -964,7 +964,7 @@ describe('ListQueryBuilder', () => {
     });
 
     // https://github.com/vendure-ecommerce/vendure/issues/1611
-    xdescribe('translations handling', () => {
+    describe('translations handling', () => {
         const allTranslations = [
             [
                 { languageCode: LanguageCode.en, name: 'apple' },

+ 22 - 0
packages/core/e2e/plugin.e2e-spec.ts

@@ -48,6 +48,7 @@ describe('Plugins', () => {
         const configService = server.app.get(ConfigService);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
+        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
     });
 
     it('extends the admin API', async () => {
@@ -153,3 +154,24 @@ describe('Plugins', () => {
         });
     });
 });
+
+describe('Multiple bootstraps in same process', () => {
+    const activeConfig = testConfig();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...activeConfig,
+        plugins: [TestPluginWithConfig.setup()],
+    });
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    it('plugin `configure` function called only once', async () => {
+        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
+    });
+});

+ 137 - 1
packages/core/e2e/product.e2e-spec.ts

@@ -11,6 +11,7 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 import { PRODUCT_VARIANT_FRAGMENT, PRODUCT_WITH_OPTIONS_FRAGMENT } from './graphql/fragments';
 import {
     AddOptionGroupToProduct,
+    ChannelFragment,
     CreateProduct,
     CreateProductVariants,
     DeleteProduct,
@@ -27,10 +28,13 @@ import {
     GetProductWithVariants,
     LanguageCode,
     ProductVariantFragment,
+    ProductVariantListOptions,
     ProductWithOptionsFragment,
     ProductWithVariants,
     RemoveOptionGroupFromProduct,
     SortOrder,
+    UpdateChannel,
+    UpdateGlobalSettings,
     UpdateProduct,
     UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
@@ -44,6 +48,8 @@ import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_SIMPLE,
     GET_PRODUCT_WITH_VARIANTS,
+    UPDATE_CHANNEL,
+    UPDATE_GLOBAL_SETTINGS,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
@@ -52,12 +58,16 @@ import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 // tslint:disable:no-non-null-assertion
 
 describe('Product resolver', () => {
-    const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig(),
+    });
 
     const removeOptionGuard: ErrorResultGuard<ProductWithOptionsFragment> = createErrorResultGuard(
         input => !!input.optionGroups,
     );
 
+    const updateChannelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard(input => !!input.id);
+
     beforeAll(async () => {
         await server.init({
             initialData,
@@ -1724,6 +1734,132 @@ describe('Product resolver', () => {
 
                 expect(product?.variants.length).toBe(1);
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/1631
+            describe('changing the Channel default language', () => {
+                let productId: string;
+
+                function getProductWithVariantsInLanguage(
+                    id: string,
+                    languageCode: LanguageCode,
+                    variantListOptions?: ProductVariantListOptions,
+                ) {
+                    return adminClient.query<
+                        GetProductWithVariantList.Query,
+                        GetProductWithVariantList.Variables
+                    >(GET_PRODUCT_WITH_VARIANT_LIST, { id, variantListOptions }, { languageCode });
+                }
+
+                beforeAll(async () => {
+                    await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
+                        UPDATE_GLOBAL_SETTINGS,
+                        {
+                            input: {
+                                availableLanguages: [LanguageCode.en, LanguageCode.de],
+                            },
+                        },
+                    );
+                    const { createProduct } = await adminClient.query<
+                        CreateProduct.Mutation,
+                        CreateProduct.Variables
+                    >(CREATE_PRODUCT, {
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: LanguageCode.en,
+                                    name: 'Bottle',
+                                    slug: 'bottle',
+                                    description: 'A container for liquids',
+                                },
+                            ],
+                        },
+                    });
+
+                    productId = createProduct.id;
+                    await adminClient.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
+                        CREATE_PRODUCT_VARIANTS,
+                        {
+                            input: [
+                                {
+                                    productId,
+                                    sku: 'BOTTLE111',
+                                    optionIds: [],
+                                    translations: [{ languageCode: LanguageCode.en, name: 'Bottle' }],
+                                },
+                            ],
+                        },
+                    );
+                });
+
+                afterAll(async () => {
+                    // Restore the default language to English for the subsequent tests
+                    await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
+                        input: {
+                            id: 'T_1',
+                            defaultLanguageCode: LanguageCode.en,
+                        },
+                    });
+                });
+
+                it('returns all variants', async () => {
+                    const { product: product1 } = await adminClient.query<
+                        GetProductWithVariants.Query,
+                        GetProductWithVariants.Variables
+                    >(
+                        GET_PRODUCT_WITH_VARIANTS,
+                        {
+                            id: productId,
+                        },
+                        { languageCode: LanguageCode.en },
+                    );
+                    expect(product1?.variants.length).toBe(1);
+
+                    // Change the default language of the channel to "de"
+                    const { updateChannel } = await adminClient.query<
+                        UpdateChannel.Mutation,
+                        UpdateChannel.Variables
+                    >(UPDATE_CHANNEL, {
+                        input: {
+                            id: 'T_1',
+                            defaultLanguageCode: LanguageCode.de,
+                        },
+                    });
+                    updateChannelGuard.assertSuccess(updateChannel);
+                    expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.de);
+
+                    // Fetch the product in en, it should still return 1 variant
+                    const { product: product2 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.en,
+                    );
+                    expect(product2?.variantList.items.length).toBe(1);
+
+                    // Fetch the product in de, it should still return 1 variant
+                    const { product: product3 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.de,
+                    );
+                    expect(product3?.variantList.items.length).toBe(1);
+                });
+
+                it('returns all variants when sorting on variant name', async () => {
+                    // Fetch the product in en, it should still return 1 variant
+                    const { product: product1 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.en,
+                        { sort: { name: SortOrder.ASC } },
+                    );
+                    expect(product1?.variantList.items.length).toBe(1);
+
+                    // Fetch the product in de, it should still return 1 variant
+                    const { product: product2 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.de,
+                        { sort: { name: SortOrder.ASC } },
+                    );
+                    expect(product2?.variantList.items.length).toBe(1);
+                });
+            });
         });
     });
 

+ 12 - 0
packages/core/src/bootstrap.ts

@@ -154,6 +154,13 @@ export async function preBootstrapConfig(
     return config;
 }
 
+// This is here to prevent a plugin's `configure` function from executing more than once in the
+// same process. Running more than once can occur e.g. if a script runs the `runMigrations()` function
+// followed by the `bootstrap()` function, and will lead to very hard-to-debug errors caused by
+// mutating the config object twice, e.g. plugins pushing custom fields again resulting in duplicate
+// custom field definitions.
+const pluginConfigDidRun = new WeakSet<RuntimeVendureConfig['plugins'][number]>();
+
 /**
  * Initialize any configured plugins.
  */
@@ -161,7 +168,12 @@ async function runPluginConfigurations(config: RuntimeVendureConfig): Promise<Ru
     for (const plugin of config.plugins) {
         const configFn = getConfigurationFunction(plugin);
         if (typeof configFn === 'function') {
+            const configAlreadyRan = pluginConfigDidRun.has(plugin);
+            if (configAlreadyRan) {
+                continue;
+            }
             config = await configFn(config);
+            pluginConfigDidRun.add(plugin);
         }
     }
     return config;

+ 11 - 2
packages/core/src/service/helpers/list-query-builder/connection-utils.ts

@@ -2,7 +2,8 @@ import { Type } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
-import { CalculatedColumnDefinition, CALCULATED_PROPERTIES } from '../../../common/calculated-decorator';
+import { Translation } from '../../../common/index';
+import { VendureEntity } from '../../../entity/index';
 
 /**
  * @description
@@ -16,9 +17,17 @@ export function getColumnMetadata<T>(connection: Connection, entity: Type<T>) {
 
     const translationRelation = relations.find(r => r.propertyName === 'translations');
     if (translationRelation) {
+        const commonFields: Array<keyof (Translation<T> & VendureEntity)> = [
+            'id',
+            'createdAt',
+            'updatedAt',
+            'languageCode',
+        ];
         const translationMetadata = connection.getMetadata(translationRelation.type);
         translationColumns = translationColumns.concat(
-            translationMetadata.columns.filter(c => !c.relationMetadata),
+            translationMetadata.columns.filter(
+                c => !c.relationMetadata && !commonFields.includes(c.propertyName as any),
+            ),
         );
     }
     const alias = metadata.name.toLowerCase();

+ 25 - 9
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -17,7 +17,7 @@ import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 import { ApiType } from '../../../api/common/get-api-type';
 import { RequestContext } from '../../../api/common/request-context';
 import { UserInputError } from '../../../common/error/errors';
-import { FilterParameter, ListQueryOptions, SortParameter } from '../../../common/types/common-types';
+import { ListQueryOptions, NullOptionals, SortParameter } from '../../../common/types/common-types';
 import { ConfigService } from '../../../config/config.service';
 import { CustomFields } from '../../../config/index';
 import { Logger } from '../../../config/logger/vendure-logger';
@@ -212,8 +212,6 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         // tslint:disable-next-line:no-non-null-assertion
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
 
-        this.applyTranslationConditions(qb, entity, extendedOptions.ctx, extendedOptions.entityAlias);
-
         // join the tables required by calculated columns
         this.joinCalculatedColumnRelations(qb, entity, options);
 
@@ -222,10 +220,18 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
             this.normalizeCustomPropertyMap(customPropertyMap, options, qb);
         }
         const customFieldsForType = this.configService.customFields[entity.name as keyof CustomFields];
+        const sortParams = Object.assign({}, options.sort, extendedOptions.orderBy);
+        this.applyTranslationConditions(
+            qb,
+            entity,
+            sortParams,
+            extendedOptions.ctx,
+            extendedOptions.entityAlias,
+        );
         const sort = parseSortParams(
             rawConnection,
             entity,
-            Object.assign({}, options.sort, extendedOptions.orderBy),
+            sortParams,
             customPropertyMap,
             entityAlias,
             customFieldsForType,
@@ -513,13 +519,15 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
     }
 
     /**
-     * If this entity is Translatable, then we need to apply appropriate WHERE clauses to limit
-     * the joined translation relations. This method applies a simple "WHERE" on the languageCode
-     * in the case of the default language, otherwise we use a more complex.
+     * @description
+     * If this entity is Translatable, and we are sorting on one of the translatable fields,
+     * then we need to apply appropriate WHERE clauses to limit
+     * the joined translation relations.
      */
     private applyTranslationConditions<T extends VendureEntity>(
         qb: SelectQueryBuilder<any>,
         entity: Type<T>,
+        sortParams: NullOptionals<SortParameter<T>> & FindOneOptions<T>['order'],
         ctx?: RequestContext,
         entityAlias?: string,
     ) {
@@ -532,7 +540,15 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         } = getColumnMetadata(this.connection.rawConnection, entity);
         const alias = entityAlias ?? defaultAlias;
 
-        if (translationColumns.length) {
+        const sortKeys = Object.keys(sortParams);
+        let sortingOnTranslatableKey = false;
+        for (const translationColumn of translationColumns) {
+            if (sortKeys.includes(translationColumn.propertyName)) {
+                sortingOnTranslatableKey = true;
+            }
+        }
+
+        if (translationColumns.length && sortingOnTranslatableKey) {
             const translationsAlias = qb.connection.namingStrategy.eagerJoinRelationAlias(
                 alias,
                 'translations',
@@ -584,7 +600,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
                     }
                     qb.setParameters({
                         nonDefaultLanguageCode: languageCode,
-                        defaultLanguageCode: this.configService.defaultLanguageCode,
+                        defaultLanguageCode,
                     });
                 }),
             );

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

@@ -20,6 +20,17 @@ import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
 
 import { prorate } from './prorate';
 
+/**
+ * @description
+ * This helper is used when making changes to an Order, to apply all applicable price adjustments to that Order,
+ * including:
+ *
+ * - Promotions
+ * - Taxes
+ * - Shipping
+ *
+ * @docsCategory service-helpers
+ */
 @Injectable()
 export class OrderCalculator {
     constructor(
@@ -32,6 +43,7 @@ export class OrderCalculator {
     ) {}
 
     /**
+     * @description
      * Applies taxes and promotions to an Order. Mutates the order object.
      * Returns an array of any OrderItems which had new adjustments
      * applied, either tax or promotions.
@@ -95,6 +107,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * Applies the correct TaxRate to each OrderItem in the order.
      */
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
@@ -106,6 +119,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * Applies the correct TaxRate to an OrderLine
      */
     private async applyTaxesToOrderLine(
@@ -129,6 +143,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * Returns a memoized function for performing an efficient
      * lookup of the correct TaxRate for a given TaxCategory.
      */
@@ -150,6 +165,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * Applies any eligible promotions to each OrderItem in the order. Returns an array of
      * any OrderItems which had their Adjustments modified.
      */
@@ -168,6 +184,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * Applies promotions to OrderItems. This is a quite complex function, due to the inherent complexity
      * of applying the promotions, and also due to added complexity in the name of performance
      * optimization. Therefore it is heavily annotated so that the purpose of each step is clear.
@@ -253,6 +270,7 @@ export class OrderCalculator {
     }
 
     /**
+     * @description
      * An OrderLine may have promotion adjustments from Promotions which are no longer applicable.
      * For example, a coupon code might have caused a discount to be applied, and now that code has
      * been removed from the order. The adjustment will still be there on each OrderItem it was applied

+ 10 - 0
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -61,6 +61,8 @@ import { translateDeep } from '../utils/translate-entity';
  * So this helper was mainly extracted to isolate the huge `modifyOrder` method since the
  * OrderService was just growing too large. Future refactoring could improve the organization
  * of these Order-related methods into a more clearly-delineated set of classes.
+ *
+ * @docsCategory service-helpers
  */
 @Injectable()
 export class OrderModifier {
@@ -80,6 +82,7 @@ export class OrderModifier {
     ) {}
 
     /**
+     * @description
      * Ensure that the ProductVariant has sufficient saleable stock to add the given
      * quantity to an Order.
      */
@@ -97,6 +100,11 @@ export class OrderModifier {
         return correctedQuantity;
     }
 
+    /**
+     * @description
+     * Given a ProductVariant ID and optional custom fields, this method will return an existing OrderLine that
+     * matches, or `undefined` if no match is found.
+     */
     async getExistingOrderLine(
         ctx: RequestContext,
         order: Order,
@@ -114,6 +122,7 @@ export class OrderModifier {
     }
 
     /**
+     * @description
      * Returns the OrderLine to which a new OrderItem belongs, creating a new OrderLine
      * if no existing line is found.
      */
@@ -162,6 +171,7 @@ export class OrderModifier {
     }
 
     /**
+     * @description
      * Updates the quantity of an OrderLine, taking into account the available saleable stock level.
      * Returns the actual quantity that the OrderLine was updated to (which may be less than the
      * `quantity` argument if insufficient stock was available.

+ 26 - 2
packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts

@@ -13,7 +13,30 @@ import { ZoneService } from '../../services/zone.service';
 /**
  * @description
  * This helper is used to apply the correct price to a ProductVariant based on the current context
- * including active Channel, any current Order, etc.
+ * including active Channel, any current Order, etc. If you use the {@link TransactionalConnection} to
+ * directly query ProductVariants, you will find that the `price` and `priceWithTax` properties will
+ * always be `0` until you use the `applyChannelPriceAndTax()` method:
+ *
+ * @example
+ * ```TypeScript
+ * export class MyCustomService {
+ *   constructor(private connection: TransactionalConnection,
+ *               private productPriceApplicator: ProductPriceApplicator) {}
+ *
+ *   getVariant(ctx: RequestContext, id: ID) {
+ *     const productVariant = await this.connection
+ *       .getRepository(ctx, ProductVariant)
+ *       .findOne(id, { relations: ['taxCategory'] });
+ *
+ *     await this.productPriceApplicator
+ *       .applyChannelPriceAndTax(productVariant, ctx);
+ *
+ *     return productVariant;
+ *   }
+ * }
+ * ```
+ *
+ * @docsCategory service-helpers
  */
 @Injectable()
 export class ProductPriceApplicator {
@@ -26,7 +49,8 @@ export class ProductPriceApplicator {
 
     /**
      * @description
-     * Populates the `price` field with the price for the specified channel.
+     * Populates the `price` field with the price for the specified channel. Make sure that
+     * the ProductVariant being passed in has its `taxCategory` relation joined.
      */
     async applyChannelPriceAndTax(
         variant: ProductVariant,

+ 17 - 1
packages/core/src/service/helpers/slug-validator/slug-validator.ts

@@ -8,6 +8,10 @@ import { TransactionalConnection } from '../../../connection/transactional-conne
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 
+/**
+ * @docsCategory service-helpers
+ * @docsPage SlugValidator
+ */
 export type InputWithSlug = {
     id?: ID | null;
     translations?: Array<{
@@ -17,6 +21,10 @@ export type InputWithSlug = {
     }> | null;
 };
 
+/**
+ * @docsCategory service-helpers
+ * @docsPage SlugValidator
+ */
 export type TranslationEntity = VendureEntity & {
     id: ID;
     languageCode: LanguageCode;
@@ -24,6 +32,14 @@ export type TranslationEntity = VendureEntity & {
     base: any;
 };
 
+/**
+ * @description
+ * Used to validate slugs to ensure they are URL-safe and unique. Designed to be used with translatable
+ * entities such as {@link Product} and {@link Collection}.
+ *
+ * @docsCategory service-helpers
+ * @docsWeight 0
+ */
 @Injectable()
 export class SlugValidator {
     constructor(private connection: TransactionalConnection) {}
@@ -53,7 +69,7 @@ export class SlugValidator {
                             .where(`translation.slug = :slug`, { slug: t.slug })
                             .andWhere(`translation.languageCode = :languageCode`, {
                                 languageCode: t.languageCode,
-                            })
+                            });
                         if (input.id) {
                             qb.andWhere(`translation.base != :id`, { id: input.id });
                         }

+ 28 - 1
packages/core/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -25,13 +25,39 @@ export interface UpdateTranslatableOptions<T extends Translatable> extends Creat
 }
 
 /**
- * A helper which contains methods for creating and updating entities which implement the Translatable interface.
+ * @description
+ * A helper which contains methods for creating and updating entities which implement the {@link Translatable} interface.
+ *
+ * @example
+ * ```TypeScript
+ * export class MyService {
+ *   constructor(private translatableSaver: TranslatableSaver) {}
+ *
+ *   async create(ctx: RequestContext, input: CreateFacetInput): Promise<Translated<Facet>> {
+ *     const facet = await this.translatableSaver.create({
+ *       ctx,
+ *       input,
+ *       entityType: Facet,
+ *       translationType: FacetTranslation,
+ *       beforeSave: async f => {
+ *           f.code = await this.ensureUniqueCode(ctx, f.code);
+ *       },
+ *     });
+ *     return facet;
+ *   }
+ *
+ *   // ...
+ * }
+ * ```
+ *
+ * @docsCategory service-helpers
  */
 @Injectable()
 export class TranslatableSaver {
     constructor(private connection: TransactionalConnection) {}
 
     /**
+     * @description
      * Create a translatable entity, including creating any translation entities according
      * to the `translations` array.
      */
@@ -59,6 +85,7 @@ export class TranslatableSaver {
     }
 
     /**
+     * @description
      * Update a translatable entity. Performs a diff of the `translations` array in order to
      * perform the correct operation on the translations.
      */

+ 11 - 8
packages/core/src/service/services/product.service.ts

@@ -172,28 +172,31 @@ export class ProductService {
     ): Promise<Translated<Product> | undefined> {
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
         FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
-            relations: (relations && false) || this.relations,
+            relations: relations ? [...new Set(this.relations.concat(relations))] : this.relations,
         });
         // tslint:disable-next-line:no-non-null-assertion
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         const translationQb = this.connection
             .getRepository(ctx, ProductTranslation)
-            .createQueryBuilder('product_translation')
-            .select('product_translation.baseId')
-            .andWhere('product_translation.slug = :slug', { slug });
+            .createQueryBuilder('_product_translation')
+            .select('_product_translation.baseId')
+            .andWhere('_product_translation.slug = :slug', { slug });
 
-        return qb
-            .leftJoin('product.channels', 'channel')
+        const translationsAlias = relations?.includes('translations' as any)
+            ? 'product__translations'
+            : 'product_translations';
+        qb.leftJoin('product.channels', 'channel')
             .andWhere('product.id IN (' + translationQb.getQuery() + ')')
             .setParameters(translationQb.getParameters())
             .andWhere('product.deletedAt IS NULL')
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .addSelect(
                 // tslint:disable-next-line:max-line-length
-                `CASE product_translations.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
+                `CASE ${translationsAlias}.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
                 'sort_order',
             )
-            .orderBy('sort_order', 'DESC')
+            .orderBy('sort_order', 'DESC');
+        return qb
             .getOne()
             .then(product =>
                 product