Browse Source

feat(core): Allow product to be queried by slug

Closes #108
Michael Bromley 6 years ago
parent
commit
a2d847dae0

+ 43 - 42
admin-ui/src/app/common/generated-types.ts

@@ -1416,16 +1416,16 @@ export type Mutation = {
   createAssets: Array<Asset>,
   login: LoginResult,
   logout: Scalars['Boolean'],
-  /** Create a new Channel */
-  createChannel: Channel,
-  /** Update an existing Channel */
-  updateChannel: Channel,
   /** Create a new Collection */
   createCollection: Collection,
   /** Update an existing Collection */
   updateCollection: Collection,
   /** Move a Collection to a different parent or index */
   moveCollection: Collection,
+  /** Create a new Channel */
+  createChannel: Channel,
+  /** Update an existing Channel */
+  updateChannel: Channel,
   /** Create a new Country */
   createCountry: Country,
   /** Update an existing Country */
@@ -1498,14 +1498,14 @@ export type Mutation = {
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
-  /** Create a new TaxRate */
-  createTaxRate: TaxRate,
-  /** Update an existing TaxRate */
-  updateTaxRate: TaxRate,
   /** Create a new TaxCategory */
   createTaxCategory: TaxCategory,
   /** Update an existing TaxCategory */
   updateTaxCategory: TaxCategory,
+  /** Create a new TaxRate */
+  createTaxRate: TaxRate,
+  /** Update an existing TaxRate */
+  updateTaxRate: TaxRate,
   /** Create a new Zone */
   createZone: Zone,
   /** Update an existing Zone */
@@ -1552,16 +1552,6 @@ export type MutationLoginArgs = {
 };
 
 
-export type MutationCreateChannelArgs = {
-  input: CreateChannelInput
-};
-
-
-export type MutationUpdateChannelArgs = {
-  input: UpdateChannelInput
-};
-
-
 export type MutationCreateCollectionArgs = {
   input: CreateCollectionInput
 };
@@ -1577,6 +1567,16 @@ export type MutationMoveCollectionArgs = {
 };
 
 
+export type MutationCreateChannelArgs = {
+  input: CreateChannelInput
+};
+
+
+export type MutationUpdateChannelArgs = {
+  input: UpdateChannelInput
+};
+
+
 export type MutationCreateCountryArgs = {
   input: CreateCountryInput
 };
@@ -1778,23 +1778,23 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
 };
 
 
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
 };
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
 };
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
 };
 
 
