Browse Source

fix(core): Fix product query by slug

Fixes #800
Artem Danilov 4 years ago
parent
commit
2ace0eb0a0

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

@@ -335,6 +335,17 @@ describe('Product resolver', () => {
 
             expect(result.product).toBeNull();
         });
+
+        it('returns null when slug not found', async () => {
+            const result = await adminClient.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                slug: 'bad_slug',
+            });
+
+            expect(result.product).toBeNull();
+        });
     });
 
     describe('productVariants list query', () => {
@@ -577,6 +588,7 @@ describe('Product resolver', () => {
     });
 
     describe('product mutation', () => {
+        let newTranslatedProduct: ProductWithVariants.Fragment;
         let newProduct: ProductWithVariants.Fragment;
         let newProductWithAssets: ProductWithVariants.Fragment;
 
@@ -607,7 +619,133 @@ describe('Product resolver', () => {
                 'A baked potato',
                 'Eine baked Erdapfel',
             ]);
-            newProduct = result.createProduct;
+            newTranslatedProduct = result.createProduct;
+        });
+
+        describe('product query with translations', () => {
+            it('en slug without translation arg', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(en_translation.slug);
+            });
+
+            it('de slug without translation arg', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const de_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.de;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(en_translation.slug);
+            });
+
+            it('en slug with translation en', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug }, { languageCode: LanguageCode.en });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(en_translation.slug);
+            });
+
+            it('de slug with translation en', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const de_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.de;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.en });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(en_translation.slug);
+            });
+
+            it('en slug with translation de', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const de_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.de;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: en_translation.slug }, { languageCode: LanguageCode.de });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(de_translation.slug);
+            });
+
+            it('de slug with translation de', async () => {
+                const de_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.de;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.de });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(de_translation.slug);
+            });
+
+            it('de slug with translation ru', async () => {
+                const en_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.en;
+                })[0];
+                const de_translation = newTranslatedProduct.translations.filter(t => {
+                    return t.languageCode === LanguageCode.de;
+                })[0];
+                const { product } = await adminClient.query<
+                    GetProductSimple.Query,
+                    GetProductSimple.Variables
+                >(GET_PRODUCT_SIMPLE, { slug: de_translation.slug }, { languageCode: LanguageCode.ru });
+
+                if (!product) {
+                    fail('Product not found');
+                    return;
+                }
+                expect(product.slug).toBe(en_translation.slug);
+            });
         });
 
         it('createProduct creates a new Product with assets', async () => {
@@ -1438,6 +1576,20 @@ describe('Product resolver', () => {
             );
             expect(result.createProduct.slug).toBe(productToDelete.slug);
         });
+
+        // https://github.com/vendure-ecommerce/vendure/issues/800
+        it('product can be fetched by slug of a deleted product', async () => {
+            const { product } = await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                GET_PRODUCT_SIMPLE,
+                { slug: productToDelete.slug },
+            );
+
+            if (!product) {
+                fail('Product not found');
+                return;
+            }
+            expect(product.slug).toBe(productToDelete.slug);
+        });
     });
 });
 

+ 33 - 12
packages/core/src/service/services/product.service.ts

@@ -136,18 +136,39 @@ export class ProductService {
     }
 
     async findOneBySlug(ctx: RequestContext, slug: string): Promise<Translated<Product> | undefined> {
-        const translations = await this.connection.getRepository(ctx, ProductTranslation).find({
-            relations: ['base'],
-            where: { slug },
-        });
-        if (!translations?.length) {
-            return;
-        }
-        const bestMatch =
-            translations.find(t => t.languageCode === ctx.languageCode) ??
-            translations.find(t => t.languageCode === ctx.channel.defaultLanguageCode) ??
-            translations[0];
-        return this.findOne(ctx, bestMatch.base.id);
+        const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
+        FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, { 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 });
+
+        return 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(
+                'CASE product_translations.languageCode WHEN \'' +
+                    ctx.languageCode +
+                    '\' THEN 2 WHEN \'' +
+                    ctx.channel.defaultLanguageCode +
+                    '\' THEN 1 ELSE 0 END',
+                'sort_order',
+            )
+            .orderBy('sort_order', 'DESC')
+            .limit(1)
+            .getMany()
+            .then(products => products[0])
+            .then(product =>
+                product
+                    ? translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']])
+                    : undefined,
+            );
     }
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {