Browse Source

feat(core): Use query param to specify language

Relates to #128

BREAKING CHANGE: All `languageCode` GraphQL arguments have been removed from queries and instead, a "languageCode" query param may be attached to the API URL to specify the language of any translatable entities.
Michael Bromley 6 years ago
parent
commit
20350031e5

+ 26 - 34
packages/common/src/generated-types.ts

@@ -205,6 +205,7 @@ export type Channel = Node & {
 
 export type Collection = Node & {
   __typename?: 'Collection',
+  isPrivate: Scalars['Boolean'],
   id: Scalars['ID'],
   createdAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
@@ -220,7 +221,6 @@ export type Collection = Node & {
   filters: Array<ConfigurableOperation>,
   translations: Array<CollectionTranslation>,
   productVariants: ProductVariantList,
-  isPrivate: Scalars['Boolean'],
   customFields?: Maybe<Scalars['JSON']>,
 };
 
@@ -236,13 +236,13 @@ export type CollectionBreadcrumb = {
 };
 
 export type CollectionFilterParameter = {
+  isPrivate?: Maybe<BooleanOperators>,
   createdAt?: Maybe<DateOperators>,
   updatedAt?: Maybe<DateOperators>,
   languageCode?: Maybe<StringOperators>,
   name?: Maybe<StringOperators>,
   position?: Maybe<NumberOperators>,
   description?: Maybe<StringOperators>,
-  isPrivate?: Maybe<BooleanOperators>,
 };
 
 export type CollectionList = PaginatedList & {
@@ -1008,6 +1008,7 @@ export enum DeletionResult {
 
 export type Facet = Node & {
   __typename?: 'Facet',
+  isPrivate: Scalars['Boolean'],
   id: Scalars['ID'],
   createdAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
@@ -1016,17 +1017,16 @@ export type Facet = Node & {
   code: Scalars['String'],
   values: Array<FacetValue>,
   translations: Array<FacetTranslation>,
-  isPrivate: Scalars['Boolean'],
   customFields?: Maybe<Scalars['JSON']>,
 };
 
 export type FacetFilterParameter = {
+  isPrivate?: Maybe<BooleanOperators>,
   createdAt?: Maybe<DateOperators>,
   updatedAt?: Maybe<DateOperators>,
   languageCode?: Maybe<StringOperators>,
   name?: Maybe<StringOperators>,
   code?: Maybe<StringOperators>,
-  isPrivate?: Maybe<BooleanOperators>,
 };
 
 export type FacetList = PaginatedList & {
@@ -1699,7 +1699,6 @@ export type Mutation = {
   addNoteToOrder: Order,
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod,
-  reindex: JobInfo,
   /** Create a new ProductOptionGroup */
   createProductOptionGroup: ProductOptionGroup,
   /** Update an existing ProductOptionGroup */
@@ -1708,6 +1707,7 @@ export type Mutation = {
   createProductOption: ProductOption,
   /** Create a new ProductOption within a ProductOptionGroup */
   updateProductOption: ProductOption,
+  reindex: JobInfo,
   /** Create a new Product */
   createProduct: Product,
   /** Update an existing Product */
@@ -2343,6 +2343,7 @@ export type PriceRange = {
 
 export type Product = Node & {
   __typename?: 'Product',
+  enabled: Scalars['Boolean'],
   id: Scalars['ID'],
   createdAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
@@ -2357,18 +2358,17 @@ export type Product = Node & {
   facetValues: Array<FacetValue>,
   translations: Array<ProductTranslation>,
   collections: Array<Collection>,
-  enabled: Scalars['Boolean'],
   customFields?: Maybe<Scalars['JSON']>,
 };
 
 export type ProductFilterParameter = {
+  enabled?: Maybe<BooleanOperators>,
   createdAt?: Maybe<DateOperators>,
   updatedAt?: Maybe<DateOperators>,
   languageCode?: Maybe<StringOperators>,
   name?: Maybe<StringOperators>,
   slug?: Maybe<StringOperators>,
   description?: Maybe<StringOperators>,
-  enabled?: Maybe<BooleanOperators>,
 };
 
 export type ProductList = PaginatedList & {
@@ -2473,6 +2473,10 @@ export type ProductTranslationInput = {
 
 export type ProductVariant = Node & {
   __typename?: 'ProductVariant',
+  enabled: Scalars['Boolean'],
+  stockOnHand: Scalars['Int'],
+  trackInventory: Scalars['Boolean'],
+  stockMovements: StockMovementList,
   id: Scalars['ID'],
   productId: Scalars['ID'],
   createdAt: Scalars['DateTime'],
@@ -2491,10 +2495,6 @@ export type ProductVariant = Node & {
   options: Array<ProductOption>,
   facetValues: Array<FacetValue>,
   translations: Array<ProductVariantTranslation>,
-  enabled: Scalars['Boolean'],
-  stockOnHand: Scalars['Int'],
-  trackInventory: Scalars['Boolean'],
-  stockMovements: StockMovementList,
   customFields?: Maybe<Scalars['JSON']>,
 };
 
@@ -2504,6 +2504,9 @@ export type ProductVariantStockMovementsArgs = {
 };
 
 export type ProductVariantFilterParameter = {
+  enabled?: Maybe<BooleanOperators>,
+  stockOnHand?: Maybe<NumberOperators>,
+  trackInventory?: Maybe<BooleanOperators>,
   createdAt?: Maybe<DateOperators>,
   updatedAt?: Maybe<DateOperators>,
   languageCode?: Maybe<StringOperators>,
@@ -2513,9 +2516,6 @@ export type ProductVariantFilterParameter = {
   currencyCode?: Maybe<StringOperators>,
   priceIncludesTax?: Maybe<BooleanOperators>,
   priceWithTax?: Maybe<NumberOperators>,
-  enabled?: Maybe<BooleanOperators>,
-  stockOnHand?: Maybe<NumberOperators>,
-  trackInventory?: Maybe<BooleanOperators>,
 };
 
 export type ProductVariantList = PaginatedList & {
@@ -2532,6 +2532,7 @@ export type ProductVariantListOptions = {
 };
 
 export type ProductVariantSortParameter = {
+  stockOnHand?: Maybe<SortOrder>,
   id?: Maybe<SortOrder>,
   productId?: Maybe<SortOrder>,
   createdAt?: Maybe<SortOrder>,
@@ -2540,7 +2541,6 @@ export type ProductVariantSortParameter = {
   name?: Maybe<SortOrder>,
   price?: Maybe<SortOrder>,
   priceWithTax?: Maybe<SortOrder>,
-  stockOnHand?: Maybe<SortOrder>,
 };
 
 export type ProductVariantTranslation = {
@@ -2625,9 +2625,9 @@ export type Query = {
   orders: OrderList,
   paymentMethods: PaymentMethodList,
   paymentMethod?: Maybe<PaymentMethod>,
-  search: SearchResponse,
   productOptionGroups: Array<ProductOptionGroup>,
   productOptionGroup?: Maybe<ProductOptionGroup>,
+  search: SearchResponse,
   products: ProductList,
   /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
   product?: Maybe<Product>,
@@ -2675,14 +2675,12 @@ export type QueryChannelArgs = {
 
 
 export type QueryCollectionsArgs = {
-  languageCode?: Maybe<LanguageCode>,
   options?: Maybe<CollectionListOptions>
 };
 
 
 export type QueryCollectionArgs = {
-  id: Scalars['ID'],
-  languageCode?: Maybe<LanguageCode>
+  id: Scalars['ID']
 };
 
 
@@ -2712,14 +2710,12 @@ export type QueryCustomerArgs = {
 
 
 export type QueryFacetsArgs = {
-  languageCode?: Maybe<LanguageCode>,
   options?: Maybe<FacetListOptions>
 };
 
 
 export type QueryFacetArgs = {
-  id: Scalars['ID'],
-  languageCode?: Maybe<LanguageCode>
+  id: Scalars['ID']
 };
 
 
@@ -2753,33 +2749,29 @@ export type QueryPaymentMethodArgs = {
 };
 
 
-export type QuerySearchArgs = {
-  input: SearchInput
-};
-
-
 export type QueryProductOptionGroupsArgs = {
-  languageCode?: Maybe<LanguageCode>,
   filterTerm?: Maybe<Scalars['String']>
 };
 
 
 export type QueryProductOptionGroupArgs = {
-  id: Scalars['ID'],
-  languageCode?: Maybe<LanguageCode>
+  id: Scalars['ID']
+};
+
+
+export type QuerySearchArgs = {
+  input: SearchInput
 };
 
 
 export type QueryProductsArgs = {
-  languageCode?: Maybe<LanguageCode>,
   options?: Maybe<ProductListOptions>
 };
 
 
 export type QueryProductArgs = {
   id?: Maybe<Scalars['ID']>,
-  slug?: Maybe<Scalars['String']>,
-  languageCode?: Maybe<LanguageCode>
+  slug?: Maybe<Scalars['String']>
 };
 
 
@@ -2945,6 +2937,7 @@ export type SearchResponse = {
 
 export type SearchResult = {
   __typename?: 'SearchResult',
+  enabled: Scalars['Boolean'],
   sku: Scalars['String'],
   slug: Scalars['String'],
   productId: Scalars['ID'],
@@ -2963,7 +2956,6 @@ export type SearchResult = {
   collectionIds: Array<Scalars['String']>,
   /** A relevence score for the result. Differs between database implementations */
   score: Scalars['Float'],
-  enabled: Scalars['Boolean'],
 };
 
 /** The price of a search result product, either as a range or as a single price */

+ 73 - 59
packages/core/e2e/collection.e2e-spec.ts

@@ -5,7 +5,10 @@ import path from 'path';
 
 import { pick } from '../../common/lib/pick';
 import { StringOperator } from '../src/common/configurable-operation';
-import { facetValueCollectionFilter, variantNameCollectionFilter } from '../src/config/collection/default-collection-filters';
+import {
+    facetValueCollectionFilter,
+    variantNameCollectionFilter,
+} from '../src/config/collection/default-collection-filters';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { COLLECTION_FRAGMENT, FACET_VALUE_FRAGMENT } from './graphql/fragments';
@@ -262,7 +265,7 @@ describe('Collection resolver', () => {
         const { updateCollection } = await client.query<
             UpdateCollection.Mutation,
             UpdateCollection.Variables
-            >(UPDATE_COLLECTION, {
+        >(UPDATE_COLLECTION, {
             input: {
                 id: pearCollection.id,
                 assetIds: [assets[1].id],
@@ -396,52 +399,52 @@ describe('Collection resolver', () => {
     });
 
     describe('deleteCollection', () => {
-
         let collectionToDelete: CreateCollection.CreateCollection;
         let laptopProductId: string;
 
         beforeAll(async () => {
-            const { createCollection } = await client.query<CreateCollection.Mutation, CreateCollection.Variables>(
-                CREATE_COLLECTION,
-                {
-                    input: {
-                        filters: [
-                            {
-                                code: variantNameCollectionFilter.code,
-                                arguments: [
-                                    {
-                                        name: 'operator',
-                                        value: 'contains',
-                                        type: ConfigArgType.STRING_OPERATOR,
-                                    },
-                                    {
-                                        name: 'term',
-                                        value: 'laptop',
-                                        type: ConfigArgType.STRING,
-                                    },
-                                ],
-                            },
-                        ],
-                        translations: [
-                            { languageCode: LanguageCode.en, name: 'Delete Me', description: '' },
-                        ],
-                    },
+            const { createCollection } = await client.query<
+                CreateCollection.Mutation,
+                CreateCollection.Variables
+            >(CREATE_COLLECTION, {
+                input: {
+                    filters: [
+                        {
+                            code: variantNameCollectionFilter.code,
+                            arguments: [
+                                {
+                                    name: 'operator',
+                                    value: 'contains',
+                                    type: ConfigArgType.STRING_OPERATOR,
+                                },
+                                {
+                                    name: 'term',
+                                    value: 'laptop',
+                                    type: ConfigArgType.STRING,
+                                },
+                            ],
+                        },
+                    ],
+                    translations: [{ languageCode: LanguageCode.en, name: 'Delete Me', description: '' }],
                 },
-            );
+            });
             collectionToDelete = createCollection;
         });
 
-        it('throws for invalid collection id', assertThrowsWithMessage(async () => {
+        it(
+            'throws for invalid collection id',
+            assertThrowsWithMessage(async () => {
                 await client.query<DeleteCollection.Mutation, DeleteCollection.Variables>(DELETE_COLLECTION, {
                     id: 'T_999',
                 });
-            },
-            'No Collection with the id \'999\' could be found',
-            ),
+            }, "No Collection with the id '999' could be found"),
         );
 
         it('collection and product related prior to deletion', async () => {
-            const { collection } = await client.query<GetCollectionProducts.Query, GetCollectionProducts.Variables>(GET_COLLECTION_PRODUCT_VARIANTS, {
+            const { collection } = await client.query<
+                GetCollectionProducts.Query,
+                GetCollectionProducts.Variables
+            >(GET_COLLECTION_PRODUCT_VARIANTS, {
                 id: collectionToDelete.id,
             });
             expect(collection!.productVariants.items.map(pick(['name']))).toEqual([
@@ -453,7 +456,10 @@ describe('Collection resolver', () => {
 
             laptopProductId = collection!.productVariants.items[0].productId;
 
-            const { product } = await client.query<GetProductCollections.Query, GetProductCollections.Variables>(GET_PRODUCT_COLLECTIONS, {
+            const { product } = await client.query<
+                GetProductCollections.Query,
+                GetProductCollections.Variables
+            >(GET_PRODUCT_COLLECTIONS, {
                 id: laptopProductId,
             });
 
@@ -466,7 +472,10 @@ describe('Collection resolver', () => {
         });
 
         it('deleteCollection works', async () => {
-            const { deleteCollection } = await client.query<DeleteCollection.Mutation, DeleteCollection.Variables>(DELETE_COLLECTION, {
+            const { deleteCollection } = await client.query<
+                DeleteCollection.Mutation,
+                DeleteCollection.Variables
+            >(DELETE_COLLECTION, {
                 id: collectionToDelete.id,
             });
 
@@ -474,14 +483,20 @@ describe('Collection resolver', () => {
         });
 
         it('deleted collection is null', async () => {
-            const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(GET_COLLECTION, {
-                id: collectionToDelete.id,
-            });
+            const { collection } = await client.query<GetCollection.Query, GetCollection.Variables>(
+                GET_COLLECTION,
+                {
+                    id: collectionToDelete.id,
+                },
+            );
             expect(collection).toBeNull();
         });
 
         it('product no longer lists collection', async () => {
-            const { product } = await client.query<GetProductCollections.Query, GetProductCollections.Variables>(GET_PRODUCT_COLLECTIONS, {
+            const { product } = await client.query<
+                GetProductCollections.Query,
+                GetProductCollections.Variables
+            >(GET_PRODUCT_COLLECTIONS, {
                 id: laptopProductId,
             });
 
@@ -491,7 +506,6 @@ describe('Collection resolver', () => {
                 { id: 'T_5', name: 'Pear' },
             ]);
         });
-
     });
 
     describe('filters', () => {
@@ -499,7 +513,7 @@ describe('Collection resolver', () => {
             const result = await client.query<
                 CreateCollectionSelectVariants.Mutation,
                 CreateCollectionSelectVariants.Variables
-                >(CREATE_COLLECTION_SELECT_VARIANTS, {
+            >(CREATE_COLLECTION_SELECT_VARIANTS, {
                 input: {
                     translations: [{ languageCode: LanguageCode.en, name: 'Empty', description: '' }],
                     filters: [],
@@ -513,7 +527,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: electronicsCollection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -545,7 +559,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: computersCollection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -573,7 +587,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
-                    >(CREATE_COLLECTION_SELECT_VARIANTS, {
+                >(CREATE_COLLECTION_SELECT_VARIANTS, {
                     input: {
                         translations: [
                             { languageCode: LanguageCode.en, name: 'Photo AND Pear', description: '' },
@@ -608,7 +622,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     CreateCollectionSelectVariants.Mutation,
                     CreateCollectionSelectVariants.Variables
-                    >(CREATE_COLLECTION_SELECT_VARIANTS, {
+                >(CREATE_COLLECTION_SELECT_VARIANTS, {
                     input: {
                         translations: [
                             { languageCode: LanguageCode.en, name: 'Photo OR Pear', description: '' },
@@ -655,7 +669,7 @@ describe('Collection resolver', () => {
                 const { createCollection } = await client.query<
                     CreateCollection.Mutation,
                     CreateCollection.Variables
-                    >(CREATE_COLLECTION, {
+                >(CREATE_COLLECTION, {
                     input: {
                         translations: [
                             { languageCode: LanguageCode.en, name: `${operator} ${term}`, description: '' },
@@ -688,7 +702,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: collection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -704,7 +718,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: collection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual(['Camera Lens']);
@@ -716,7 +730,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: collection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -731,7 +745,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, {
+                >(GET_COLLECTION_PRODUCT_VARIANTS, {
                     id: collection.id,
                 });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -792,7 +806,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
                     'Laptop 13 inch 8GB',
                     'Laptop 15 inch 8GB',
@@ -820,7 +834,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
                     'Laptop 13 inch 8GB',
                     'Laptop 15 inch 8GB',
@@ -849,7 +863,7 @@ describe('Collection resolver', () => {
                 const result = await client.query<
                     GetCollectionProducts.Query,
                     GetCollectionProducts.Variables
-                    >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
+                >(GET_COLLECTION_PRODUCT_VARIANTS, { id: pearCollection.id });
                 expect(result.collection!.productVariants.items.map(i => i.name)).toEqual([
                     'Laptop 13 inch 8GB',
                     'Laptop 15 inch 8GB',
@@ -869,7 +883,7 @@ describe('Collection resolver', () => {
             const result = await client.query<
                 GetCollectionsForProducts.Query,
                 GetCollectionsForProducts.Variables
-                >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
+            >(GET_COLLECTIONS_FOR_PRODUCTS, { term: 'camera' });
             expect(result.products.items[0].collections).toEqual([
                 { id: 'T_3', name: 'Electronics' },
                 { id: 'T_5', name: 'Pear' },
@@ -888,7 +902,7 @@ describe('Collection resolver', () => {
         const { collection } = await client.query<
             GetCollectionProducts.Query,
             GetCollectionProducts.Variables
-            >(GET_COLLECTION_PRODUCT_VARIANTS, {
+        >(GET_COLLECTION_PRODUCT_VARIANTS, {
             id: pearCollection.id,
         });
         expect(collection!.productVariants.items.map(i => i.name)).toEqual([
@@ -911,8 +925,8 @@ describe('Collection resolver', () => {
 });
 
 export const GET_COLLECTION = gql`
-    query GetCollection($id: ID!, $languageCode: LanguageCode) {
-        collection(id: $id, languageCode: $languageCode) {
+    query GetCollection($id: ID!) {
+        collection(id: $id) {
             ...Collection
         }
     }
@@ -943,7 +957,7 @@ const GET_FACET_VALUES = gql`
 
 const GET_COLLECTIONS = gql`
     query GetCollections {
-        collections(languageCode: en) {
+        collections {
             items {
                 id
                 name

+ 2 - 0
packages/core/e2e/config/tsconfig.e2e.json

@@ -5,6 +5,8 @@
     "lib": ["es2015"],
     "skipLibCheck": false,
     "inlineSourceMap": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
     "allowJs": true
   },
   "include": ["../**/*.e2e-spec.ts", "../**/*.d.ts"]

+ 1 - 1
packages/core/e2e/entity-id-strategy.e2e-spec.ts

@@ -26,7 +26,7 @@ describe('EntityIdStrategy', () => {
     it('Does not doubly-encode ids from resolved properties', async () => {
         const result = await shopClient.query<EntityIdTest.Query>(gql`
             query EntityIdTest {
-                product(id: "T_1", languageCode: en) {
+                product(id: "T_1") {
                     id
                     variants {
                         id

+ 22 - 12
packages/core/e2e/facet.e2e-spec.ts

@@ -268,7 +268,10 @@ describe('Facet resolver', () => {
         });
 
         it('deleteFacet that is in use returns NOT_DELETED', async () => {
-            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, { id: speakerTypeFacet.id, force: false });
+            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
+                id: speakerTypeFacet.id,
+                force: false,
+            });
             const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
                 GET_FACET_WITH_VALUES,
                 {
@@ -285,7 +288,10 @@ describe('Facet resolver', () => {
         });
 
         it('deleteFacet that is in use can be force deleted', async () => {
-            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, { id: speakerTypeFacet.id, force: true });
+            const result1 = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
+                id: speakerTypeFacet.id,
+                force: true,
+            });
 
             expect(result1.deleteFacet).toEqual({
                 result: DeletionResult.DELETED,
@@ -312,24 +318,28 @@ describe('Facet resolver', () => {
         });
 
         it('deleteFacet with no FacetValues works', async () => {
-            const { createFacet } = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
-                input: {
-                    code: 'test',
-                    isPrivate: false,
-                    translations: [
-                        { languageCode: LanguageCode.en, name: 'Test' },
-                    ],
+            const { createFacet } = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(
+                CREATE_FACET,
+                {
+                    input: {
+                        code: 'test',
+                        isPrivate: false,
+                        translations: [{ languageCode: LanguageCode.en, name: 'Test' }],
+                    },
                 },
+            );
+            const result = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, {
+                id: createFacet.id,
+                force: false,
             });
-            const result = await client.query<DeleteFacet.Mutation, DeleteFacet.Variables>(DELETE_FACET, { id: createFacet.id, force: false });
             expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
         });
     });
 });
 
 export const GET_FACET_WITH_VALUES = gql`
-    query GetFacetWithValues($id: ID!, $languageCode: LanguageCode) {
-        facet(id: $id, languageCode: $languageCode) {
+    query GetFacetWithValues($id: ID!) {
+        facet(id: $id) {
             ...FacetWithValues
         }
     }

+ 38 - 39
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -206,6 +206,7 @@ export type Channel = Node & {
 
 export type Collection = Node & {
     __typename?: 'Collection';
+    isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -221,7 +222,6 @@ export type Collection = Node & {
     filters: Array<ConfigurableOperation>;
     translations: Array<CollectionTranslation>;
     productVariants: ProductVariantList;
-    isPrivate: Scalars['Boolean'];
     customFields?: Maybe<Scalars['JSON']>;
 };
 
@@ -236,13 +236,13 @@ export type CollectionBreadcrumb = {
 };
 
 export type CollectionFilterParameter = {
+    isPrivate?: Maybe<BooleanOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     languageCode?: Maybe<StringOperators>;
     name?: Maybe<StringOperators>;
     position?: Maybe<NumberOperators>;
     description?: Maybe<StringOperators>;
-    isPrivate?: Maybe<BooleanOperators>;
 };
 
 export type CollectionList = PaginatedList & {
@@ -1012,6 +1012,7 @@ export enum DeletionResult {
 
 export type Facet = Node & {
     __typename?: 'Facet';
+    isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1020,17 +1021,16 @@ export type Facet = Node & {
     code: Scalars['String'];
     values: Array<FacetValue>;
     translations: Array<FacetTranslation>;
-    isPrivate: Scalars['Boolean'];
     customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type FacetFilterParameter = {
+    isPrivate?: Maybe<BooleanOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     languageCode?: Maybe<StringOperators>;
     name?: Maybe<StringOperators>;
     code?: Maybe<StringOperators>;
-    isPrivate?: Maybe<BooleanOperators>;
 };
 
 export type FacetList = PaginatedList & {
@@ -1702,7 +1702,6 @@ export type Mutation = {
     addNoteToOrder: Order;
     /** Update an existing PaymentMethod */
     updatePaymentMethod: PaymentMethod;
-    reindex: JobInfo;
     /** Create a new ProductOptionGroup */
     createProductOptionGroup: ProductOptionGroup;
     /** Update an existing ProductOptionGroup */
@@ -1711,6 +1710,7 @@ export type Mutation = {
     createProductOption: ProductOption;
     /** Create a new ProductOption within a ProductOptionGroup */
     updateProductOption: ProductOption;
+    reindex: JobInfo;
     /** Create a new Product */
     createProduct: Product;
     /** Update an existing Product */
@@ -2278,6 +2278,7 @@ export type PriceRange = {
 
 export type Product = Node & {
     __typename?: 'Product';
+    enabled: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2292,18 +2293,17 @@ export type Product = Node & {
     facetValues: Array<FacetValue>;
     translations: Array<ProductTranslation>;
     collections: Array<Collection>;
-    enabled: Scalars['Boolean'];
     customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type ProductFilterParameter = {
+    enabled?: Maybe<BooleanOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     languageCode?: Maybe<StringOperators>;
     name?: Maybe<StringOperators>;
     slug?: Maybe<StringOperators>;
     description?: Maybe<StringOperators>;
-    enabled?: Maybe<BooleanOperators>;
 };
 
 export type ProductList = PaginatedList & {
@@ -2408,6 +2408,10 @@ export type ProductTranslationInput = {
 
 export type ProductVariant = Node & {
     __typename?: 'ProductVariant';
+    enabled: Scalars['Boolean'];
+    stockOnHand: Scalars['Int'];
+    trackInventory: Scalars['Boolean'];
+    stockMovements: StockMovementList;
     id: Scalars['ID'];
     productId: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -2426,10 +2430,6 @@ export type ProductVariant = Node & {
     options: Array<ProductOption>;
     facetValues: Array<FacetValue>;
     translations: Array<ProductVariantTranslation>;
-    enabled: Scalars['Boolean'];
-    stockOnHand: Scalars['Int'];
-    trackInventory: Scalars['Boolean'];
-    stockMovements: StockMovementList;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
@@ -2438,6 +2438,9 @@ export type ProductVariantStockMovementsArgs = {
 };
 
 export type ProductVariantFilterParameter = {
+    enabled?: Maybe<BooleanOperators>;
+    stockOnHand?: Maybe<NumberOperators>;
+    trackInventory?: Maybe<BooleanOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     languageCode?: Maybe<StringOperators>;
@@ -2447,9 +2450,6 @@ export type ProductVariantFilterParameter = {
     currencyCode?: Maybe<StringOperators>;
     priceIncludesTax?: Maybe<BooleanOperators>;
     priceWithTax?: Maybe<NumberOperators>;
-    enabled?: Maybe<BooleanOperators>;
-    stockOnHand?: Maybe<NumberOperators>;
-    trackInventory?: Maybe<BooleanOperators>;
 };
 
 export type ProductVariantList = PaginatedList & {
@@ -2466,6 +2466,7 @@ export type ProductVariantListOptions = {
 };
 
 export type ProductVariantSortParameter = {
+    stockOnHand?: Maybe<SortOrder>;
     id?: Maybe<SortOrder>;
     productId?: Maybe<SortOrder>;
     createdAt?: Maybe<SortOrder>;
@@ -2474,7 +2475,6 @@ export type ProductVariantSortParameter = {
     name?: Maybe<SortOrder>;
     price?: Maybe<SortOrder>;
     priceWithTax?: Maybe<SortOrder>;
-    stockOnHand?: Maybe<SortOrder>;
 };
 
 export type ProductVariantTranslation = {
@@ -2559,9 +2559,9 @@ export type Query = {
     orders: OrderList;
     paymentMethods: PaymentMethodList;
     paymentMethod?: Maybe<PaymentMethod>;
-    search: SearchResponse;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
+    search: SearchResponse;
     products: ProductList;
     /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
     product?: Maybe<Product>;
@@ -2603,13 +2603,11 @@ export type QueryChannelArgs = {
 };
 
 export type QueryCollectionsArgs = {
-    languageCode?: Maybe<LanguageCode>;
     options?: Maybe<CollectionListOptions>;
 };
 
 export type QueryCollectionArgs = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type QueryCountriesArgs = {
@@ -2633,13 +2631,11 @@ export type QueryCustomerArgs = {
 };
 
 export type QueryFacetsArgs = {
-    languageCode?: Maybe<LanguageCode>;
     options?: Maybe<FacetListOptions>;
 };
 
 export type QueryFacetArgs = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type QueryJobArgs = {
@@ -2666,29 +2662,25 @@ export type QueryPaymentMethodArgs = {
     id: Scalars['ID'];
 };
 
-export type QuerySearchArgs = {
-    input: SearchInput;
-};
-
 export type QueryProductOptionGroupsArgs = {
-    languageCode?: Maybe<LanguageCode>;
     filterTerm?: Maybe<Scalars['String']>;
 };
 
 export type QueryProductOptionGroupArgs = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
+};
+
+export type QuerySearchArgs = {
+    input: SearchInput;
 };
 
 export type QueryProductsArgs = {
-    languageCode?: Maybe<LanguageCode>;
     options?: Maybe<ProductListOptions>;
 };
 
 export type QueryProductArgs = {
     id?: Maybe<Scalars['ID']>;
     slug?: Maybe<Scalars['String']>;
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type QueryPromotionArgs = {
@@ -2846,6 +2838,7 @@ export type SearchResponse = {
 
 export type SearchResult = {
     __typename?: 'SearchResult';
+    enabled: Scalars['Boolean'];
     sku: Scalars['String'];
     slug: Scalars['String'];
     productId: Scalars['ID'];
@@ -2864,7 +2857,6 @@ export type SearchResult = {
     collectionIds: Array<Scalars['String']>;
     /** A relevence score for the result. Differs between database implementations */
     score: Scalars['Float'];
-    enabled: Scalars['Boolean'];
 };
 
 /** The price of a search result product, either as a range or as a single price */
@@ -3309,7 +3301,6 @@ export type GetProductsWithVariantIdsQuery = { __typename?: 'Query' } & {
 
 export type GetCollectionQueryVariables = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetCollectionQuery = { __typename?: 'Query' } & {
@@ -3599,7 +3590,6 @@ export type EntityIdTestQuery = { __typename?: 'Query' } & {
 
 export type GetFacetWithValuesQueryVariables = {
     id: Scalars['ID'];
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetFacetWithValuesQuery = { __typename?: 'Query' } & {
@@ -3953,7 +3943,6 @@ export type CreateProductMutation = { __typename?: 'Mutation' } & {
 export type GetProductWithVariantsQueryVariables = {
     id?: Maybe<Scalars['ID']>;
     slug?: Maybe<Scalars['String']>;
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetProductWithVariantsQuery = { __typename?: 'Query' } & {
@@ -3962,16 +3951,14 @@ export type GetProductWithVariantsQuery = { __typename?: 'Query' } & {
 
 export type GetProductListQueryVariables = {
     options?: Maybe<ProductListOptions>;
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetProductListQuery = { __typename?: 'Query' } & {
     products: { __typename?: 'ProductList' } & Pick<ProductList, 'totalItems'> & {
             items: Array<
-                { __typename?: 'Product' } & Pick<
-                    Product,
-                    'id' | 'enabled' | 'languageCode' | 'name' | 'slug'
-                > & { featuredAsset: Maybe<{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>> }
+                { __typename?: 'Product' } & Pick<Product, 'id' | 'languageCode' | 'name' | 'slug'> & {
+                        featuredAsset: Maybe<{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>>;
+                    }
             >;
         };
 };
@@ -4115,7 +4102,6 @@ export type UpdateCountryMutation = { __typename?: 'Mutation' } & {
 
 export type GetFacetListQueryVariables = {
     options?: Maybe<FacetListOptions>;
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetFacetListQuery = { __typename?: 'Query' } & {
@@ -4135,7 +4121,6 @@ export type DeleteProductMutation = { __typename?: 'Mutation' } & {
 export type GetProductSimpleQueryVariables = {
     id?: Maybe<Scalars['ID']>;
     slug?: Maybe<Scalars['String']>;
-    languageCode?: Maybe<LanguageCode>;
 };
 
 export type GetProductSimpleQuery = { __typename?: 'Query' } & {
@@ -4215,6 +4200,14 @@ export type GetProductsQuery = { __typename?: 'Query' } & {
         };
 };
 
+export type UpdateOptionGroupMutationVariables = {
+    input: UpdateProductOptionGroupInput;
+};
+
+export type UpdateOptionGroupMutation = { __typename?: 'Mutation' } & {
+    updateProductOptionGroup: { __typename?: 'ProductOptionGroup' } & Pick<ProductOptionGroup, 'id'>;
+};
+
 export type GetOrderListQueryVariables = {
     options?: Maybe<OrderListOptions>;
 };
@@ -5358,6 +5351,12 @@ export namespace GetProducts {
     >;
 }
 
+export namespace UpdateOptionGroup {
+    export type Variables = UpdateOptionGroupMutationVariables;
+    export type Mutation = UpdateOptionGroupMutation;
+    export type UpdateProductOptionGroup = UpdateOptionGroupMutation['updateProductOptionGroup'];
+}
+
 export namespace GetOrderList {
     export type Variables = GetOrderListQueryVariables;
     export type Query = GetOrderListQuery;

+ 8 - 8
packages/core/e2e/graphql/shared-definitions.ts

@@ -43,8 +43,8 @@ export const CREATE_PRODUCT = gql`
 `;
 
 export const GET_PRODUCT_WITH_VARIANTS = gql`
-    query GetProductWithVariants($id: ID, $slug: String, $languageCode: LanguageCode) {
-        product(languageCode: $languageCode, slug: $slug, id: $id) {
+    query GetProductWithVariants($id: ID, $slug: String) {
+        product(slug: $slug, id: $id) {
             ...ProductWithVariants
         }
     }
@@ -52,8 +52,8 @@ export const GET_PRODUCT_WITH_VARIANTS = gql`
 `;
 
 export const GET_PRODUCT_LIST = gql`
-    query GetProductList($options: ProductListOptions, $languageCode: LanguageCode) {
-        products(languageCode: $languageCode, options: $options) {
+    query GetProductList($options: ProductListOptions) {
+        products(options: $options) {
             items {
                 id
                 languageCode
@@ -227,8 +227,8 @@ export const UPDATE_COUNTRY = gql`
 `;
 
 export const GET_FACET_LIST = gql`
-    query GetFacetList($options: FacetListOptions, $languageCode: LanguageCode) {
-        facets(languageCode: $languageCode, options: $options) {
+    query GetFacetList($options: FacetListOptions) {
+        facets(options: $options) {
             items {
                 ...FacetWithValues
             }
@@ -247,8 +247,8 @@ 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) {
+    query GetProductSimple($id: ID, $slug: String) {
+        product(slug: $slug, id: $id) {
             id
             slug
         }

+ 164 - 0
packages/core/e2e/localization.e2e-spec.ts

@@ -0,0 +1,164 @@
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { pick } from '../../common/lib/pick';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import {
+    GetProductWithVariants,
+    LanguageCode,
+    UpdateOptionGroup,
+    UpdateProduct,
+} from './graphql/generated-e2e-admin-types';
+import { GET_PRODUCT_WITH_VARIANTS, UPDATE_PRODUCT } from './graphql/shared-definitions';
+import { TestAdminClient } from './test-client';
+import { TestServer } from './test-server';
+
+/* tslint:disable:no-non-null-assertion */
+describe('Role resolver', () => {
+    const client = new TestAdminClient();
+    const server = new TestServer();
+
+    beforeAll(async () => {
+        const token = await server.init({
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await client.init();
+
+        const { updateProduct } = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+            UPDATE_PRODUCT,
+            {
+                input: {
+                    id: 'T_1',
+                    translations: [
+                        {
+                            languageCode: LanguageCode.en,
+                            name: 'en name',
+                            slug: 'en-slug',
+                            description: 'en-description',
+                        },
+                        {
+                            languageCode: LanguageCode.de,
+                            name: 'de name',
+                            slug: 'de-slug',
+                            description: 'de-description',
+                        },
+                        {
+                            languageCode: LanguageCode.zh,
+                            name: 'zh name',
+                            slug: 'zh-slug',
+                            description: 'zh-description',
+                        },
+                    ],
+                },
+            },
+        );
+
+        await client.query<UpdateOptionGroup.Mutation, UpdateOptionGroup.Variables>(UPDATE_OPTION_GROUP, {
+            input: {
+                id: 'T_1',
+                translations: [
+                    { languageCode: LanguageCode.en, name: 'en name' },
+                    { languageCode: LanguageCode.de, name: 'de name' },
+                    { languageCode: LanguageCode.zh, name: 'zh name' },
+                ],
+            },
+        });
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('returns default language when none specified', async () => {
+        const { product } = await client.query<
+            GetProductWithVariants.Query,
+            GetProductWithVariants.Variables
+        >(GET_PRODUCT_WITH_VARIANTS, {
+            id: 'T_1',
+        });
+        expect(pick(product!, ['name', 'slug', 'description'])).toEqual({
+            name: 'en name',
+            slug: 'en-slug',
+            description: 'en-description',
+        });
+    });
+
+    it('returns specified language', async () => {
+        const { product } = await client.query<
+            GetProductWithVariants.Query,
+            GetProductWithVariants.Variables
+        >(
+            GET_PRODUCT_WITH_VARIANTS,
+            {
+                id: 'T_1',
+            },
+            { languageCode: LanguageCode.de },
+        );
+        expect(pick(product!, ['name', 'slug', 'description'])).toEqual({
+            name: 'de name',
+            slug: 'de-slug',
+            description: 'de-description',
+        });
+    });
+
+    it('falls back to default language code', async () => {
+        const { product } = await client.query<
+            GetProductWithVariants.Query,
+            GetProductWithVariants.Variables
+        >(
+            GET_PRODUCT_WITH_VARIANTS,
+            {
+                id: 'T_1',
+            },
+            { languageCode: LanguageCode.ga },
+        );
+        expect(pick(product!, ['name', 'slug', 'description'])).toEqual({
+            name: 'en name',
+            slug: 'en-slug',
+            description: 'en-description',
+        });
+    });
+
+    it('nested entites are translated', async () => {
+        const { product } = await client.query<
+            GetProductWithVariants.Query,
+            GetProductWithVariants.Variables
+        >(
+            GET_PRODUCT_WITH_VARIANTS,
+            {
+                id: 'T_1',
+            },
+            { languageCode: LanguageCode.zh },
+        );
+        expect(pick(product!.optionGroups[0], ['name'])).toEqual({
+            name: 'zh name',
+        });
+    });
+
+    it('translates results of mutation', async () => {
+        const { updateProduct } = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+            UPDATE_PRODUCT,
+            {
+                input: {
+                    id: 'T_1',
+                    enabled: true,
+                },
+            },
+            { languageCode: LanguageCode.zh },
+        );
+        expect(updateProduct.name).toBe('zh name');
+        expect(pick(updateProduct.optionGroups[0], ['name'])).toEqual({
+            name: 'zh name',
+        });
+    });
+});
+
+const UPDATE_OPTION_GROUP = gql`
+    mutation UpdateOptionGroup($input: UpdateProductOptionGroupInput!) {
+        updateProductOptionGroup(input: $input) {
+            id
+        }
+    }
+`;

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

@@ -63,9 +63,7 @@ describe('Product resolver', () => {
         it('returns all products when no options passed', async () => {
             const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
-                {
-                    languageCode: LanguageCode.en,
-                },
+                {},
             );
 
             expect(result.products.items.length).toBe(20);
@@ -76,7 +74,6 @@ describe('Product resolver', () => {
             const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         skip: 0,
                         take: 3,
@@ -92,7 +89,6 @@ describe('Product resolver', () => {
             const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         filter: {
                             name: {
@@ -111,7 +107,6 @@ describe('Product resolver', () => {
             const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         filter: {
                             name: {
@@ -132,7 +127,6 @@ describe('Product resolver', () => {
             const result = await adminClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         sort: {
                             name: SortOrder.ASC,
@@ -169,7 +163,6 @@ describe('Product resolver', () => {
             const result = await shopClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         filter: {
                             name: {
@@ -188,7 +181,6 @@ describe('Product resolver', () => {
             const result = await shopClient.query<GetProductList.Query, GetProductList.Variables>(
                 GET_PRODUCT_LIST,
                 {
-                    languageCode: LanguageCode.en,
                     options: {
                         sort: {
                             name: SortOrder.ASC,
@@ -262,10 +254,13 @@ describe('Product resolver', () => {
         it(
             'throws if id and slug do not refer to the same Product',
             assertThrowsWithMessage(async () => {
-                await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(GET_PRODUCT_SIMPLE, {
-                    id: 'T_2',
-                    slug: 'laptop',
-                });
+                await adminClient.query<GetProductSimple.Query, GetProductSimple.Variables>(
+                    GET_PRODUCT_SIMPLE,
+                    {
+                        id: 'T_2',
+                        slug: 'laptop',
+                    },
+                );
             }, 'The provided id and slug refer to different Products'),
         );
 
@@ -273,8 +268,7 @@ describe('Product resolver', () => {
             const { product } = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
-                languageCode: LanguageCode.en,
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 id: 'T_2',
             });
 
@@ -287,13 +281,12 @@ describe('Product resolver', () => {
         });
 
         it('ProductVariant price properties are correct', async () => {
-            const result = await adminClient.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
-                GET_PRODUCT_WITH_VARIANTS,
-                {
-                    languageCode: LanguageCode.en,
-                    id: 'T_2',
-                },
-            );
+            const result = await adminClient.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: 'T_2',
+            });
 
             if (!result.product) {
                 fail('Product not found');
@@ -307,13 +300,12 @@ describe('Product resolver', () => {
         });
 
         it('returns null when id not found', async () => {
-            const result = await adminClient.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
-                GET_PRODUCT_WITH_VARIANTS,
-                {
-                    languageCode: LanguageCode.en,
-                    id: 'bad_id',
-                },
-            );
+            const result = await adminClient.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: 'bad_id',
+            });
 
             expect(result.product).toBeNull();
         });
@@ -527,9 +519,8 @@ describe('Product resolver', () => {
             const productResult = await adminClient.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 id: newProduct.id,
-                languageCode: LanguageCode.en,
             });
             const assets = productResult.product!.assets;
 
@@ -589,7 +580,7 @@ describe('Product resolver', () => {
             const result = await adminClient.query<
                 AddOptionGroupToProduct.Mutation,
                 AddOptionGroupToProduct.Variables
-                >(ADD_OPTION_GROUP_TO_PRODUCT, {
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
                 optionGroupId: 'T_2',
                 productId: newProduct.id,
             });
@@ -631,7 +622,7 @@ describe('Product resolver', () => {
             const { addOptionGroupToProduct } = await adminClient.query<
                 AddOptionGroupToProduct.Mutation,
                 AddOptionGroupToProduct.Variables
-                >(ADD_OPTION_GROUP_TO_PRODUCT, {
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
                 optionGroupId: 'T_1',
                 productId: newProductWithAssets.id,
             });
@@ -640,7 +631,7 @@ describe('Product resolver', () => {
             const result = await adminClient.query<
                 RemoveOptionGroupFromProduct.Mutation,
                 RemoveOptionGroupFromProduct.Variables
-                >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+            >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                 optionGroupId: 'T_1',
                 productId: newProductWithAssets.id,
             });
@@ -655,7 +646,7 @@ describe('Product resolver', () => {
                     adminClient.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: 'T_3',
                         productId: 'T_2',
                     }),
@@ -670,7 +661,7 @@ describe('Product resolver', () => {
                     adminClient.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: '1',
                         productId: '999',
                     }),
@@ -685,7 +676,7 @@ describe('Product resolver', () => {
                     adminClient.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: '999',
                         productId: newProduct.id,
                     }),
@@ -760,7 +751,7 @@ describe('Product resolver', () => {
                 const { createProductVariants } = await adminClient.query<
                     CreateProductVariants.Mutation,
                     CreateProductVariants.Variables
-                    >(CREATE_PRODUCT_VARIANTS, {
+                >(CREATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             productId: newProduct.id,
@@ -781,7 +772,7 @@ describe('Product resolver', () => {
                 const { createProductVariants } = await adminClient.query<
                     CreateProductVariants.Mutation,
                     CreateProductVariants.Variables
-                    >(CREATE_PRODUCT_VARIANTS, {
+                >(CREATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             productId: newProduct.id,
@@ -834,7 +825,7 @@ describe('Product resolver', () => {
                 const { updateProductVariants } = await adminClient.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -858,7 +849,7 @@ describe('Product resolver', () => {
                 const result = await adminClient.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -881,7 +872,7 @@ describe('Product resolver', () => {
                 const result = await adminClient.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -904,7 +895,7 @@ describe('Product resolver', () => {
                 const result = await adminClient.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -946,7 +937,7 @@ describe('Product resolver', () => {
                 const result1 = await adminClient.query<
                     GetProductWithVariants.Query,
                     GetProductWithVariants.Variables
-                    >(GET_PRODUCT_WITH_VARIANTS, {
+                >(GET_PRODUCT_WITH_VARIANTS, {
                     id: newProduct.id,
                 });
                 expect(result1.product!.variants.map(v => v.id)).toEqual(['T_35', 'T_36', 'T_37']);
@@ -954,7 +945,7 @@ describe('Product resolver', () => {
                 const { deleteProductVariant } = await adminClient.query<
                     DeleteProductVariant.Mutation,
                     DeleteProductVariant.Variables
-                    >(DELETE_PRODUCT_VARIANT, {
+                >(DELETE_PRODUCT_VARIANT, {
                     id: result1.product!.variants[0].id,
                 });
 
@@ -963,7 +954,7 @@ describe('Product resolver', () => {
                 const result2 = await adminClient.query<
                     GetProductWithVariants.Query,
                     GetProductWithVariants.Variables
-                    >(GET_PRODUCT_WITH_VARIANTS, {
+                >(GET_PRODUCT_WITH_VARIANTS, {
                     id: newProduct.id,
                 });
                 expect(result2.product!.variants.map(v => v.id)).toEqual(['T_36', 'T_37']);
@@ -991,12 +982,12 @@ describe('Product resolver', () => {
         });
 
         it('cannot get a deleted product', async () => {
-            const result = await adminClient.query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(
-                GET_PRODUCT_WITH_VARIANTS,
-                {
-                    id: productToDelete.id,
-                },
-            );
+            const result = await adminClient.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: productToDelete.id,
+            });
 
             expect(result.product).toBe(null);
         });
@@ -1044,7 +1035,7 @@ describe('Product resolver', () => {
                     adminClient.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: 'T_1',
                         productId: productToDelete.id,
                     }),

+ 17 - 4
packages/core/mock-data/simple-graphql-client.ts

@@ -5,6 +5,7 @@ import gql from 'graphql-tag';
 import { print } from 'graphql/language/printer';
 import fetch, { Response } from 'node-fetch';
 import { Curl } from 'node-libcurl';
+import { stringify } from 'querystring';
 
 import { ImportInfo } from '../e2e/graphql/generated-e2e-admin-types';
 import { getConfig } from '../src/config/config-helpers';
@@ -23,6 +24,8 @@ const LOGIN = gql`
     }
 `;
 
+export type QueryParams = { [key: string]: string | number };
+
 // tslint:disable:no-console
 /**
  * A minimalistic GraphQL client for populating and querying test data.
@@ -50,8 +53,12 @@ export class SimpleGraphQLClient {
     /**
      * Performs both query and mutation operations.
      */
-    async query<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<T> {
-        const response = await this.request(query, variables);
+    async query<T = any, V = Record<string, any>>(
+        query: DocumentNode,
+        variables?: V,
+        queryParams?: QueryParams,
+    ): Promise<T> {
+        const response = await this.request(query, variables, queryParams);
         const result = await this.getResult(response);
 
         if (response.ok && !result.errors && result.data) {
@@ -115,14 +122,20 @@ export class SimpleGraphQLClient {
         );
     }
 
-    private async request(query: DocumentNode, variables?: { [key: string]: any }): Promise<Response> {
+    private async request(
+        query: DocumentNode,
+        variables?: { [key: string]: any },
+        queryParams?: QueryParams,
+    ): Promise<Response> {
         const queryString = print(query);
         const body = JSON.stringify({
             query: queryString,
             variables: variables ? variables : undefined,
         });
 
-        const response = await fetch(this.apiUrl, {
+        const url = queryParams ? this.apiUrl + `?${stringify(queryParams)}` : this.apiUrl;
+
+        const response = await fetch(url, {
             method: 'POST',
             headers: { 'Content-Type': 'application/json', ...this.headers },
             body,

+ 8 - 5
packages/core/src/api/common/request-context.service.ts

@@ -10,8 +10,8 @@ import { AuthenticatedSession } from '../../entity/session/authenticated-session
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
 import { ChannelService } from '../../service/services/channel.service';
-import { getApiType } from './get-api-type';
 
+import { getApiType } from './get-api-type';
 import { RequestContext } from './request-context';
 
 export const REQUEST_CONTEXT_KEY = 'vendureRequestContext';
@@ -66,7 +66,7 @@ export class RequestContextService {
     }
 
     private getLanguageCode(req: Request): LanguageCode | undefined {
-        return req.body && req.body.variables && req.body.variables.languageCode;
+        return req.query && req.query.languageCode;
     }
 
     private isAuthenticatedSession(session?: Session): session is AuthenticatedSession {
@@ -91,8 +91,11 @@ export class RequestContextService {
      * Returns true if any element of arr1 appears in arr2.
      */
     private arraysIntersect<T>(arr1: T[], arr2: T[]): boolean {
-        return arr1.reduce((intersects, role) => {
-            return intersects || arr2.includes(role);
-        }, false as boolean);
+        return arr1.reduce(
+            (intersects, role) => {
+                return intersects || arr2.includes(role);
+            },
+            false as boolean,
+        );
     }
 }

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

@@ -1,6 +1,6 @@
 type Query {
-    collections(languageCode: LanguageCode, options: CollectionListOptions): CollectionList!
-    collection(id: ID!, languageCode: LanguageCode): Collection
+    collections(options: CollectionListOptions): CollectionList!
+    collection(id: ID!): Collection
     collectionFilters: [ConfigurableOperation!]!
 }
 

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

@@ -1,6 +1,6 @@
 type Query {
-    facets(languageCode: LanguageCode, options: FacetListOptions): FacetList!
-    facet(id: ID!, languageCode: LanguageCode): Facet
+    facets(options: FacetListOptions): FacetList!
+    facet(id: ID!): Facet
 }
 
 type Mutation {

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

@@ -1,6 +1,6 @@
 type Query {
-    productOptionGroups(languageCode: LanguageCode, filterTerm: String): [ProductOptionGroup!]!
-    productOptionGroup(id: ID!, languageCode: LanguageCode): ProductOptionGroup
+    productOptionGroups(filterTerm: String): [ProductOptionGroup!]!
+    productOptionGroup(id: ID!): ProductOptionGroup
 }
 
 type Mutation {

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

@@ -1,7 +1,7 @@
 type Query {
-    products(languageCode: LanguageCode, options: ProductListOptions): ProductList!
+    products(options: ProductListOptions): ProductList!
     "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
+    product(id: ID, slug: String): Product
 }
 
 type Mutation {

+ 3 - 6
packages/core/src/service/helpers/utils/translate-entity.spec.ts

@@ -1,5 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 
+import { DEFAULT_LANGUAGE_CODE } from '../../../common/constants';
 import { Translatable, Translation } from '../../../common/types/locale-types';
 import { CollectionTranslation } from '../../../entity/collection/collection-translation.entity';
 import { Collection } from '../../../entity/collection/collection.entity';
@@ -123,12 +124,8 @@ describe('translateEntity()', () => {
         );
     });
 
-    it('throw if the desired translation is not available', () => {
-        product.translations = [];
-
-        expect(() => translateEntity(product, LanguageCode.zu)).toThrow(
-            'error.entity-has-no-translation-in-language',
-        );
+    it('falls back to default language', () => {
+        expect(translateEntity(product, LanguageCode.zu).name).toEqual(PRODUCT_NAME_EN);
     });
 });
 

+ 15 - 6
packages/core/src/service/helpers/utils/translate-entity.ts

@@ -1,9 +1,9 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 
-import { UnwrappedArray } from '../../../common/types/common-types';
-
+import { DEFAULT_LANGUAGE_CODE } from '../../../common/constants';
 import { InternalServerError } from '../../../common/error/errors';
-import { Translatable, Translated } from '../../../common/types/locale-types';
+import { UnwrappedArray } from '../../../common/types/common-types';
+import { Translatable, Translated, Translation } from '../../../common/types/locale-types';
 
 // prettier-ignore
 export type TranslatableRelationsKeys<T> = {
@@ -38,8 +38,13 @@ export function translateEntity<T extends Translatable>(
     translatable: T,
     languageCode: LanguageCode,
 ): Translated<T> {
-    const translation =
-        translatable.translations && translatable.translations.find(t => t.languageCode === languageCode);
+    let translation: Translation<any> | undefined;
+    if (translatable.translations) {
+        translation = translatable.translations.find(t => t.languageCode === languageCode);
+        if (!translation && languageCode !== DEFAULT_LANGUAGE_CODE) {
+            translation = translatable.translations.find(t => t.languageCode === DEFAULT_LANGUAGE_CODE);
+        }
+    }
 
     if (!translation) {
         throw new InternalServerError(`error.entity-has-no-translation-in-language`, {
@@ -112,7 +117,11 @@ export function translateDeep<T extends Translatable>(
     return translatedEntity;
 }
 
-function translateLeaf(object: { [key: string]: any } | undefined, property: string, languageCode: LanguageCode): any {
+function translateLeaf(
+    object: { [key: string]: any } | undefined,
+    property: string,
+    languageCode: LanguageCode,
+): any {
     if (object && object[property]) {
         if (Array.isArray(object[property])) {
             return object[property].map((nested2: any) => translateEntity(nested2, languageCode));

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


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