@@ -2298,12 +2298,12 @@ export type Query = {
   assets: AssetList,
   asset?: Maybe<Asset>,
   me?: Maybe<CurrentUser>,
-  channels: Array<Channel>,
-  channel?: Maybe<Channel>,
-  activeChannel: Channel,
   collections: CollectionList,
   collection?: Maybe<Collection>,
   collectionFilters: Array<ConfigurableOperation>,
+  channels: Array<Channel>,
+  channel?: Maybe<Channel>,
+  activeChannel: Channel,
   countries: CountryList,
   country?: Maybe<Country>,
   customerGroups: Array<CustomerGroup>,
@@ -2331,10 +2331,10 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
-  taxRates: TaxRateList,
-  taxRate?: Maybe<TaxRate>,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
+  taxRates: TaxRateList,
+  taxRate?: Maybe<TaxRate>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
   temp__?: Maybe<Scalars['Boolean']>,
@@ -2364,11 +2364,6 @@ export type QueryAssetArgs = {
 };
 
 
-export type QueryChannelArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryCollectionsArgs = {
   languageCode?: Maybe<LanguageCode>,
   options?: Maybe<CollectionListOptions>
@@ -2381,6 +2376,11 @@ export type QueryCollectionArgs = {
 };
 
 
+export type QueryChannelArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryCountriesArgs = {
   options?: Maybe<CountryListOptions>
 };
@@ -2462,7 +2462,8 @@ export type QueryProductsArgs = {
 
 
 export type QueryProductArgs = {
-  id: Scalars['ID'],
+  id?: Maybe<Scalars['ID']>,
+  slug?: Maybe<Scalars['String']>,
   languageCode?: Maybe<LanguageCode>
 };
 
@@ -2497,17 +2498,17 @@ export type QueryShippingMethodArgs = {
 };
 
 
-export type QueryTaxRatesArgs = {
-  options?: Maybe<TaxRateListOptions>
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
 };
 
 
-export type QueryTaxRateArgs = {
-  id: Scalars['ID']
+export type QueryTaxRatesArgs = {
+  options?: Maybe<TaxRateListOptions>
 };
 
 
-export type QueryTaxCategoryArgs = {
+export type QueryTaxRateArgs = {
   id: Scalars['ID']
 };
 

+ 2 - 1
packages/common/src/generated-shop-types.ts

@@ -1645,7 +1645,8 @@ export type QueryOrderByCodeArgs = {
 };
 
 export type QueryProductArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
     languageCode?: Maybe<LanguageCode>;
 };
 

+ 43 - 42
packages/common/src/generated-types.ts

@@ -1415,16 +1415,16 @@ export type Mutation = {
   createAssets: Array<Asset>,
   login: LoginResult,
   logout: Scalars['Boolean'],
-  /** Create a new Channel */
-  createChannel: Channel,
-  /** Update an existing Channel */
-  updateChannel: Channel,
   /** Create a new Collection */
   createCollection: Collection,
   /** Update an existing Collection */
   updateCollection: Collection,
   /** Move a Collection to a different parent or index */
   moveCollection: Collection,
+  /** Create a new Channel */
+  createChannel: Channel,
+  /** Update an existing Channel */
+  updateChannel: Channel,
   /** Create a new Country */
   createCountry: Country,
   /** Update an existing Country */
@@ -1497,14 +1497,14 @@ export type Mutation = {
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
-  /** Create a new TaxRate */
-  createTaxRate: TaxRate,
-  /** Update an existing TaxRate */
-  updateTaxRate: TaxRate,
   /** Create a new TaxCategory */
   createTaxCategory: TaxCategory,
   /** Update an existing TaxCategory */
   updateTaxCategory: TaxCategory,
+  /** Create a new TaxRate */
+  createTaxRate: TaxRate,
+  /** Update an existing TaxRate */
+  updateTaxRate: TaxRate,
   /** Create a new Zone */
   createZone: Zone,
   /** Update an existing Zone */
@@ -1546,16 +1546,6 @@ export type MutationLoginArgs = {
 };
 
 
-export type MutationCreateChannelArgs = {
-  input: CreateChannelInput
-};
-
-
-export type MutationUpdateChannelArgs = {
-  input: UpdateChannelInput
-};
-
-
 export type MutationCreateCollectionArgs = {
   input: CreateCollectionInput
 };
@@ -1571,6 +1561,16 @@ export type MutationMoveCollectionArgs = {
 };
 
 
+export type MutationCreateChannelArgs = {
+  input: CreateChannelInput
+};
+
+
+export type MutationUpdateChannelArgs = {
+  input: UpdateChannelInput
+};
+
+
 export type MutationCreateCountryArgs = {
   input: CreateCountryInput
 };
@@ -1772,23 +1772,23 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
 };
 
 
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
 };
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
 };
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
 };
 
 
@@ -2277,12 +2277,12 @@ export type Query = {
   assets: AssetList,
   asset?: Maybe<Asset>,
   me?: Maybe<CurrentUser>,
-  channels: Array<Channel>,
-  channel?: Maybe<Channel>,
-  activeChannel: Channel,
   collections: CollectionList,
   collection?: Maybe<Collection>,
   collectionFilters: Array<ConfigurableOperation>,
+  channels: Array<Channel>,
+  channel?: Maybe<Channel>,
+  activeChannel: Channel,
   countries: CountryList,
   country?: Maybe<Country>,
   customerGroups: Array<CustomerGroup>,
@@ -2310,10 +2310,10 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
-  taxRates: TaxRateList,
-  taxRate?: Maybe<TaxRate>,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
+  taxRates: TaxRateList,
+  taxRate?: Maybe<TaxRate>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
   temp__?: Maybe<Scalars['Boolean']>,
@@ -2340,11 +2340,6 @@ export type QueryAssetArgs = {
 };
 
 
-export type QueryChannelArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryCollectionsArgs = {
   languageCode?: Maybe<LanguageCode>,
   options?: Maybe<CollectionListOptions>
@@ -2357,6 +2352,11 @@ export type QueryCollectionArgs = {
 };
 
 
+export type QueryChannelArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryCountriesArgs = {
   options?: Maybe<CountryListOptions>
 };
@@ -2438,7 +2438,8 @@ export type QueryProductsArgs = {
 
 
 export type QueryProductArgs = {
-  id: Scalars['ID'],
+  id?: Maybe<Scalars['ID']>,
+  slug?: Maybe<Scalars['String']>,
   languageCode?: Maybe<LanguageCode>
 };
 
@@ -2473,17 +2474,17 @@ export type QueryShippingMethodArgs = {
 };
 
 
-export type QueryTaxRatesArgs = {
-  options?: Maybe<TaxRateListOptions>
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
 };
 
 
-export type QueryTaxRateArgs = {
-  id: Scalars['ID']
+export type QueryTaxRatesArgs = {
+  options?: Maybe<TaxRateListOptions>
 };
 
 
-export type QueryTaxCategoryArgs = {
+export type QueryTaxRateArgs = {
   id: Scalars['ID']
 };
 

+ 71 - 53
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1412,16 +1412,16 @@ export type Mutation = {
     createAssets: Array<Asset>;
     login: LoginResult;
     logout: Scalars['Boolean'];
-    /** Create a new Channel */
-    createChannel: Channel;
-    /** Update an existing Channel */
-    updateChannel: Channel;
     /** Create a new Collection */
     createCollection: Collection;
     /** Update an existing Collection */
     updateCollection: Collection;
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
+    /** Create a new Channel */
+    createChannel: Channel;
+    /** Update an existing Channel */
+    updateChannel: Channel;
     /** Create a new Country */
     createCountry: Country;
     /** Update an existing Country */
@@ -1494,14 +1494,14 @@ export type Mutation = {
     createShippingMethod: ShippingMethod;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
-    /** Create a new TaxRate */
-    createTaxRate: TaxRate;
-    /** Update an existing TaxRate */
-    updateTaxRate: TaxRate;
     /** Create a new TaxCategory */
     createTaxCategory: TaxCategory;
     /** Update an existing TaxCategory */
     updateTaxCategory: TaxCategory;
+    /** Create a new TaxRate */
+    createTaxRate: TaxRate;
+    /** Update an existing TaxRate */
+    updateTaxRate: TaxRate;
     /** Create a new Zone */
     createZone: Zone;
     /** Update an existing Zone */
@@ -1537,14 +1537,6 @@ export type MutationLoginArgs = {
     rememberMe?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationCreateChannelArgs = {
-    input: CreateChannelInput;
-};
-
-export type MutationUpdateChannelArgs = {
-    input: UpdateChannelInput;
-};
-
 export type MutationCreateCollectionArgs = {
     input: CreateCollectionInput;
 };
@@ -1557,6 +1549,14 @@ export type MutationMoveCollectionArgs = {
     input: MoveCollectionInput;
 };
 
+export type MutationCreateChannelArgs = {
+    input: CreateChannelInput;
+};
+
+export type MutationUpdateChannelArgs = {
+    input: UpdateChannelInput;
+};
+
 export type MutationCreateCountryArgs = {
     input: CreateCountryInput;
 };
@@ -1720,14 +1720,6 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
-export type MutationCreateTaxRateArgs = {
-    input: CreateTaxRateInput;
-};
-
-export type MutationUpdateTaxRateArgs = {
-    input: UpdateTaxRateInput;
-};
-
 export type MutationCreateTaxCategoryArgs = {
     input: CreateTaxCategoryInput;
 };
@@ -1736,6 +1728,14 @@ export type MutationUpdateTaxCategoryArgs = {
     input: UpdateTaxCategoryInput;
 };
 
+export type MutationCreateTaxRateArgs = {
+    input: CreateTaxRateInput;
+};
+
+export type MutationUpdateTaxRateArgs = {
+    input: UpdateTaxRateInput;
+};
+
 export type MutationCreateZoneArgs = {
     input: CreateZoneInput;
 };
@@ -2216,12 +2216,12 @@ export type Query = {
     assets: AssetList;
     asset?: Maybe<Asset>;
     me?: Maybe<CurrentUser>;
-    channels: Array<Channel>;
-    channel?: Maybe<Channel>;
-    activeChannel: Channel;
     collections: CollectionList;
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperation>;
+    channels: Array<Channel>;
+    channel?: Maybe<Channel>;
+    activeChannel: Channel;
     countries: CountryList;
     country?: Maybe<Country>;
     customerGroups: Array<CustomerGroup>;
@@ -2249,10 +2249,10 @@ export type Query = {
     shippingMethod?: Maybe<ShippingMethod>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
-    taxRates: TaxRateList;
-    taxRate?: Maybe<TaxRate>;
     taxCategories: Array<TaxCategory>;
     taxCategory?: Maybe<TaxCategory>;
+    taxRates: TaxRateList;
+    taxRate?: Maybe<TaxRate>;
     zones: Array<Zone>;
     zone?: Maybe<Zone>;
     temp__?: Maybe<Scalars['Boolean']>;
@@ -2274,10 +2274,6 @@ export type QueryAssetArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryChannelArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryCollectionsArgs = {
     languageCode?: Maybe<LanguageCode>;
     options?: Maybe<CollectionListOptions>;
@@ -2288,6 +2284,10 @@ export type QueryCollectionArgs = {
     languageCode?: Maybe<LanguageCode>;
 };
 
+export type QueryChannelArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryCountriesArgs = {
     options?: Maybe<CountryListOptions>;
 };
@@ -2354,7 +2354,8 @@ export type QueryProductsArgs = {
 };
 
 export type QueryProductArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
     languageCode?: Maybe<LanguageCode>;
 };
 
@@ -2382,6 +2383,10 @@ export type QueryShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryTaxCategoryArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryTaxRatesArgs = {
     options?: Maybe<TaxRateListOptions>;
 };
@@ -2390,10 +2395,6 @@ export type QueryTaxRateArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryTaxCategoryArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryZoneArgs = {
     id: Scalars['ID'];
 };
@@ -3503,8 +3504,19 @@ export type CreateProductMutation = { __typename?: 'Mutation' } & {
     createProduct: { __typename?: 'Product' } & ProductWithVariantsFragment;
 };
 
+export type GetProductSimpleQueryVariables = {
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
+    languageCode?: Maybe<LanguageCode>;
+};
+
+export type GetProductSimpleQuery = { __typename?: 'Query' } & {
+    product: Maybe<{ __typename?: 'Product' } & Pick<Product, 'id' | 'slug'>>;
+};
+
 export type GetProductWithVariantsQueryVariables = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
     languageCode?: Maybe<LanguageCode>;
 };
 
@@ -3668,6 +3680,14 @@ export type GetFacetListQuery = { __typename?: 'Query' } & {
         };
 };
 
+export type DeleteProductMutationVariables = {
+    id: Scalars['ID'];
+};
+
+export type DeleteProductMutation = { __typename?: 'Mutation' } & {
+    deleteProduct: { __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'result'>;
+};
+
 export type GetProductsQueryVariables = {
     options?: Maybe<ProductListOptions>;
 };
@@ -3747,14 +3767,6 @@ export type GetOrderQuery = { __typename?: 'Query' } & {
     order: Maybe<{ __typename?: 'Order' } & OrderWithLinesFragment>;
 };
 
-export type DeleteProductMutationVariables = {
-    id: Scalars['ID'];
-};
-
-export type DeleteProductMutation = { __typename?: 'Mutation' } & {
-    deleteProduct: { __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'result'>;
-};
-
 export type AddOptionGroupToProductMutationVariables = {
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -4477,6 +4489,12 @@ export namespace CreateProduct {
     export type CreateProduct = ProductWithVariantsFragment;
 }
 
+export namespace GetProductSimple {
+    export type Variables = GetProductSimpleQueryVariables;
+    export type Query = GetProductSimpleQuery;
+    export type Product = NonNullable<GetProductSimpleQuery['product']>;
+}
+
 export namespace GetProductWithVariants {
     export type Variables = GetProductWithVariantsQueryVariables;
     export type Query = GetProductWithVariantsQuery;
@@ -4585,6 +4603,12 @@ export namespace GetFacetList {
     export type Items = FacetWithValuesFragment;
 }
 
+export namespace DeleteProduct {
+    export type Variables = DeleteProductMutationVariables;
+    export type Mutation = DeleteProductMutation;
+    export type DeleteProduct = DeleteProductMutation['deleteProduct'];
+}
+
 export namespace GetProducts {
     export type Variables = GetProductsQueryVariables;
     export type Query = GetProductsQuery;
@@ -4662,12 +4686,6 @@ export namespace GetOrder {
     export type Order = OrderWithLinesFragment;
 }
 
-export namespace DeleteProduct {
-    export type Variables = DeleteProductMutationVariables;
-    export type Mutation = DeleteProductMutation;
-    export type DeleteProduct = DeleteProductMutation['deleteProduct'];
-}
-
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;

+ 2 - 1
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1645,7 +1645,8 @@ export type QueryOrderByCodeArgs = {
 };
 
 export type QueryProductArgs = {
-    id: Scalars['ID'];
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
     languageCode?: Maybe<LanguageCode>;
 };
 

+ 11 - 2
packages/core/e2e/graphql/shared-definitions.ts

@@ -42,8 +42,8 @@ export const CREATE_PRODUCT = gql`
 `;
 
 export const GET_PRODUCT_WITH_VARIANTS = gql`
-    query GetProductWithVariants($id: ID!, $languageCode: LanguageCode) {
-        product(languageCode: $languageCode, id: $id) {
+    query GetProductWithVariants($id: ID, $slug: String, $languageCode: LanguageCode) {
+        product(languageCode: $languageCode, slug: $slug, id: $id) {
             ...ProductWithVariants
         }
     }
@@ -236,3 +236,12 @@ export const DELETE_PRODUCT = gql`
         }
     }
 `;
+
+export const GET_PRODUCT_SIMPLE = gql`
+    query GetProductSimple($id: ID, $slug: String, $languageCode: LanguageCode) {
+        product(languageCode: $languageCode, slug: $slug, id: $id) {
+            id
+            slug
+        }
+    }
+`;

+ 53 - 12
packages/core/e2e/product.e2e-spec.ts

@@ -12,6 +12,7 @@ import {
     GenerateProductVariants,
     GetAssetList,
     GetProductList,
+    GetProductSimple,
     GetProductWithVariants,
     LanguageCode,
     ProductWithVariants,
@@ -25,6 +26,7 @@ import {
     DELETE_PRODUCT,
     GET_ASSET_LIST,
     GET_PRODUCT_LIST,
+    GET_PRODUCT_SIMPLE,
     GET_PRODUCT_WITH_VARIANTS,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
@@ -117,11 +119,47 @@ describe('Product resolver', () => {
     });
 
     describe('product query', () => {
+        it('by id', async () => {
+            const { product } = await client.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                GET_PRODUCT_SIMPLE,
+                { id: 'T_2' },
+            );
+
+            if (!product) {
+                fail('Product not found');
+                return;
+            }
+            expect(product.id).toBe('T_2');
+        });
+
+        it('by slug', async () => {
+            const { product } = await client.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                GET_PRODUCT_SIMPLE,
+                { slug: 'curvy-monitor' },
+            );
+
+            if (!product) {
+                fail('Product not found');
+                return;
+            }
+            expect(product.slug).toBe('curvy-monitor');
+        });
+
+        it(
+            'throws if neither id nor slug provided',
+            assertThrowsWithMessage(async () => {
+                await client.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                    GET_PRODUCT_SIMPLE,
+                    {},
+                );
+            }, 'Either the product id or slug must be provided'),
+        );
+
         it('returns expected properties', async () => {
             const { product } = await client.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 languageCode: LanguageCode.en,
                 id: 'T_2',
             });
@@ -373,7 +411,7 @@ describe('Product resolver', () => {
             const productResult = await client.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 id: newProduct.id,
                 languageCode: LanguageCode.en,
             });
@@ -435,7 +473,7 @@ describe('Product resolver', () => {
             const result = await client.query<
                 AddOptionGroupToProduct.Mutation,
                 AddOptionGroupToProduct.Variables
-                >(ADD_OPTION_GROUP_TO_PRODUCT, {
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
                 optionGroupId: 'T_2',
                 productId: newProduct.id,
             });
@@ -477,7 +515,7 @@ describe('Product resolver', () => {
             const result = await client.query<
                 RemoveOptionGroupFromProduct.Mutation,
                 RemoveOptionGroupFromProduct.Variables
-                >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+            >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                 optionGroupId: 'T_1',
                 productId: 'T_1',
             });
@@ -492,7 +530,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: '1',
                         productId: '999',
                     }),
@@ -536,7 +574,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     GenerateProductVariants.Mutation,
                     GenerateProductVariants.Variables
-                    >(GENERATE_PRODUCT_VARIANTS, {
+                >(GENERATE_PRODUCT_VARIANTS, {
                     productId: newProduct.id,
                     defaultPrice: 123,
                     defaultSku: 'ABC',
@@ -552,7 +590,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -576,7 +614,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -599,7 +637,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -622,7 +660,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -673,7 +711,10 @@ describe('Product resolver', () => {
 
         it('deletes a product', async () => {
             productToDelete = allProducts[0];
-            const result = await client.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, { id: productToDelete.id });
+            const result = await client.query<DeleteProduct.Mutation, DeleteProduct.Variables>(
+                DELETE_PRODUCT,
+                { id: productToDelete.id },
+            );
 
             expect(result.deleteProduct).toEqual({ result: DeletionResult.DELETED });
         });
@@ -732,7 +773,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: 'T_1',
                         productId: productToDelete.id,
                     }),

+ 39 - 0
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -18,6 +18,7 @@ import {
     GetProduct2Variants,
     GetProductCollection,
     GetProductFacetValues,
+    GetProductSimple,
     GetProductsTake3,
     GetProductWithVariants,
     GetVariantFacetValues,
@@ -30,6 +31,7 @@ import {
     CREATE_COLLECTION,
     CREATE_FACET,
     GET_FACET_LIST,
+    GET_PRODUCT_SIMPLE,
     GET_PRODUCT_WITH_VARIANTS,
     UPDATE_COLLECTION,
     UPDATE_PRODUCT,
@@ -37,6 +39,7 @@ import {
 } from './graphql/shared-definitions';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 describe('Shop catalog', () => {
     const shopClient = new TestShopClient();
@@ -98,6 +101,42 @@ describe('Shop catalog', () => {
             expect(result.products.items.map(item => item.id)).toEqual(['T_2', 'T_3', 'T_4']);
         });
 
+        it('by id', async () => {
+            const { product } = await shopClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                GET_PRODUCT_SIMPLE,
+                { id: 'T_2' },
+            );
+
+            if (!product) {
+                fail('Product not found');
+                return;
+            }
+            expect(product.id).toBe('T_2');
+        });
+
+        it('by slug', async () => {
+            const { product } = await shopClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                GET_PRODUCT_SIMPLE,
+                { slug: 'curvy-monitor' },
+            );
+
+            if (!product) {
+                fail('Product not found');
+                return;
+            }
+            expect(product.slug).toBe('curvy-monitor');
+        });
+
+        it(
+            'throws if neither id nor slug provided',
+            assertThrowsWithMessage(async () => {
+                await shopClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                    GET_PRODUCT_SIMPLE,
+                    {},
+                );
+            }, 'Either the product id or slug must be provided'),
+        );
+
         it('product returns null for disabled product', async () => {
             const result = await shopClient.query<GetProduct1.Query>(gql`
                 query GetProduct1 {

+ 12 - 5
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -1,19 +1,20 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    DeletionResponse,
     MutationAddOptionGroupToProductArgs,
     MutationCreateProductArgs,
     MutationDeleteProductArgs,
-    DeletionResponse,
     MutationGenerateVariantsForProductArgs,
-    Permission,
-    QueryProductArgs,
-    QueryProductsArgs,
     MutationRemoveOptionGroupFromProductArgs,
     MutationUpdateProductArgs,
     MutationUpdateProductVariantsArgs,
+    Permission,
+    QueryProductArgs,
+    QueryProductsArgs,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
+import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
 import { assertFound } from '../../../common/utils';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
@@ -49,7 +50,13 @@ export class ProductResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryProductArgs,
     ): Promise<Translated<Product> | undefined> {
-        return this.productService.findOne(ctx, args.id);
+        if (args.id) {
+            return this.productService.findOne(ctx, args.id);
+        } else if (args.slug) {
+            return this.productService.findOneBySlug(ctx, args.slug);
+        } else {
+            throw new UserInputError(`error.product-id-or-slug-must-be-provided`);
+        }
     }
 
     @Mutation()

+ 9 - 2
packages/core/src/api/resolvers/shop/shop-products.resolver.ts

@@ -9,7 +9,7 @@ import { QueryProductsArgs } from '@vendure/common/lib/generated-shop-types';
 import { Omit } from '@vendure/common/lib/omit';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
-import { InternalServerError } from '../../../common/error/errors';
+import { InternalServerError, UserInputError } from '../../../common/error/errors';
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
 import { Collection } from '../../../entity/collection/collection.entity';
@@ -50,7 +50,14 @@ export class ShopProductsResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryProductArgs,
     ): Promise<Translated<Product> | undefined> {
-        const result = await this.productService.findOne(ctx, args.id);
+        let result: Translated<Product> | undefined;
+        if (args.id) {
+            result = await this.productService.findOne(ctx, args.id);
+        } else if (args.slug) {
+            result = await this.productService.findOneBySlug(ctx, args.slug);
+        } else {
+            throw new UserInputError(`error.product-id-or-slug-must-be-provided`);
+        }
         if (!result) {
             return;
         }

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

@@ -1,6 +1,7 @@
 type Query {
     products(languageCode: LanguageCode, options: ProductListOptions): ProductList!
-    product(id: ID!, languageCode: LanguageCode): Product
+    "Get a Product either by id or slug. If neither id nor slug is speicified, an error will result."
+    product(id: ID, slug: String, languageCode: LanguageCode): Product
 }
 
 type Mutation {

+ 2 - 1
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -10,7 +10,8 @@ type Query {
     nextOrderStates: [String!]!
     order(id: ID!): Order
     orderByCode(code: String!): Order
-    product(id: ID!, languageCode: LanguageCode): Product
+    "Get a Product either by id or slug. If neither id nor slug is speicified, an error will result."
+    product(id: ID, slug: String, languageCode: LanguageCode): Product
     products(languageCode: LanguageCode, options: ProductListOptions): ProductList!
     search(input: SearchInput!): SearchResponse!
 }

+ 5 - 2
packages/core/src/entity/product/product-translation.entity.ts

@@ -1,6 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { DeepPartial, HasCustomFields } from '@vendure/common/lib/shared-types';
-import { Column, Entity, Index, ManyToOne } from 'typeorm';
+import { DeepPartial, HasCustomFields, ID } from '@vendure/common/lib/shared-types';
+import { Column, Entity, Index, ManyToOne, RelationId } from 'typeorm';
 
 import { Translation } from '../../common/types/locale-types';
 import { VendureEntity } from '../base/base.entity';
@@ -26,6 +26,9 @@ export class ProductTranslation extends VendureEntity implements Translation<Pro
     @ManyToOne(type => Product, base => base.translations)
     base: Product;
 
+    @RelationId((item: ProductTranslation) => item.base)
+    baseId: ID;
+
     @Column(type => CustomProductFieldsTranslation)
     customFields: CustomProductFieldsTranslation;
 }

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -26,6 +26,7 @@
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-not-recognized": "Password reset token not recognized",
+    "product-id-or-slug-must-be-provided": "Either the product id or slug must be provided",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
     "verification-token-has-expired": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "verification-token-not-recognized": "Verification token not recognized",

+ 13 - 0
packages/core/src/service/services/product.service.ts

@@ -99,6 +99,19 @@ export class ProductService {
         ]);
     }
 
+    async findOneBySlug(ctx: RequestContext, slug: string): Promise<Translated<Product> | undefined> {
+        const translation = await this.connection.getRepository(ProductTranslation).findOne({
+            where: {
+                languageCode: ctx.languageCode,
+                slug,
+            },
+        });
+        if (!translation) {
+            return;
+        }
+        return this.findOne(ctx, translation.baseId);
+    }
+
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
         await this.validateSlugs(input);
         const product = await this.translatableSaver.create({

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


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


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