Browse Source

feat(core): Implement create and update of ProductOption

Michael Bromley 6 years ago
parent
commit
601c76605f

+ 65 - 38
packages/common/src/generated-types.ts

@@ -458,6 +458,11 @@ export type CreateFacetValueWithFacetInput = {
   translations: Array<FacetValueTranslationInput>,
 };
 
+export type CreateGroupOptionInput = {
+  code: Scalars['String'],
+  translations: Array<ProductOptionGroupTranslationInput>,
+};
+
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
@@ -469,11 +474,12 @@ export type CreateProductInput = {
 export type CreateProductOptionGroupInput = {
   code: Scalars['String'],
   translations: Array<ProductOptionGroupTranslationInput>,
-  options: Array<CreateProductOptionInput>,
+  options: Array<CreateGroupOptionInput>,
   customFields?: Maybe<Scalars['JSON']>,
 };
 
 export type CreateProductOptionInput = {
+  productOptionGroupId: Scalars['ID'],
   code: Scalars['String'],
   translations: Array<ProductOptionGroupTranslationInput>,
   customFields?: Maybe<Scalars['JSON']>,
@@ -1564,6 +1570,14 @@ export type Mutation = {
   updateCountry: Country,
   /** Delete a Country */
   deleteCountry: DeletionResponse,
+  /** Create a new CustomerGroup */
+  createCustomerGroup: CustomerGroup,
+  /** Update an existing CustomerGroup */
+  updateCustomerGroup: CustomerGroup,
+  /** Add Customers to a CustomerGroup */
+  addCustomersToGroup: CustomerGroup,
+  /** Remove Customers from a CustomerGroup */
+  removeCustomersFromGroup: CustomerGroup,
   /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
   createCustomer: Customer,
   /** Update an existing Customer */
@@ -1576,14 +1590,6 @@ export type Mutation = {
   updateCustomerAddress: Address,
   /** Update an existing Address */
   deleteCustomerAddress: Scalars['Boolean'],
-  /** Create a new CustomerGroup */
-  createCustomerGroup: CustomerGroup,
-  /** Update an existing CustomerGroup */
-  updateCustomerGroup: CustomerGroup,
-  /** Add Customers to a CustomerGroup */
-  addCustomersToGroup: CustomerGroup,
-  /** Remove Customers from a CustomerGroup */
-  removeCustomersFromGroup: CustomerGroup,
   /** Create a new Facet */
   createFacet: Facet,
   /** Update an existing Facet */
@@ -1610,6 +1616,10 @@ export type Mutation = {
   createProductOptionGroup: ProductOptionGroup,
   /** Update an existing ProductOptionGroup */
   updateProductOptionGroup: ProductOptionGroup,
+  /** Create a new ProductOption within a ProductOptionGroup */
+  createProductOption: ProductOption,
+  /** Create a new ProductOption within a ProductOptionGroup */
+  updateProductOption: ProductOption,
   reindex: JobInfo,
   /** Create a new Product */
   createProduct: Product,
@@ -1727,6 +1737,28 @@ export type MutationDeleteCountryArgs = {
 };
 
 
+export type MutationCreateCustomerGroupArgs = {
+  input: CreateCustomerGroupInput
+};
+
+
+export type MutationUpdateCustomerGroupArgs = {
+  input: UpdateCustomerGroupInput
+};
+
+
+export type MutationAddCustomersToGroupArgs = {
+  customerGroupId: Scalars['ID'],
+  customerIds: Array<Scalars['ID']>
+};
+
+
+export type MutationRemoveCustomersFromGroupArgs = {
+  customerGroupId: Scalars['ID'],
+  customerIds: Array<Scalars['ID']>
+};
+
+
 export type MutationCreateCustomerArgs = {
   input: CreateCustomerInput,
   password?: Maybe<Scalars['String']>
@@ -1759,28 +1791,6 @@ export type MutationDeleteCustomerAddressArgs = {
 };
 
 
-export type MutationCreateCustomerGroupArgs = {
-  input: CreateCustomerGroupInput
-};
-
-
-export type MutationUpdateCustomerGroupArgs = {
-  input: UpdateCustomerGroupInput
-};
-
-
-export type MutationAddCustomersToGroupArgs = {
-  customerGroupId: Scalars['ID'],
-  customerIds: Array<Scalars['ID']>
-};
-
-
-export type MutationRemoveCustomersFromGroupArgs = {
-  customerGroupId: Scalars['ID'],
-  customerIds: Array<Scalars['ID']>
-};
-
-
 export type MutationCreateFacetArgs = {
   input: CreateFacetInput
 };
@@ -1868,6 +1878,16 @@ export type MutationUpdateProductOptionGroupArgs = {
 };
 
 
+export type MutationCreateProductOptionArgs = {
+  input: CreateProductOptionInput
+};
+
+
+export type MutationUpdateProductOptionArgs = {
+  input: UpdateProductOptionInput
+};
+
+
 export type MutationCreateProductArgs = {
   input: CreateProductInput
 };
@@ -2500,10 +2520,10 @@ export type Query = {
   collectionFilters: Array<ConfigurableOperation>,
   countries: CountryList,
   country?: Maybe<Country>,
-  customers: CustomerList,
-  customer?: Maybe<Customer>,
   customerGroups: Array<CustomerGroup>,
   customerGroup?: Maybe<CustomerGroup>,
+  customers: CustomerList,
+  customer?: Maybe<Customer>,
   facets: FacetList,
   facet?: Maybe<Facet>,
   globalSettings: GlobalSettings,
@@ -2584,17 +2604,17 @@ export type QueryCountryArgs = {
 };
 
 
-export type QueryCustomersArgs = {
-  options?: Maybe<CustomerListOptions>
+export type QueryCustomerGroupArgs = {
+  id: Scalars['ID']
 };
 
 
-export type QueryCustomerArgs = {
-  id: Scalars['ID']
+export type QueryCustomersArgs = {
+  options?: Maybe<CustomerListOptions>
 };
 
 
-export type QueryCustomerGroupArgs = {
+export type QueryCustomerArgs = {
   id: Scalars['ID']
 };
 
@@ -3140,6 +3160,13 @@ export type UpdateProductOptionGroupInput = {
   customFields?: Maybe<Scalars['JSON']>,
 };
 
+export type UpdateProductOptionInput = {
+  id: Scalars['ID'],
+  code?: Maybe<Scalars['String']>,
+  translations?: Maybe<Array<ProductOptionGroupTranslationInput>>,
+  customFields?: Maybe<Scalars['JSON']>,
+};
+
 export type UpdateProductVariantInput = {
   id: Scalars['ID'],
   enabled?: Maybe<Scalars['Boolean']>,

+ 4 - 4
packages/core/e2e/fixtures/e2e-products-minimal.csv

@@ -1,5 +1,5 @@
 name   , slug   , description                                                                                                                                                                                                                                                                  , assets                           , facets                                  , optionGroups       , optionValues    , sku      , price   , taxCategory , stockOnHand , trackInventory , variantAssets , variantFacets
-Laptop , laptop , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz." , derick-david-409858-unsplash.jpg , category:electronics|category:computers , "screen size, RAM" , "13 inch, 8GB"  , L2201308 , 1299.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch, 8GB"  , L2201508 , 1399.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "13 inch, 16GB" , L2201316 , 2199.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch, 16GB" , L2201516 , 2299.00 , standard    , 100         , false          ,               ,
+Laptop , laptop , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz." , derick-david-409858-unsplash.jpg , category:electronics|category:computers , "screen size|RAM"  , "13 inch|8GB"   , L2201308 , 1299.00 , standard    , 100         , false          ,               ,
+       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch|8GB"   , L2201508 , 1399.00 , standard    , 100         , false          ,               ,
+       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "13 inch|16GB"  , L2201316 , 2199.00 , standard    , 100         , false          ,               ,
+       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch|16GB"  , L2201516 , 2299.00 , standard    , 100         , false          ,               ,

+ 149 - 33
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -458,6 +458,11 @@ export type CreateFacetValueWithFacetInput = {
     translations: Array<FacetValueTranslationInput>;
 };
 
+export type CreateGroupOptionInput = {
+    code: Scalars['String'];
+    translations: Array<ProductOptionGroupTranslationInput>;
+};
+
 export type CreateProductInput = {
     featuredAssetId?: Maybe<Scalars['ID']>;
     assetIds?: Maybe<Array<Scalars['ID']>>;
@@ -469,11 +474,12 @@ export type CreateProductInput = {
 export type CreateProductOptionGroupInput = {
     code: Scalars['String'];
     translations: Array<ProductOptionGroupTranslationInput>;
-    options: Array<CreateProductOptionInput>;
+    options: Array<CreateGroupOptionInput>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CreateProductOptionInput = {
+    productOptionGroupId: Scalars['ID'];
     code: Scalars['String'];
     translations: Array<ProductOptionGroupTranslationInput>;
     customFields?: Maybe<Scalars['JSON']>;
@@ -1561,6 +1567,14 @@ export type Mutation = {
     updateCountry: Country;
     /** Delete a Country */
     deleteCountry: DeletionResponse;
+    /** Create a new CustomerGroup */
+    createCustomerGroup: CustomerGroup;
+    /** Update an existing CustomerGroup */
+    updateCustomerGroup: CustomerGroup;
+    /** Add Customers to a CustomerGroup */
+    addCustomersToGroup: CustomerGroup;
+    /** Remove Customers from a CustomerGroup */
+    removeCustomersFromGroup: CustomerGroup;
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
     createCustomer: Customer;
     /** Update an existing Customer */
@@ -1573,14 +1587,6 @@ export type Mutation = {
     updateCustomerAddress: Address;
     /** Update an existing Address */
     deleteCustomerAddress: Scalars['Boolean'];
-    /** Create a new CustomerGroup */
-    createCustomerGroup: CustomerGroup;
-    /** Update an existing CustomerGroup */
-    updateCustomerGroup: CustomerGroup;
-    /** Add Customers to a CustomerGroup */
-    addCustomersToGroup: CustomerGroup;
-    /** Remove Customers from a CustomerGroup */
-    removeCustomersFromGroup: CustomerGroup;
     /** Create a new Facet */
     createFacet: Facet;
     /** Update an existing Facet */
@@ -1607,6 +1613,10 @@ export type Mutation = {
     createProductOptionGroup: ProductOptionGroup;
     /** Update an existing ProductOptionGroup */
     updateProductOptionGroup: ProductOptionGroup;
+    /** Create a new ProductOption within a ProductOptionGroup */
+    createProductOption: ProductOption;
+    /** Create a new ProductOption within a ProductOptionGroup */
+    updateProductOption: ProductOption;
     reindex: JobInfo;
     /** Create a new Product */
     createProduct: Product;
@@ -1710,6 +1720,24 @@ export type MutationDeleteCountryArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationCreateCustomerGroupArgs = {
+    input: CreateCustomerGroupInput;
+};
+
+export type MutationUpdateCustomerGroupArgs = {
+    input: UpdateCustomerGroupInput;
+};
+
+export type MutationAddCustomersToGroupArgs = {
+    customerGroupId: Scalars['ID'];
+    customerIds: Array<Scalars['ID']>;
+};
+
+export type MutationRemoveCustomersFromGroupArgs = {
+    customerGroupId: Scalars['ID'];
+    customerIds: Array<Scalars['ID']>;
+};
+
 export type MutationCreateCustomerArgs = {
     input: CreateCustomerInput;
     password?: Maybe<Scalars['String']>;
@@ -1736,24 +1764,6 @@ export type MutationDeleteCustomerAddressArgs = {
     id: Scalars['ID'];
 };
 
-export type MutationCreateCustomerGroupArgs = {
-    input: CreateCustomerGroupInput;
-};
-
-export type MutationUpdateCustomerGroupArgs = {
-    input: UpdateCustomerGroupInput;
-};
-
-export type MutationAddCustomersToGroupArgs = {
-    customerGroupId: Scalars['ID'];
-    customerIds: Array<Scalars['ID']>;
-};
-
-export type MutationRemoveCustomersFromGroupArgs = {
-    customerGroupId: Scalars['ID'];
-    customerIds: Array<Scalars['ID']>;
-};
-
 export type MutationCreateFacetArgs = {
     input: CreateFacetInput;
 };
@@ -1824,6 +1834,14 @@ export type MutationUpdateProductOptionGroupArgs = {
     input: UpdateProductOptionGroupInput;
 };
 
+export type MutationCreateProductOptionArgs = {
+    input: CreateProductOptionInput;
+};
+
+export type MutationUpdateProductOptionArgs = {
+    input: UpdateProductOptionInput;
+};
+
 export type MutationCreateProductArgs = {
     input: CreateProductInput;
 };
@@ -2431,10 +2449,10 @@ export type Query = {
     collectionFilters: Array<ConfigurableOperation>;
     countries: CountryList;
     country?: Maybe<Country>;
-    customers: CustomerList;
-    customer?: Maybe<Customer>;
     customerGroups: Array<CustomerGroup>;
     customerGroup?: Maybe<CustomerGroup>;
+    customers: CustomerList;
+    customer?: Maybe<Customer>;
     facets: FacetList;
     facet?: Maybe<Facet>;
     globalSettings: GlobalSettings;
@@ -2505,6 +2523,10 @@ export type QueryCountryArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryCustomerGroupArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryCustomersArgs = {
     options?: Maybe<CustomerListOptions>;
 };
@@ -2513,10 +2535,6 @@ export type QueryCustomerArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryCustomerGroupArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryFacetsArgs = {
     languageCode?: Maybe<LanguageCode>;
     options?: Maybe<FacetListOptions>;
@@ -3039,6 +3057,13 @@ export type UpdateProductOptionGroupInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type UpdateProductOptionInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<ProductOptionGroupTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
 export type UpdateProductVariantInput = {
     id: Scalars['ID'];
     enabled?: Maybe<Scalars['Boolean']>;
@@ -4204,6 +4229,64 @@ export type AddNoteToOrderMutation = { __typename?: 'Mutation' } & {
     addNoteToOrder: { __typename?: 'Order' } & Pick<Order, 'id'>;
 };
 
+export type ProductOptionGroupFragment = { __typename?: 'ProductOptionGroup' } & Pick<
+    ProductOptionGroup,
+    'id' | 'code' | 'name'
+> & {
+        options: Array<{ __typename?: 'ProductOption' } & Pick<ProductOption, 'id' | 'code' | 'name'>>;
+        translations: Array<
+            { __typename?: 'ProductOptionGroupTranslation' } & Pick<
+                ProductOptionGroupTranslation,
+                'id' | 'languageCode' | 'name'
+            >
+        >;
+    };
+
+export type CreateProductOptionGroupMutationVariables = {
+    input: CreateProductOptionGroupInput;
+};
+
+export type CreateProductOptionGroupMutation = { __typename?: 'Mutation' } & {
+    createProductOptionGroup: { __typename?: 'ProductOptionGroup' } & ProductOptionGroupFragment;
+};
+
+export type UpdateProductOptionGroupMutationVariables = {
+    input: UpdateProductOptionGroupInput;
+};
+
+export type UpdateProductOptionGroupMutation = { __typename?: 'Mutation' } & {
+    updateProductOptionGroup: { __typename?: 'ProductOptionGroup' } & ProductOptionGroupFragment;
+};
+
+export type CreateProductOptionMutationVariables = {
+    input: CreateProductOptionInput;
+};
+
+export type CreateProductOptionMutation = { __typename?: 'Mutation' } & {
+    createProductOption: { __typename?: 'ProductOption' } & Pick<
+        ProductOption,
+        'id' | 'code' | 'name' | 'groupId'
+    > & {
+            translations: Array<
+                { __typename?: 'ProductOptionTranslation' } & Pick<
+                    ProductOptionTranslation,
+                    'id' | 'languageCode' | 'name'
+                >
+            >;
+        };
+};
+
+export type UpdateProductOptionMutationVariables = {
+    input: UpdateProductOptionInput;
+};
+
+export type UpdateProductOptionMutation = { __typename?: 'Mutation' } & {
+    updateProductOption: { __typename?: 'ProductOption' } & Pick<
+        ProductOption,
+        'id' | 'code' | 'name' | 'groupId'
+    >;
+};
+
 export type AddOptionGroupToProductMutationVariables = {
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -5225,6 +5308,39 @@ export namespace AddNoteToOrder {
     export type AddNoteToOrder = AddNoteToOrderMutation['addNoteToOrder'];
 }
 
+export namespace ProductOptionGroup {
+    export type Fragment = ProductOptionGroupFragment;
+    export type Options = NonNullable<ProductOptionGroupFragment['options'][0]>;
+    export type Translations = NonNullable<ProductOptionGroupFragment['translations'][0]>;
+}
+
+export namespace CreateProductOptionGroup {
+    export type Variables = CreateProductOptionGroupMutationVariables;
+    export type Mutation = CreateProductOptionGroupMutation;
+    export type CreateProductOptionGroup = ProductOptionGroupFragment;
+}
+
+export namespace UpdateProductOptionGroup {
+    export type Variables = UpdateProductOptionGroupMutationVariables;
+    export type Mutation = UpdateProductOptionGroupMutation;
+    export type UpdateProductOptionGroup = ProductOptionGroupFragment;
+}
+
+export namespace CreateProductOption {
+    export type Variables = CreateProductOptionMutationVariables;
+    export type Mutation = CreateProductOptionMutation;
+    export type CreateProductOption = CreateProductOptionMutation['createProductOption'];
+    export type Translations = NonNullable<
+        CreateProductOptionMutation['createProductOption']['translations'][0]
+    >;
+}
+
+export namespace UpdateProductOption {
+    export type Variables = UpdateProductOptionMutationVariables;
+    export type Mutation = UpdateProductOptionMutation;
+    export type UpdateProductOption = UpdateProductOptionMutation['updateProductOption'];
+}
+
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;

+ 211 - 0
packages/core/e2e/product-option.e2e-spec.ts

@@ -0,0 +1,211 @@
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { omit } from '../../common/lib/omit';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import {
+    CreateProductOption,
+    CreateProductOptionGroup,
+    LanguageCode,
+    ProductOptionGroupFragment,
+    UpdateProductOption,
+    UpdateProductOptionGroup,
+} from './graphql/generated-e2e-admin-types';
+import { TestAdminClient } from './test-client';
+import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
+
+// tslint:disable:no-non-null-assertion
+
+describe('ProductOption resolver', () => {
+    const client = new TestAdminClient();
+    const server = new TestServer();
+    let sizeGroup: ProductOptionGroupFragment;
+    let mediumOption: CreateProductOption.CreateProductOption;
+
+    beforeAll(async () => {
+        const token = await server.init({
+            customerCount: 1,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+        });
+        await client.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('createProductOptionGroup', async () => {
+        const { createProductOptionGroup } = await client.query<
+            CreateProductOptionGroup.Mutation,
+            CreateProductOptionGroup.Variables
+        >(CREATE_PRODUCT_OPTION_GROUP, {
+            input: {
+                code: 'size',
+                translations: [
+                    { languageCode: LanguageCode.en, name: 'Size' },
+                    { languageCode: LanguageCode.de, name: 'Größe' },
+                ],
+                options: [
+                    {
+                        code: 'small',
+                        translations: [
+                            { languageCode: LanguageCode.en, name: 'Small' },
+                            { languageCode: LanguageCode.de, name: 'Klein' },
+                        ],
+                    },
+                    {
+                        code: 'large',
+                        translations: [
+                            { languageCode: LanguageCode.en, name: 'Large' },
+                            { languageCode: LanguageCode.de, name: 'Groß' },
+                        ],
+                    },
+                ],
+            },
+        });
+
+        expect(omit(createProductOptionGroup, ['options', 'translations'])).toEqual({
+            id: 'T_3',
+            name: 'Size',
+            code: 'size',
+        });
+        sizeGroup = createProductOptionGroup;
+    });
+
+    it('updateProductOptionGroup', async () => {
+        const { updateProductOptionGroup } = await client.query<
+            UpdateProductOptionGroup.Mutation,
+            UpdateProductOptionGroup.Variables
+        >(UPDATE_PRODUCT_OPTION_GROUP, {
+            input: {
+                id: sizeGroup.id,
+                translations: [
+                    { id: sizeGroup.translations[0].id, languageCode: LanguageCode.en, name: 'Bigness' },
+                ],
+            },
+        });
+
+        expect(updateProductOptionGroup.name).toBe('Bigness');
+    });
+
+    it(
+        'createProductOption throws with invalid productOptionGroupId',
+        assertThrowsWithMessage(async () => {
+            const { createProductOption } = await client.query<
+                CreateProductOption.Mutation,
+                CreateProductOption.Variables
+            >(CREATE_PRODUCT_OPTION, {
+                input: {
+                    productOptionGroupId: 'T_999',
+                    code: 'medium',
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'Medium' },
+                        { languageCode: LanguageCode.de, name: 'Mittel' },
+                    ],
+                },
+            });
+        }, 'No ProductOptionGroup with the id \'999\' could be found'),
+    );
+
+    it('createProductOption', async () => {
+        const { createProductOption } = await client.query<
+            CreateProductOption.Mutation,
+            CreateProductOption.Variables
+        >(CREATE_PRODUCT_OPTION, {
+            input: {
+                productOptionGroupId: sizeGroup.id,
+                code: 'medium',
+                translations: [
+                    { languageCode: LanguageCode.en, name: 'Medium' },
+                    { languageCode: LanguageCode.de, name: 'Mittel' },
+                ],
+            },
+        });
+
+        expect(omit(createProductOption, ['translations'])).toEqual({
+            id: 'T_7',
+            groupId: sizeGroup.id,
+            code: 'medium',
+            name: 'Medium',
+        });
+        mediumOption = createProductOption;
+    });
+
+    it('updateProductOption', async () => {
+        const { updateProductOption } = await client.query<UpdateProductOption.Mutation, UpdateProductOption.Variables>(UPDATE_PRODUCT_OPTION, {
+            input: {
+                id: 'T_7',
+                translations: [
+                    { id: mediumOption.translations[0].id, languageCode: LanguageCode.en, name: 'Middling' },
+                ],
+            },
+        });
+
+        expect(updateProductOption.name).toBe('Middling');
+    });
+});
+
+const PRODUCT_OPTION_GROUP_FRAGMENT = gql`
+    fragment ProductOptionGroup on ProductOptionGroup {
+        id
+        code
+        name
+        options {
+            id
+            code
+            name
+        }
+        translations {
+            id
+            languageCode
+            name
+        }
+    }
+`;
+
+const CREATE_PRODUCT_OPTION_GROUP = gql`
+    mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
+        createProductOptionGroup(input: $input) {
+            ...ProductOptionGroup
+        }
+    }
+    ${PRODUCT_OPTION_GROUP_FRAGMENT}
+`;
+
+const UPDATE_PRODUCT_OPTION_GROUP = gql`
+    mutation UpdateProductOptionGroup($input: UpdateProductOptionGroupInput!) {
+        updateProductOptionGroup(input: $input) {
+            ...ProductOptionGroup
+        }
+    }
+    ${PRODUCT_OPTION_GROUP_FRAGMENT}
+`;
+
+const CREATE_PRODUCT_OPTION = gql`
+    mutation CreateProductOption($input: CreateProductOptionInput!) {
+        createProductOption(input: $input) {
+            id
+            code
+            name
+            groupId
+            translations {
+                id
+                languageCode
+                name
+            }
+        }
+    }
+`;
+
+const UPDATE_PRODUCT_OPTION = gql`
+    mutation UpdateProductOption($input: UpdateProductOptionInput!) {
+        updateProductOption(input: $input) {
+            id
+            code
+            name
+            groupId
+        }
+    }
+`;

+ 1 - 0
packages/core/src/api/common/id-codec.ts

@@ -12,6 +12,7 @@ const ID_KEYS = [
     'fulfillmentId',
     'orderItemIds',
     'refundId',
+    'groupId',
 ];
 
 /**

+ 35 - 7
packages/core/src/api/resolvers/admin/product-option.resolver.ts

@@ -1,21 +1,25 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    MutationCreateProductOptionArgs,
     MutationCreateProductOptionGroupArgs,
+    MutationUpdateProductOptionArgs,
+    MutationUpdateProductOptionGroupArgs,
     Permission,
     QueryProductOptionGroupArgs,
     QueryProductOptionGroupsArgs,
-    MutationUpdateProductOptionGroupArgs,
 } from '@vendure/common/lib/generated-types';
 
 import { Translated } from '../../../common/types/locale-types';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
+import { ProductOption } from '../../../entity/product-option/product-option.entity';
 import { ProductOptionGroupService } from '../../../service/services/product-option-group.service';
 import { ProductOptionService } from '../../../service/services/product-option.service';
 import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
+import { Decode } from '../../decorators/decode.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
-@Resolver('ProductOptionGroup')
+@Resolver()
 export class ProductOptionResolver {
     constructor(
         private productOptionGroupService: ProductOptionGroupService,
@@ -28,7 +32,7 @@ export class ProductOptionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryProductOptionGroupsArgs,
     ): Promise<Array<Translated<ProductOptionGroup>>> {
-        return this.productOptionGroupService.findAll(ctx.languageCode, args.filterTerm || undefined);
+        return this.productOptionGroupService.findAll(ctx, args.filterTerm || undefined);
     }
 
     @Query()
@@ -37,20 +41,21 @@ export class ProductOptionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: QueryProductOptionGroupArgs,
     ): Promise<Translated<ProductOptionGroup> | undefined> {
-        return this.productOptionGroupService.findOne(args.id, ctx.languageCode);
+        return this.productOptionGroupService.findOne(ctx, args.id);
     }
 
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createProductOptionGroup(
+        @Ctx() ctx: RequestContext,
         @Args() args: MutationCreateProductOptionGroupArgs,
     ): Promise<Translated<ProductOptionGroup>> {
         const { input } = args;
-        const group = await this.productOptionGroupService.create(args.input);
+        const group = await this.productOptionGroupService.create(ctx, input);
 
         if (input.options && input.options.length) {
             for (const option of input.options) {
-                const newOption = await this.productOptionService.create(group, option);
+                const newOption = await this.productOptionService.create(ctx, group, option);
                 group.options.push(newOption);
             }
         }
@@ -60,9 +65,32 @@ export class ProductOptionResolver {
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateProductOptionGroup(
+        @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdateProductOptionGroupArgs,
     ): Promise<Translated<ProductOptionGroup>> {
         const { input } = args;
-        return this.productOptionGroupService.update(args.input);
+        return this.productOptionGroupService.update(ctx, input);
+    }
+
+    @Mutation()
+    @Allow(Permission.CreateCatalog)
+    @Decode('productOptionGroupId')
+    async createProductOption(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationCreateProductOptionArgs,
+    ): Promise<Translated<ProductOption>> {
+        const { input } = args;
+        return this.productOptionService.create(ctx, input.productOptionGroupId, input);
+    }
+
+    @Mutation()
+    @Allow(Permission.UpdateCatalog)
+    @Decode('productOptionGroupId')
+    async updateProductOption(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationUpdateProductOptionArgs,
+    ): Promise<Translated<ProductOption>> {
+        const { input } = args;
+        return this.productOptionService.update(ctx, input);
     }
 }

+ 5 - 3
packages/core/src/api/resolvers/entity/product-option-group-entity.resolver.ts

@@ -1,11 +1,13 @@
-import { ResolveProperty, Resolver } from '@nestjs/graphql';
+import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 import { Permission } from '@vendure/common/lib/generated-types';
 
 import { Translated } from '../../../common/types/locale-types';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { ProductOption } from '../../../entity/product-option/product-option.entity';
 import { ProductOptionGroupService } from '../../../service/services/product-option-group.service';
+import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('ProductOptionGroup')
 export class ProductOptionGroupEntityResolver {
@@ -13,11 +15,11 @@ export class ProductOptionGroupEntityResolver {
 
     @ResolveProperty()
     @Allow(Permission.ReadCatalog, Permission.Public)
-    async options(optionGroup: Translated<ProductOptionGroup>): Promise<Array<Translated<ProductOption>>> {
+    async options(@Ctx() ctx: RequestContext, @Parent() optionGroup: Translated<ProductOptionGroup>): Promise<Array<Translated<ProductOption>>> {
         if (optionGroup.options) {
             return Promise.resolve(optionGroup.options);
         }
-        const group = await this.productOptionGroupService.findOne(optionGroup.id, optionGroup.languageCode);
+        const group = await this.productOptionGroupService.findOne(ctx, optionGroup.id);
         return group ? group.options : [];
     }
 }

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

@@ -8,6 +8,10 @@ type Mutation {
     createProductOptionGroup(input: CreateProductOptionGroupInput!): ProductOptionGroup!
     "Update an existing ProductOptionGroup"
     updateProductOptionGroup(input: UpdateProductOptionGroupInput!): ProductOptionGroup!
+    "Create a new ProductOption within a ProductOptionGroup"
+    createProductOption(input: CreateProductOptionInput!): ProductOption!
+    "Create a new ProductOption within a ProductOptionGroup"
+    updateProductOption(input: UpdateProductOptionInput!): ProductOption!
 }
 
 input ProductOptionGroupTranslationInput {
@@ -19,7 +23,7 @@ input ProductOptionGroupTranslationInput {
 input CreateProductOptionGroupInput {
     code: String!
     translations: [ProductOptionGroupTranslationInput!]!
-    options: [CreateProductOptionInput!]!
+    options: [CreateGroupOptionInput!]!
 }
 
 input UpdateProductOptionGroupInput {
@@ -34,7 +38,19 @@ input ProductOptionTranslationInput {
     name: String
 }
 
+input CreateGroupOptionInput {
+    code: String!
+    translations: [ProductOptionGroupTranslationInput!]!
+}
+
 input CreateProductOptionInput {
+    productOptionGroupId: ID!
     code: String!
     translations: [ProductOptionGroupTranslationInput!]!
 }
+
+input UpdateProductOptionInput {
+    id: ID!
+    code: String
+    translations: [ProductOptionGroupTranslationInput!]
+}

+ 2 - 2
packages/core/src/data-import/providers/importer/importer.ts

@@ -173,7 +173,7 @@ export class Importer {
             const optionsMap: { [optionName: string]: string } = {};
             for (const optionGroup of product.optionGroups) {
                 const code = normalizeString(`${product.name}-${optionGroup.name}`, '-');
-                const group = await this.productOptionGroupService.create({
+                const group = await this.productOptionGroupService.create(ctx, {
                     code,
                     options: optionGroup.values.map(name => ({} as any)),
                     translations: [
@@ -184,7 +184,7 @@ export class Importer {
                     ],
                 });
                 for (const option of optionGroup.values) {
-                    const createdOption = await this.productOptionService.create(group, {
+                    const createdOption = await this.productOptionService.create(ctx, group, {
                         code: normalizeString(option, '-'),
                         translations: [
                             {

+ 10 - 14
packages/core/src/service/services/product-option-group.service.ts

@@ -1,14 +1,10 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
-import {
-    CreateProductOptionGroupInput,
-    LanguageCode,
-    UpdateProductOptionGroupInput,
-} from '@vendure/common/lib/generated-types';
+import { CreateProductOptionGroupInput, UpdateProductOptionGroupInput } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { Connection, FindManyOptions, Like } from 'typeorm';
 
-import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { RequestContext } from '../../api/common/request-context';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { ProductOptionGroupTranslation } from '../../entity/product-option-group/product-option-group-translation.entity';
@@ -23,7 +19,7 @@ export class ProductOptionGroupService {
         private translatableSaver: TranslatableSaver,
     ) {}
 
-    findAll(lang: LanguageCode, filterTerm?: string): Promise<Array<Translated<ProductOptionGroup>>> {
+    findAll(ctx: RequestContext, filterTerm?: string): Promise<Array<Translated<ProductOptionGroup>>> {
         const findOptions: FindManyOptions = {
             relations: ['options'],
         };
@@ -34,32 +30,32 @@ export class ProductOptionGroupService {
         }
         return this.connection.manager
             .find(ProductOptionGroup, findOptions)
-            .then(groups => groups.map(group => translateDeep(group, lang, ['options'])));
+            .then(groups => groups.map(group => translateDeep(group, ctx.languageCode, ['options'])));
     }
 
-    findOne(id: ID, lang: LanguageCode): Promise<Translated<ProductOptionGroup> | undefined> {
+    findOne(ctx: RequestContext, id: ID): Promise<Translated<ProductOptionGroup> | undefined> {
         return this.connection.manager
             .findOne(ProductOptionGroup, id, {
                 relations: ['options'],
             })
-            .then(group => group && translateDeep(group, lang, ['options']));
+            .then(group => group && translateDeep(group, ctx.languageCode, ['options']));
     }
 
-    async create(input: CreateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
+    async create(ctx: RequestContext, input: CreateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
         const group = await this.translatableSaver.create({
             input,
             entityType: ProductOptionGroup,
             translationType: ProductOptionGroupTranslation,
         });
-        return assertFound(this.findOne(group.id, DEFAULT_LANGUAGE_CODE));
+        return assertFound(this.findOne(ctx, group.id));
     }
 
-    async update(input: UpdateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
+    async update(ctx: RequestContext, input: UpdateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
         const group = await this.translatableSaver.update({
             input,
             entityType: ProductOptionGroup,
             translationType: ProductOptionGroupTranslation,
         });
-        return assertFound(this.findOne(group.id, DEFAULT_LANGUAGE_CODE));
+        return assertFound(this.findOne(ctx, group.id));
     }
 }

+ 25 - 9
packages/core/src/service/services/product-option.service.ts

@@ -1,9 +1,10 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
-import { CreateProductOptionInput, LanguageCode } from '@vendure/common/lib/generated-types';
+import { CreateGroupOptionInput, CreateProductOptionInput, LanguageCode, UpdateProductOptionInput } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 
+import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
@@ -11,6 +12,7 @@ import { ProductOptionGroup } from '../../entity/product-option-group/product-op
 import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
@@ -20,32 +22,46 @@ export class ProductOptionService {
         private translatableSaver: TranslatableSaver,
     ) {}
 
-    findAll(lang: LanguageCode): Promise<Array<Translated<ProductOption>>> {
+    findAll(ctx: RequestContext): Promise<Array<Translated<ProductOption>>> {
         return this.connection.manager
             .find(ProductOption, {
                 relations: ['group'],
             })
-            .then(options => options.map(option => translateDeep(option, lang)));
+            .then(options => options.map(option => translateDeep(option, ctx.languageCode)));
     }
 
-    findOne(id: ID, lang: LanguageCode): Promise<Translated<ProductOption> | undefined> {
+    findOne(ctx: RequestContext, id: ID): Promise<Translated<ProductOption> | undefined> {
         return this.connection.manager
             .findOne(ProductOption, id, {
                 relations: ['group'],
             })
-            .then(option => option && translateDeep(option, lang));
+            .then(option => option && translateDeep(option, ctx.languageCode));
     }
 
     async create(
-        group: ProductOptionGroup,
-        input: CreateProductOptionInput,
+        ctx: RequestContext,
+        group: ProductOptionGroup | ID,
+        input: CreateGroupOptionInput | CreateProductOptionInput,
     ): Promise<Translated<ProductOption>> {
+        const productOptionGroup =
+            group instanceof ProductOptionGroup
+                ? group
+                : await getEntityOrThrow(this.connection, ProductOptionGroup, group);
         const option = await this.translatableSaver.create({
             input,
             entityType: ProductOption,
             translationType: ProductOptionTranslation,
-            beforeSave: po => (po.group = group),
+            beforeSave: po => (po.group = productOptionGroup),
         });
-        return assertFound(this.findOne(option.id, DEFAULT_LANGUAGE_CODE));
+        return assertFound(this.findOne(ctx, option.id));
+    }
+
+    async update(ctx: RequestContext, input: UpdateProductOptionInput): Promise<Translated<ProductOption>> {
+        const option = await this.translatableSaver.update({
+            input,
+            entityType: ProductOption,
+            translationType: ProductOptionTranslation,
+        });
+        return assertFound(this.findOne(ctx, option.id));
     }
 }

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