Jelajahi Sumber

feat(core): Implement deleteProductVariant mutation

Relates to #124
Michael Bromley 6 tahun lalu
induk
melakukan
8b22831b41

+ 80 - 82
packages/common/src/generated-types.ts

@@ -1544,10 +1544,10 @@ export type Mutation = {
   updateAdministrator: Administrator,
   /** Assign a Role to an Administrator */
   assignRoleToAdministrator: Administrator,
-  login: LoginResult,
-  logout: Scalars['Boolean'],
   /** Create a new Asset */
   createAssets: Array<Asset>,
+  login: LoginResult,
+  logout: Scalars['Boolean'],
   /** Create a new Channel */
   createChannel: Channel,
   /** Update an existing Channel */
@@ -1604,16 +1604,13 @@ export type Mutation = {
   refundOrder: Refund,
   settleRefund: Refund,
   addNoteToOrder: Order,
+  /** Update an existing PaymentMethod */
+  updatePaymentMethod: PaymentMethod,
   /** Create a new ProductOptionGroup */
   createProductOptionGroup: ProductOptionGroup,
   /** Update an existing ProductOptionGroup */
   updateProductOptionGroup: ProductOptionGroup,
   reindex: JobInfo,
-  /** Update an existing PaymentMethod */
-  updatePaymentMethod: PaymentMethod,
-  createPromotion: Promotion,
-  updatePromotion: Promotion,
-  deletePromotion: DeletionResponse,
   /** Create a new Product */
   createProduct: Product,
   /** Update an existing Product */
@@ -1625,22 +1622,26 @@ export type Mutation = {
   /** Remove an OptionGroup from a Product */
   removeOptionGroupFromProduct: Product,
   /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
-  generateVariantsForProduct: Product,
   createProductVariants: Array<Maybe<ProductVariant>>,
   /** Update existing ProductVariants */
   updateProductVariants: Array<Maybe<ProductVariant>>,
+  /** Delete a ProductVariant */
+  deleteProductVariant: DeletionResponse,
+  createPromotion: Promotion,
+  updatePromotion: Promotion,
+  deletePromotion: DeletionResponse,
   /** Create a new Role */
   createRole: Role,
   /** Update an existing Role */
   updateRole: Role,
-  /** Create a new TaxCategory */
-  createTaxCategory: TaxCategory,
-  /** Update an existing TaxCategory */
-  updateTaxCategory: TaxCategory,
   /** Create a new ShippingMethod */
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
+  /** Create a new TaxCategory */
+  createTaxCategory: TaxCategory,
+  /** Update an existing TaxCategory */
+  updateTaxCategory: TaxCategory,
   /** Create a new TaxRate */
   createTaxRate: TaxRate,
   /** Update an existing TaxRate */
@@ -1674,6 +1675,11 @@ export type MutationAssignRoleToAdministratorArgs = {
 };
 
 
+export type MutationCreateAssetsArgs = {
+  input: Array<CreateAssetInput>
+};
+
+
 export type MutationLoginArgs = {
   username: Scalars['String'],
   password: Scalars['String'],
@@ -1681,11 +1687,6 @@ export type MutationLoginArgs = {
 };
 
 
-export type MutationCreateAssetsArgs = {
-  input: Array<CreateAssetInput>
-};
-
-
 export type MutationCreateChannelArgs = {
   input: CreateChannelInput
 };
@@ -1852,33 +1853,18 @@ export type MutationAddNoteToOrderArgs = {
 };
 
 
-export type MutationCreateProductOptionGroupArgs = {
-  input: CreateProductOptionGroupInput
-};
-
-
-export type MutationUpdateProductOptionGroupArgs = {
-  input: UpdateProductOptionGroupInput
-};
-
-
 export type MutationUpdatePaymentMethodArgs = {
   input: UpdatePaymentMethodInput
 };
 
 
-export type MutationCreatePromotionArgs = {
-  input: CreatePromotionInput
-};
-
-
-export type MutationUpdatePromotionArgs = {
-  input: UpdatePromotionInput
+export type MutationCreateProductOptionGroupArgs = {
+  input: CreateProductOptionGroupInput
 };
 
 
-export type MutationDeletePromotionArgs = {
-  id: Scalars['ID']
+export type MutationUpdateProductOptionGroupArgs = {
+  input: UpdateProductOptionGroupInput
 };
 
 
@@ -1909,14 +1895,6 @@ export type MutationRemoveOptionGroupFromProductArgs = {
 };
 
 
-export type MutationGenerateVariantsForProductArgs = {
-  productId: Scalars['ID'],
-  defaultTaxCategoryId?: Maybe<Scalars['ID']>,
-  defaultPrice?: Maybe<Scalars['Int']>,
-  defaultSku?: Maybe<Scalars['String']>
-};
-
-
 export type MutationCreateProductVariantsArgs = {
   input: Array<CreateProductVariantInput>
 };
@@ -1927,23 +1905,33 @@ export type MutationUpdateProductVariantsArgs = {
 };
 
 
-export type MutationCreateRoleArgs = {
-  input: CreateRoleInput
+export type MutationDeleteProductVariantArgs = {
+  id: Scalars['ID']
 };
 
 
-export type MutationUpdateRoleArgs = {
-  input: UpdateRoleInput
+export type MutationCreatePromotionArgs = {
+  input: CreatePromotionInput
 };
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationUpdatePromotionArgs = {
+  input: UpdatePromotionInput
 };
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationDeletePromotionArgs = {
+  id: Scalars['ID']
+};
+
+
+export type MutationCreateRoleArgs = {
+  input: CreateRoleInput
+};
+
+
+export type MutationUpdateRoleArgs = {
+  input: UpdateRoleInput
 };
 
 
@@ -1957,6 +1945,16 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
+};
+
+
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
+};
+
+
 export type MutationCreateTaxRateArgs = {
   input: CreateTaxRateInput
 };
@@ -2491,9 +2489,9 @@ export type Query = {
   __typename?: 'Query',
   administrators: AdministratorList,
   administrator?: Maybe<Administrator>,
-  me?: Maybe<CurrentUser>,
   assets: AssetList,
   asset?: Maybe<Asset>,
+  me?: Maybe<CurrentUser>,
   channels: Array<Channel>,
   channel?: Maybe<Channel>,
   activeChannel: Channel,
@@ -2513,25 +2511,25 @@ export type Query = {
   jobs: Array<JobInfo>,
   order?: Maybe<Order>,
   orders: OrderList,
+  paymentMethods: PaymentMethodList,
+  paymentMethod?: Maybe<PaymentMethod>,
   productOptionGroups: Array<ProductOptionGroup>,
   productOptionGroup?: Maybe<ProductOptionGroup>,
   search: SearchResponse,
-  paymentMethods: PaymentMethodList,
-  paymentMethod?: Maybe<PaymentMethod>,
-  promotion?: Maybe<Promotion>,
-  promotions: PromotionList,
-  adjustmentOperations: AdjustmentOperations,
   products: ProductList,
   /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
   product?: Maybe<Product>,
+  promotion?: Maybe<Promotion>,
+  promotions: PromotionList,
+  adjustmentOperations: AdjustmentOperations,
   roles: RoleList,
   role?: Maybe<Role>,
-  taxCategories: Array<TaxCategory>,
-  taxCategory?: Maybe<TaxCategory>,
   shippingMethods: ShippingMethodList,
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
+  taxCategories: Array<TaxCategory>,
+  taxCategory?: Maybe<TaxCategory>,
   taxRates: TaxRateList,
   taxRate?: Maybe<TaxRate>,
   zones: Array<Zone>,
@@ -2633,6 +2631,16 @@ export type QueryOrdersArgs = {
 };
 
 
+export type QueryPaymentMethodsArgs = {
+  options?: Maybe<PaymentMethodListOptions>
+};
+
+
+export type QueryPaymentMethodArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryProductOptionGroupsArgs = {
   languageCode?: Maybe<LanguageCode>,
   filterTerm?: Maybe<Scalars['String']>
@@ -2650,13 +2658,16 @@ export type QuerySearchArgs = {
 };
 
 
-export type QueryPaymentMethodsArgs = {
-  options?: Maybe<PaymentMethodListOptions>
+export type QueryProductsArgs = {
+  languageCode?: Maybe<LanguageCode>,
+  options?: Maybe<ProductListOptions>
 };
 
 
-export type QueryPaymentMethodArgs = {
-  id: Scalars['ID']
+export type QueryProductArgs = {
+  id?: Maybe<Scalars['ID']>,
+  slug?: Maybe<Scalars['String']>,
+  languageCode?: Maybe<LanguageCode>
 };
 
 
@@ -2670,19 +2681,6 @@ export type QueryPromotionsArgs = {
 };
 
 
-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 QueryRolesArgs = {
   options?: Maybe<RoleListOptions>
 };
@@ -2693,11 +2691,6 @@ export type QueryRoleArgs = {
 };
 
 
-export type QueryTaxCategoryArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryShippingMethodsArgs = {
   options?: Maybe<ShippingMethodListOptions>
 };
@@ -2708,6 +2701,11 @@ export type QueryShippingMethodArgs = {
 };
 
 
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryTaxRatesArgs = {
   options?: Maybe<TaxRateListOptions>
 };

+ 86 - 74
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1541,10 +1541,10 @@ export type Mutation = {
     updateAdministrator: Administrator;
     /** Assign a Role to an Administrator */
     assignRoleToAdministrator: Administrator;
-    login: LoginResult;
-    logout: Scalars['Boolean'];
     /** Create a new Asset */
     createAssets: Array<Asset>;
+    login: LoginResult;
+    logout: Scalars['Boolean'];
     /** Create a new Channel */
     createChannel: Channel;
     /** Update an existing Channel */
@@ -1601,16 +1601,13 @@ export type Mutation = {
     refundOrder: Refund;
     settleRefund: Refund;
     addNoteToOrder: Order;
+    /** Update an existing PaymentMethod */
+    updatePaymentMethod: PaymentMethod;
     /** Create a new ProductOptionGroup */
     createProductOptionGroup: ProductOptionGroup;
     /** Update an existing ProductOptionGroup */
     updateProductOptionGroup: ProductOptionGroup;
     reindex: JobInfo;
-    /** Update an existing PaymentMethod */
-    updatePaymentMethod: PaymentMethod;
-    createPromotion: Promotion;
-    updatePromotion: Promotion;
-    deletePromotion: DeletionResponse;
     /** Create a new Product */
     createProduct: Product;
     /** Update an existing Product */
@@ -1622,22 +1619,26 @@ export type Mutation = {
     /** Remove an OptionGroup from a Product */
     removeOptionGroupFromProduct: Product;
     /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
-    generateVariantsForProduct: Product;
     createProductVariants: Array<Maybe<ProductVariant>>;
     /** Update existing ProductVariants */
     updateProductVariants: Array<Maybe<ProductVariant>>;
+    /** Delete a ProductVariant */
+    deleteProductVariant: DeletionResponse;
+    createPromotion: Promotion;
+    updatePromotion: Promotion;
+    deletePromotion: DeletionResponse;
     /** Create a new Role */
     createRole: Role;
     /** Update an existing Role */
     updateRole: Role;
-    /** Create a new TaxCategory */
-    createTaxCategory: TaxCategory;
-    /** Update an existing TaxCategory */
-    updateTaxCategory: TaxCategory;
     /** Create a new ShippingMethod */
     createShippingMethod: ShippingMethod;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
+    /** Create a new TaxCategory */
+    createTaxCategory: TaxCategory;
+    /** Update an existing TaxCategory */
+    updateTaxCategory: TaxCategory;
     /** Create a new TaxRate */
     createTaxRate: TaxRate;
     /** Update an existing TaxRate */
@@ -1667,16 +1668,16 @@ export type MutationAssignRoleToAdministratorArgs = {
     roleId: Scalars['ID'];
 };
 
+export type MutationCreateAssetsArgs = {
+    input: Array<CreateAssetInput>;
+};
+
 export type MutationLoginArgs = {
     username: Scalars['String'];
     password: Scalars['String'];
     rememberMe?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationCreateAssetsArgs = {
-    input: Array<CreateAssetInput>;
-};
-
 export type MutationCreateChannelArgs = {
     input: CreateChannelInput;
 };
@@ -1811,28 +1812,16 @@ export type MutationAddNoteToOrderArgs = {
     input: AddNoteToOrderInput;
 };
 
-export type MutationCreateProductOptionGroupArgs = {
-    input: CreateProductOptionGroupInput;
-};
-
-export type MutationUpdateProductOptionGroupArgs = {
-    input: UpdateProductOptionGroupInput;
-};
-
 export type MutationUpdatePaymentMethodArgs = {
     input: UpdatePaymentMethodInput;
 };
 
-export type MutationCreatePromotionArgs = {
-    input: CreatePromotionInput;
-};
-
-export type MutationUpdatePromotionArgs = {
-    input: UpdatePromotionInput;
+export type MutationCreateProductOptionGroupArgs = {
+    input: CreateProductOptionGroupInput;
 };
 
-export type MutationDeletePromotionArgs = {
-    id: Scalars['ID'];
+export type MutationUpdateProductOptionGroupArgs = {
+    input: UpdateProductOptionGroupInput;
 };
 
 export type MutationCreateProductArgs = {
@@ -1857,13 +1846,6 @@ export type MutationRemoveOptionGroupFromProductArgs = {
     optionGroupId: Scalars['ID'];
 };
 
-export type MutationGenerateVariantsForProductArgs = {
-    productId: Scalars['ID'];
-    defaultTaxCategoryId?: Maybe<Scalars['ID']>;
-    defaultPrice?: Maybe<Scalars['Int']>;
-    defaultSku?: Maybe<Scalars['String']>;
-};
-
 export type MutationCreateProductVariantsArgs = {
     input: Array<CreateProductVariantInput>;
 };
@@ -1872,20 +1854,28 @@ export type MutationUpdateProductVariantsArgs = {
     input: Array<UpdateProductVariantInput>;
 };
 
-export type MutationCreateRoleArgs = {
-    input: CreateRoleInput;
+export type MutationDeleteProductVariantArgs = {
+    id: Scalars['ID'];
 };
 
-export type MutationUpdateRoleArgs = {
-    input: UpdateRoleInput;
+export type MutationCreatePromotionArgs = {
+    input: CreatePromotionInput;
 };
 
-export type MutationCreateTaxCategoryArgs = {
-    input: CreateTaxCategoryInput;
+export type MutationUpdatePromotionArgs = {
+    input: UpdatePromotionInput;
 };
 
-export type MutationUpdateTaxCategoryArgs = {
-    input: UpdateTaxCategoryInput;
+export type MutationDeletePromotionArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateRoleArgs = {
+    input: CreateRoleInput;
+};
+
+export type MutationUpdateRoleArgs = {
+    input: UpdateRoleInput;
 };
 
 export type MutationCreateShippingMethodArgs = {
@@ -1896,6 +1886,14 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
+export type MutationCreateTaxCategoryArgs = {
+    input: CreateTaxCategoryInput;
+};
+
+export type MutationUpdateTaxCategoryArgs = {
+    input: UpdateTaxCategoryInput;
+};
+
 export type MutationCreateTaxRateArgs = {
     input: CreateTaxRateInput;
 };
@@ -2422,9 +2420,9 @@ export type Query = {
     __typename?: 'Query';
     administrators: AdministratorList;
     administrator?: Maybe<Administrator>;
-    me?: Maybe<CurrentUser>;
     assets: AssetList;
     asset?: Maybe<Asset>;
+    me?: Maybe<CurrentUser>;
     channels: Array<Channel>;
     channel?: Maybe<Channel>;
     activeChannel: Channel;
@@ -2444,25 +2442,25 @@ export type Query = {
     jobs: Array<JobInfo>;
     order?: Maybe<Order>;
     orders: OrderList;
+    paymentMethods: PaymentMethodList;
+    paymentMethod?: Maybe<PaymentMethod>;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
     search: SearchResponse;
-    paymentMethods: PaymentMethodList;
-    paymentMethod?: Maybe<PaymentMethod>;
-    promotion?: Maybe<Promotion>;
-    promotions: PromotionList;
-    adjustmentOperations: AdjustmentOperations;
     products: ProductList;
     /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
     product?: Maybe<Product>;
+    promotion?: Maybe<Promotion>;
+    promotions: PromotionList;
+    adjustmentOperations: AdjustmentOperations;
     roles: RoleList;
     role?: Maybe<Role>;
-    taxCategories: Array<TaxCategory>;
-    taxCategory?: Maybe<TaxCategory>;
     shippingMethods: ShippingMethodList;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
+    taxCategories: Array<TaxCategory>;
+    taxCategory?: Maybe<TaxCategory>;
     taxRates: TaxRateList;
     taxRate?: Maybe<TaxRate>;
     zones: Array<Zone>;
@@ -2545,6 +2543,14 @@ export type QueryOrdersArgs = {
     options?: Maybe<OrderListOptions>;
 };
 
+export type QueryPaymentMethodsArgs = {
+    options?: Maybe<PaymentMethodListOptions>;
+};
+
+export type QueryPaymentMethodArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryProductOptionGroupsArgs = {
     languageCode?: Maybe<LanguageCode>;
     filterTerm?: Maybe<Scalars['String']>;
@@ -2559,12 +2565,15 @@ export type QuerySearchArgs = {
     input: SearchInput;
 };
 
-export type QueryPaymentMethodsArgs = {
-    options?: Maybe<PaymentMethodListOptions>;
+export type QueryProductsArgs = {
+    languageCode?: Maybe<LanguageCode>;
+    options?: Maybe<ProductListOptions>;
 };
 
-export type QueryPaymentMethodArgs = {
-    id: Scalars['ID'];
+export type QueryProductArgs = {
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
+    languageCode?: Maybe<LanguageCode>;
 };
 
 export type QueryPromotionArgs = {
@@ -2575,17 +2584,6 @@ export type QueryPromotionsArgs = {
     options?: Maybe<PromotionListOptions>;
 };
 
-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 QueryRolesArgs = {
     options?: Maybe<RoleListOptions>;
 };
@@ -2594,10 +2592,6 @@ export type QueryRoleArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryTaxCategoryArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryShippingMethodsArgs = {
     options?: Maybe<ShippingMethodListOptions>;
 };
@@ -2606,6 +2600,10 @@ export type QueryShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryTaxCategoryArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryTaxRatesArgs = {
     options?: Maybe<TaxRateListOptions>;
 };
@@ -4248,6 +4246,14 @@ export type GetOptionGroupQuery = { __typename?: 'Query' } & {
     >;
 };
 
+export type DeleteProductVariantMutationVariables = {
+    id: Scalars['ID'];
+};
+
+export type DeleteProductVariantMutation = { __typename?: 'Mutation' } & {
+    deleteProductVariant: { __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'result' | 'message'>;
+};
+
 export type DeletePromotionMutationVariables = {
     id: Scalars['ID'];
 };
@@ -5254,6 +5260,12 @@ export namespace GetOptionGroup {
     export type Options = NonNullable<(NonNullable<GetOptionGroupQuery['productOptionGroup']>)['options'][0]>;
 }
 
+export namespace DeleteProductVariant {
+    export type Variables = DeleteProductVariantMutationVariables;
+    export type Mutation = DeleteProductVariantMutation;
+    export type DeleteProductVariant = DeleteProductVariantMutation['deleteProductVariant'];
+}
+
 export namespace DeletePromotion {
     export type Variables = DeletePromotionMutationVariables;
     export type Mutation = DeletePromotionMutation;

+ 151 - 69
packages/core/e2e/product.e2e-spec.ts

@@ -5,14 +5,13 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
-import { PRODUCT_WITH_VARIANTS_FRAGMENT } from './graphql/fragments';
 import {
     AddOptionGroupToProduct,
     CreateProduct,
     CreateProductVariants,
     DeleteProduct,
+    DeleteProductVariant,
     DeletionResult,
-    GenerateProductVariants,
     GetAssetList,
     GetOptionGroup,
     GetProductList,
@@ -160,11 +159,21 @@ describe('Product resolver', () => {
             }, 'Either the product id or slug must be provided'),
         );
 
+        it(
+            'throws if id and slug do not refer to the same Product',
+            assertThrowsWithMessage(async () => {
+                await client.query<GetProductSimple.Query, GetProductSimple.Variables>(GET_PRODUCT_SIMPLE, {
+                    id: 'T_2',
+                    slug: 'laptop',
+                });
+            }, 'The provided id and slug refer to different Products'),
+        );
+
         it('returns expected properties', async () => {
             const { product } = await client.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 languageCode: LanguageCode.en,
                 id: 'T_2',
             });
@@ -418,7 +427,7 @@ describe('Product resolver', () => {
             const productResult = await client.query<
                 GetProductWithVariants.Query,
                 GetProductWithVariants.Variables
-                >(GET_PRODUCT_WITH_VARIANTS, {
+            >(GET_PRODUCT_WITH_VARIANTS, {
                 id: newProduct.id,
                 languageCode: LanguageCode.en,
             });
@@ -480,7 +489,7 @@ describe('Product resolver', () => {
             const result = await client.query<
                 AddOptionGroupToProduct.Mutation,
                 AddOptionGroupToProduct.Variables
-                >(ADD_OPTION_GROUP_TO_PRODUCT, {
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
                 optionGroupId: 'T_2',
                 productId: newProduct.id,
             });
@@ -522,7 +531,7 @@ describe('Product resolver', () => {
             const { addOptionGroupToProduct } = await client.query<
                 AddOptionGroupToProduct.Mutation,
                 AddOptionGroupToProduct.Variables
-                >(ADD_OPTION_GROUP_TO_PRODUCT, {
+            >(ADD_OPTION_GROUP_TO_PRODUCT, {
                 optionGroupId: 'T_1',
                 productId: newProductWithAssets.id,
             });
@@ -531,7 +540,7 @@ describe('Product resolver', () => {
             const result = await client.query<
                 RemoveOptionGroupFromProduct.Mutation,
                 RemoveOptionGroupFromProduct.Variables
-                >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+            >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                 optionGroupId: 'T_1',
                 productId: newProductWithAssets.id,
             });
@@ -546,7 +555,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: 'T_3',
                         productId: 'T_2',
                     }),
@@ -561,7 +570,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: '1',
                         productId: '999',
                     }),
@@ -576,7 +585,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: '999',
                         productId: newProduct.id,
                     }),
@@ -590,15 +599,21 @@ describe('Product resolver', () => {
             let optionGroup3: GetOptionGroup.ProductOptionGroup;
 
             beforeAll(async () => {
-                await client.query<
-                    AddOptionGroupToProduct.Mutation,
-                    AddOptionGroupToProduct.Variables
-                    >(ADD_OPTION_GROUP_TO_PRODUCT, {
-                    optionGroupId: 'T_3',
-                    productId: newProduct.id,
-                });
-                const result1 = await client.query<GetOptionGroup.Query, GetOptionGroup.Variables>(GET_OPTION_GROUP, { id: 'T_2' });
-                const result2 = await client.query<GetOptionGroup.Query, GetOptionGroup.Variables>(GET_OPTION_GROUP, { id: 'T_3' });
+                await client.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
+                    ADD_OPTION_GROUP_TO_PRODUCT,
+                    {
+                        optionGroupId: 'T_3',
+                        productId: newProduct.id,
+                    },
+                );
+                const result1 = await client.query<GetOptionGroup.Query, GetOptionGroup.Variables>(
+                    GET_OPTION_GROUP,
+                    { id: 'T_2' },
+                );
+                const result2 = await client.query<GetOptionGroup.Query, GetOptionGroup.Variables>(
+                    GET_OPTION_GROUP,
+                    { id: 'T_3' },
+                );
                 optionGroup2 = result1.productOptionGroup!;
                 optionGroup3 = result2.productOptionGroup!;
             });
@@ -606,38 +621,38 @@ describe('Product resolver', () => {
             it(
                 'createProductVariants throws if optionIds not compatible with product',
                 assertThrowsWithMessage(async () => {
-                    await client.query<
-                        CreateProductVariants.Mutation,
-                        CreateProductVariants.Variables
-                        >(CREATE_PRODUCT_VARIANTS, {
-                        input: [
-                            {
-                                productId: newProduct.id,
-                                sku: 'PV1',
-                                optionIds: [],
-                                translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
-                            },
-                        ],
-                    });
+                    await client.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
+                        CREATE_PRODUCT_VARIANTS,
+                        {
+                            input: [
+                                {
+                                    productId: newProduct.id,
+                                    sku: 'PV1',
+                                    optionIds: [],
+                                    translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
+                                },
+                            ],
+                        },
+                    );
                 }, 'ProductVariant optionIds must include one optionId from each of the groups: curvy-monitor-monitor-size, laptop-ram'),
             );
 
             it(
                 'createProductVariants throws if optionIds are duplicated',
                 assertThrowsWithMessage(async () => {
-                    await client.query<
-                        CreateProductVariants.Mutation,
-                        CreateProductVariants.Variables
-                        >(CREATE_PRODUCT_VARIANTS, {
-                        input: [
-                            {
-                                productId: newProduct.id,
-                                sku: 'PV1',
-                                optionIds: [optionGroup2.options[0].id, optionGroup2.options[1].id],
-                                translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
-                            },
-                        ],
-                    });
+                    await client.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
+                        CREATE_PRODUCT_VARIANTS,
+                        {
+                            input: [
+                                {
+                                    productId: newProduct.id,
+                                    sku: 'PV1',
+                                    optionIds: [optionGroup2.options[0].id, optionGroup2.options[1].id],
+                                    translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }],
+                                },
+                            ],
+                        },
+                    );
                 }, 'ProductVariant optionIds must include one optionId from each of the groups: curvy-monitor-monitor-size, laptop-ram'),
             );
 
@@ -645,7 +660,7 @@ describe('Product resolver', () => {
                 const { createProductVariants } = await client.query<
                     CreateProductVariants.Mutation,
                     CreateProductVariants.Variables
-                    >(CREATE_PRODUCT_VARIANTS, {
+                >(CREATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             productId: newProduct.id,
@@ -660,27 +675,58 @@ describe('Product resolver', () => {
                     { id: optionGroup2.options[0].id },
                     { id: optionGroup3.options[0].id },
                 ]);
+            });
+
+            it('createProductVariants adds multiple variants at once', async () => {
+                const { createProductVariants } = await client.query<
+                    CreateProductVariants.Mutation,
+                    CreateProductVariants.Variables
+                >(CREATE_PRODUCT_VARIANTS, {
+                    input: [
+                        {
+                            productId: newProduct.id,
+                            sku: 'PV2',
+                            optionIds: [optionGroup2.options[1].id, optionGroup3.options[0].id],
+                            translations: [{ languageCode: LanguageCode.en, name: 'Variant 2' }],
+                        },
+                        {
+                            productId: newProduct.id,
+                            sku: 'PV3',
+                            optionIds: [optionGroup2.options[1].id, optionGroup3.options[1].id],
+                            translations: [{ languageCode: LanguageCode.en, name: 'Variant 3' }],
+                        },
+                    ],
+                });
+                expect(createProductVariants[0]!.name).toBe('Variant 2');
+                expect(createProductVariants[1]!.name).toBe('Variant 3');
+                expect(createProductVariants[0]!.options.map(pick(['id']))).toEqual([
+                    { id: optionGroup2.options[1].id },
+                    { id: optionGroup3.options[0].id },
+                ]);
+                expect(createProductVariants[1]!.options.map(pick(['id']))).toEqual([
+                    { id: optionGroup2.options[1].id },
+                    { id: optionGroup3.options[1].id },
+                ]);
                 variants = createProductVariants.filter(notNullOrUndefined);
             });
 
-            it('createProductVariants throws if options combination exists', assertThrowsWithMessage(
-                async () => {
-                    await client.query<
-                        CreateProductVariants.Mutation,
-                        CreateProductVariants.Variables
-                        >(CREATE_PRODUCT_VARIANTS, {
-                        input: [
-                            {
-                                productId: newProduct.id,
-                                sku: 'PV2',
-                                optionIds: [optionGroup2.options[0].id, optionGroup3.options[0].id],
-                                translations: [{ languageCode: LanguageCode.en, name: 'Variant 2' }],
-                            },
-                        ],
-                    });
-                },
-                'A ProductVariant already exists with the options: 16gb, 24-inch',
-                ),
+            it(
+                'createProductVariants throws if options combination already exists',
+                assertThrowsWithMessage(async () => {
+                    await client.query<CreateProductVariants.Mutation, CreateProductVariants.Variables>(
+                        CREATE_PRODUCT_VARIANTS,
+                        {
+                            input: [
+                                {
+                                    productId: newProduct.id,
+                                    sku: 'PV2',
+                                    optionIds: [optionGroup2.options[0].id, optionGroup3.options[0].id],
+                                    translations: [{ languageCode: LanguageCode.en, name: 'Variant 2' }],
+                                },
+                            ],
+                        },
+                    );
+                }, 'A ProductVariant already exists with the options: 16gb, 24-inch'),
             );
 
             it('updateProductVariants updates variants', async () => {
@@ -688,7 +734,7 @@ describe('Product resolver', () => {
                 const { updateProductVariants } = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -712,7 +758,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -735,7 +781,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -758,7 +804,7 @@ describe('Product resolver', () => {
                 const result = await client.query<
                     UpdateProductVariants.Mutation,
                     UpdateProductVariants.Variables
-                    >(UPDATE_PRODUCT_VARIANTS, {
+                >(UPDATE_PRODUCT_VARIANTS, {
                     input: [
                         {
                             id: firstVariant.id,
@@ -795,6 +841,33 @@ describe('Product resolver', () => {
                     `No ProductVariant with the id '999' could be found`,
                 ),
             );
+
+            it('deleteProductVariant', async () => {
+                const result1 = await client.query<
+                    GetProductWithVariants.Query,
+                    GetProductWithVariants.Variables
+                >(GET_PRODUCT_WITH_VARIANTS, {
+                    id: newProduct.id,
+                });
+                expect(result1.product!.variants.map(v => v.id)).toEqual(['T_35', 'T_36', 'T_37']);
+
+                const { deleteProductVariant } = await client.query<
+                    DeleteProductVariant.Mutation,
+                    DeleteProductVariant.Variables
+                >(DELETE_PRODUCT_VARIANT, {
+                    id: result1.product!.variants[0].id,
+                });
+
+                expect(deleteProductVariant.result).toBe(DeletionResult.DELETED);
+
+                const result2 = await client.query<
+                    GetProductWithVariants.Query,
+                    GetProductWithVariants.Variables
+                >(GET_PRODUCT_WITH_VARIANTS, {
+                    id: newProduct.id,
+                });
+                expect(result2.product!.variants.map(v => v.id)).toEqual(['T_36', 'T_37']);
+            });
         });
     });
 
@@ -871,7 +944,7 @@ describe('Product resolver', () => {
                     client.query<
                         RemoveOptionGroupFromProduct.Mutation,
                         RemoveOptionGroupFromProduct.Variables
-                        >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                         optionGroupId: 'T_1',
                         productId: productToDelete.id,
                     }),
@@ -925,3 +998,12 @@ export const GET_OPTION_GROUP = gql`
         }
     }
 `;
+
+export const DELETE_PRODUCT_VARIANT = gql`
+    mutation DeleteProductVariant($id: ID!) {
+        deleteProductVariant(id: $id) {
+            result
+            message
+        }
+    }
+`;

+ 15 - 1
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -5,6 +5,7 @@ import {
     MutationCreateProductArgs,
     MutationCreateProductVariantsArgs,
     MutationDeleteProductArgs,
+    MutationDeleteProductVariantArgs,
     MutationRemoveOptionGroupFromProductArgs,
     MutationUpdateProductArgs,
     MutationUpdateProductVariantsArgs,
@@ -50,7 +51,11 @@ export class ProductResolver {
         @Args() args: QueryProductArgs,
     ): Promise<Translated<Product> | undefined> {
         if (args.id) {
-            return this.productService.findOne(ctx, args.id);
+            const product = await this.productService.findOne(ctx, args.id);
+            if (args.slug && product && product.slug !== args.slug) {
+                throw new UserInputError(`error.product-id-slug-mismatch`);
+            }
+            return product;
         } else if (args.slug) {
             return this.productService.findOneBySlug(ctx, args.slug);
         } else {
@@ -132,4 +137,13 @@ export class ProductResolver {
         const { input } = args;
         return Promise.all(input.map(i => this.productVariantService.update(ctx, i)));
     }
+
+    @Mutation()
+    @Allow(Permission.DeleteCatalog)
+    async deleteProductVariant(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationDeleteProductVariantArgs,
+    ): Promise<DeletionResponse> {
+        return this.productVariantService.softDelete(ctx, args.id);
+    }
 }

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

@@ -25,6 +25,9 @@ type Mutation {
 
     "Update existing ProductVariants"
     updateProductVariants(input: [UpdateProductVariantInput!]!): [ProductVariant]!
+
+    "Delete a ProductVariant"
+    deleteProductVariant(id: ID!): DeletionResponse!
 }
 
 type Product {

+ 5 - 1
packages/core/src/entity/product-variant/product-variant.entity.ts

@@ -2,6 +2,7 @@ import { CurrencyCode } from '@vendure/common/lib/generated-types';
 import { DeepPartial, HasCustomFields } from '@vendure/common/lib/shared-types';
 import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
 
+import { SoftDeletable } from '../../common/types/common-types';
 import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
 import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
@@ -27,11 +28,14 @@ import { ProductVariantTranslation } from './product-variant-translation.entity'
  * @docsCategory entities
  */
 @Entity()
-export class ProductVariant extends VendureEntity implements Translatable, HasCustomFields {
+export class ProductVariant extends VendureEntity implements Translatable, HasCustomFields, SoftDeletable {
     constructor(input?: DeepPartial<ProductVariant>) {
         super(input);
     }
 
+    @Column({ type: Date, nullable: true, default: null })
+    deletedAt: Date | null;
+
     name: LocaleString;
 
     @Column({ default: true })

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

@@ -37,6 +37,7 @@
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-not-recognized": "Password reset token not recognized",
     "product-id-or-slug-must-be-provided": "Either the product id or slug must be provided",
+    "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",
     "product-variant-options-combination-already-exists": "A ProductVariant already exists with the options: {optionNames}",
     "refund-order-item-already-refunded": "Cannot refund an OrderItem which has already been refunded",

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

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
-import { CreateProductVariantInput, UpdateProductVariantInput } from '@vendure/common/lib/generated-types';
+import { CreateProductVariantInput, DeletionResponse, DeletionResult, UpdateProductVariantInput } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { generateAllCombinations } from '@vendure/common/lib/shared-utils';
 import { Connection } from 'typeorm';
@@ -71,6 +71,7 @@ export class ProductVariantService {
             .find({
                 where: {
                     product: { id: productId } as any,
+                    deletedAt: null,
                 },
                 relations: [
                     'options',
@@ -274,6 +275,16 @@ export class ProductVariantService {
         ]);
     }
 
+    async softDelete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
+        const variant = await getEntityOrThrow(this.connection, ProductVariant, id);
+        variant.deletedAt = new Date();
+        await this.connection.getRepository(ProductVariant).save(variant);
+        this.eventBus.publish(new CatalogModificationEvent(ctx, variant));
+        return {
+            result: DeletionResult.DELETED,
+        };
+    }
+
     async generateVariantsForProduct(
         ctx: RequestContext,
         productId: ID,

+ 2 - 1
packages/core/src/service/services/product.service.ts

@@ -150,7 +150,8 @@ export class ProductService {
 
     async softDelete(ctx: RequestContext, productId: ID): Promise<DeletionResponse> {
         const product = await getEntityOrThrow(this.connection, Product, productId);
-        await this.connection.getRepository(Product).update({ id: productId }, { deletedAt: new Date() });
+        product.deletedAt = new Date();
+        await this.connection.getRepository(Product).save(product);
         this.eventBus.publish(new CatalogModificationEvent(ctx, product));
         return {
             result: DeletionResult.DELETED,

File diff ditekan karena terlalu besar
+ 0 - 0
schema-admin.json


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini