Browse Source

feat(core): Add "enabled" field to Product & ProductVariant

Relates to #62
Michael Bromley 6 years ago
parent
commit
a8778539d4

+ 1 - 0
packages/common/package.json

@@ -4,6 +4,7 @@
   "main": "index.js",
   "license": "MIT",
   "scripts": {
+    "watch": "tsc -p ./tsconfig.build.json -w",
     "build": "rimraf dist && tsc -p ./tsconfig.build.json"
   },
   "publishConfig": {

+ 3 - 3
packages/common/src/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-04-09T14:28:32+02:00
+// Generated in 2019-04-24T10:46:04+02:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -1626,9 +1626,9 @@ export interface Mutation {
     verifyCustomerAccount: LoginResult;
     /** Update the password of the active Customer */
     updateCustomerPassword?: Maybe<boolean>;
-    /** Request to update the emailAddress of the active Customer */
+    /** Request to update the emailAddress of the active Customer. If `authOptions.requireVerification` is enabled (as is the default), then the `identifierChangeToken` will be assigned to the current User and a IdentifierChangeRequestEvent will be raised. This can then be used e.g. by the EmailPlugin to email that verification token to the Customer, which is then used to verify the change of email address. */
     requestUpdateCustomerEmailAddress?: Maybe<boolean>;
-    /** Confirm the update of the emailAddress with the provided token */
+    /** Confirm the update of the emailAddress with the provided token, which has been generated by the `requestUpdateCustomerEmailAddress` mutation. */
     updateCustomerEmailAddress?: Maybe<boolean>;
     /** Requests a password reset email to be sent */
     requestPasswordReset?: Maybe<boolean>;

+ 280 - 262
packages/common/src/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-04-09T14:28:32+02:00
+// Generated in 2019-04-24T10:46:05+02:00
 export type Maybe<T> = T | null;
 
 
@@ -220,6 +220,8 @@ export interface ProductVariantFilterParameter {
   priceIncludesTax?: Maybe<BooleanOperators>;
   
   priceWithTax?: Maybe<NumberOperators>;
+  
+  enabled?: Maybe<BooleanOperators>;
 }
 
 export interface BooleanOperators {
@@ -258,43 +260,6 @@ export interface CountryFilterParameter {
   enabled?: Maybe<BooleanOperators>;
 }
 
-export interface FacetListOptions {
-  
-  skip?: Maybe<number>;
-  
-  take?: Maybe<number>;
-  
-  sort?: Maybe<FacetSortParameter>;
-  
-  filter?: Maybe<FacetFilterParameter>;
-}
-
-export interface FacetSortParameter {
-  
-  id?: Maybe<SortOrder>;
-  
-  createdAt?: Maybe<SortOrder>;
-  
-  updatedAt?: Maybe<SortOrder>;
-  
-  name?: Maybe<SortOrder>;
-  
-  code?: Maybe<SortOrder>;
-}
-
-export interface FacetFilterParameter {
-  
-  createdAt?: Maybe<DateOperators>;
-  
-  updatedAt?: Maybe<DateOperators>;
-  
-  languageCode?: Maybe<StringOperators>;
-  
-  name?: Maybe<StringOperators>;
-  
-  code?: Maybe<StringOperators>;
-}
-
 export interface CustomerListOptions {
   
   skip?: Maybe<number>;
@@ -401,18 +366,18 @@ export interface OrderFilterParameter {
   total?: Maybe<NumberOperators>;
 }
 
-export interface PaymentMethodListOptions {
+export interface FacetListOptions {
   
   skip?: Maybe<number>;
   
   take?: Maybe<number>;
   
-  sort?: Maybe<PaymentMethodSortParameter>;
+  sort?: Maybe<FacetSortParameter>;
   
-  filter?: Maybe<PaymentMethodFilterParameter>;
+  filter?: Maybe<FacetFilterParameter>;
 }
 
-export interface PaymentMethodSortParameter {
+export interface FacetSortParameter {
   
   id?: Maybe<SortOrder>;
   
@@ -420,32 +385,36 @@ export interface PaymentMethodSortParameter {
   
   updatedAt?: Maybe<SortOrder>;
   
+  name?: Maybe<SortOrder>;
+  
   code?: Maybe<SortOrder>;
 }
 
-export interface PaymentMethodFilterParameter {
+export interface FacetFilterParameter {
   
   createdAt?: Maybe<DateOperators>;
   
   updatedAt?: Maybe<DateOperators>;
   
-  code?: Maybe<StringOperators>;
+  languageCode?: Maybe<StringOperators>;
   
-  enabled?: Maybe<BooleanOperators>;
+  name?: Maybe<StringOperators>;
+  
+  code?: Maybe<StringOperators>;
 }
 
-export interface ProductListOptions {
+export interface PaymentMethodListOptions {
   
   skip?: Maybe<number>;
   
   take?: Maybe<number>;
   
-  sort?: Maybe<ProductSortParameter>;
+  sort?: Maybe<PaymentMethodSortParameter>;
   
-  filter?: Maybe<ProductFilterParameter>;
+  filter?: Maybe<PaymentMethodFilterParameter>;
 }
 
-export interface ProductSortParameter {
+export interface PaymentMethodSortParameter {
   
   id?: Maybe<SortOrder>;
   
@@ -453,26 +422,18 @@ export interface ProductSortParameter {
   
   updatedAt?: Maybe<SortOrder>;
   
-  name?: Maybe<SortOrder>;
-  
-  slug?: Maybe<SortOrder>;
-  
-  description?: Maybe<SortOrder>;
+  code?: Maybe<SortOrder>;
 }
 
-export interface ProductFilterParameter {
+export interface PaymentMethodFilterParameter {
   
   createdAt?: Maybe<DateOperators>;
   
   updatedAt?: Maybe<DateOperators>;
   
-  languageCode?: Maybe<StringOperators>;
-  
-  name?: Maybe<StringOperators>;
-  
-  slug?: Maybe<StringOperators>;
+  code?: Maybe<StringOperators>;
   
-  description?: Maybe<StringOperators>;
+  enabled?: Maybe<BooleanOperators>;
 }
 
 export interface SearchInput {
@@ -499,6 +460,49 @@ export interface SearchResultSortParameter {
   price?: Maybe<SortOrder>;
 }
 
+export interface ProductListOptions {
+  
+  skip?: Maybe<number>;
+  
+  take?: Maybe<number>;
+  
+  sort?: Maybe<ProductSortParameter>;
+  
+  filter?: Maybe<ProductFilterParameter>;
+}
+
+export interface ProductSortParameter {
+  
+  id?: Maybe<SortOrder>;
+  
+  createdAt?: Maybe<SortOrder>;
+  
+  updatedAt?: Maybe<SortOrder>;
+  
+  name?: Maybe<SortOrder>;
+  
+  slug?: Maybe<SortOrder>;
+  
+  description?: Maybe<SortOrder>;
+}
+
+export interface ProductFilterParameter {
+  
+  createdAt?: Maybe<DateOperators>;
+  
+  updatedAt?: Maybe<DateOperators>;
+  
+  languageCode?: Maybe<StringOperators>;
+  
+  name?: Maybe<StringOperators>;
+  
+  slug?: Maybe<StringOperators>;
+  
+  description?: Maybe<StringOperators>;
+  
+  enabled?: Maybe<BooleanOperators>;
+}
+
 export interface PromotionListOptions {
   
   skip?: Maybe<number>;
@@ -821,79 +825,6 @@ export interface UpdateCustomerGroupInput {
   name?: Maybe<string>;
 }
 
-export interface CreateFacetInput {
-  
-  code: string;
-  
-  translations: FacetTranslationInput[];
-  
-  values?: Maybe<CreateFacetValueWithFacetInput[]>;
-  
-  customFields?: Maybe<Json>;
-}
-
-export interface FacetTranslationInput {
-  
-  id?: Maybe<string>;
-  
-  languageCode: LanguageCode;
-  
-  name?: Maybe<string>;
-  
-  customFields?: Maybe<Json>;
-}
-
-export interface CreateFacetValueWithFacetInput {
-  
-  code: string;
-  
-  translations: FacetValueTranslationInput[];
-}
-
-export interface FacetValueTranslationInput {
-  
-  id?: Maybe<string>;
-  
-  languageCode: LanguageCode;
-  
-  name?: Maybe<string>;
-  
-  customFields?: Maybe<Json>;
-}
-
-export interface UpdateFacetInput {
-  
-  id: string;
-  
-  code?: Maybe<string>;
-  
-  translations?: Maybe<FacetTranslationInput[]>;
-  
-  customFields?: Maybe<Json>;
-}
-
-export interface CreateFacetValueInput {
-  
-  facetId: string;
-  
-  code: string;
-  
-  translations: FacetValueTranslationInput[];
-  
-  customFields?: Maybe<Json>;
-}
-
-export interface UpdateFacetValueInput {
-  
-  id: string;
-  
-  code?: Maybe<string>;
-  
-  translations?: Maybe<FacetValueTranslationInput[]>;
-  
-  customFields?: Maybe<Json>;
-}
-
 export interface CreateCustomerInput {
   
   title?: Maybe<string>;
@@ -982,6 +913,79 @@ export interface UpdateAddressInput {
   customFields?: Maybe<Json>;
 }
 
+export interface CreateFacetInput {
+  
+  code: string;
+  
+  translations: FacetTranslationInput[];
+  
+  values?: Maybe<CreateFacetValueWithFacetInput[]>;
+  
+  customFields?: Maybe<Json>;
+}
+
+export interface FacetTranslationInput {
+  
+  id?: Maybe<string>;
+  
+  languageCode: LanguageCode;
+  
+  name?: Maybe<string>;
+  
+  customFields?: Maybe<Json>;
+}
+
+export interface CreateFacetValueWithFacetInput {
+  
+  code: string;
+  
+  translations: FacetValueTranslationInput[];
+}
+
+export interface FacetValueTranslationInput {
+  
+  id?: Maybe<string>;
+  
+  languageCode: LanguageCode;
+  
+  name?: Maybe<string>;
+  
+  customFields?: Maybe<Json>;
+}
+
+export interface UpdateFacetInput {
+  
+  id: string;
+  
+  code?: Maybe<string>;
+  
+  translations?: Maybe<FacetTranslationInput[]>;
+  
+  customFields?: Maybe<Json>;
+}
+
+export interface CreateFacetValueInput {
+  
+  facetId: string;
+  
+  code: string;
+  
+  translations: FacetValueTranslationInput[];
+  
+  customFields?: Maybe<Json>;
+}
+
+export interface UpdateFacetValueInput {
+  
+  id: string;
+  
+  code?: Maybe<string>;
+  
+  translations?: Maybe<FacetValueTranslationInput[]>;
+  
+  customFields?: Maybe<Json>;
+}
+
 export interface UpdateGlobalSettingsInput {
   
   availableLanguages?: Maybe<LanguageCode[]>;
@@ -1074,6 +1078,8 @@ export interface UpdateProductInput {
   
   id: string;
   
+  enabled?: Maybe<boolean>;
+  
   featuredAssetId?: Maybe<string>;
   
   assetIds?: Maybe<string[]>;
@@ -1089,6 +1095,8 @@ export interface UpdateProductVariantInput {
   
   id: string;
   
+  enabled?: Maybe<boolean>;
+  
   translations?: Maybe<ProductVariantTranslationInput[]>;
   
   facetValueIds?: Maybe<string[]>;
@@ -2705,6 +2713,8 @@ export namespace GetProductList {
     
     id: string;
     
+    enabled: boolean;
+    
     languageCode: LanguageCode;
     
     name: string;
@@ -4125,6 +4135,8 @@ export namespace ProductWithVariants {
     
     id: string;
     
+    enabled: boolean;
+    
     languageCode: LanguageCode;
     
     name: string;
@@ -4546,13 +4558,13 @@ export interface Query {
   
   customerGroup?: Maybe<CustomerGroup>;
   
-  facets: FacetList;
+  customers: CustomerList;
   
-  facet?: Maybe<Facet>;
+  customer?: Maybe<Customer>;
   
-  customers: CustomerList;
+  facets: FacetList;
   
-  customer?: Maybe<Customer>;
+  facet?: Maybe<Facet>;
   
   globalSettings: GlobalSettings;
   
@@ -4568,12 +4580,12 @@ export interface Query {
   
   productOptionGroup?: Maybe<ProductOptionGroup>;
   
+  search: SearchResponse;
+  
   products: ProductList;
   
   product?: Maybe<Product>;
   
-  search: SearchResponse;
-  
   promotion?: Maybe<Promotion>;
   
   promotions: PromotionList;
@@ -4916,6 +4928,8 @@ export interface ProductVariant extends Node {
   
   translations: ProductVariantTranslation[];
   
+  enabled: boolean;
+  
   customFields?: Maybe<Json>;
 }
 
@@ -5094,14 +5108,6 @@ export interface CountryList extends PaginatedList {
 }
 
 
-export interface FacetList extends PaginatedList {
-  
-  items: Facet[];
-  
-  totalItems: number;
-}
-
-
 export interface CustomerList extends PaginatedList {
   
   items: Customer[];
@@ -5344,6 +5350,14 @@ export interface ShippingMethod extends Node {
 }
 
 
+export interface FacetList extends PaginatedList {
+  
+  items: Facet[];
+  
+  totalItems: number;
+}
+
+
 export interface GlobalSettings {
   
   id: string;
@@ -5426,66 +5440,6 @@ export interface ProductOptionGroupTranslation {
 }
 
 
-export interface ProductList extends PaginatedList {
-  
-  items: Product[];
-  
-  totalItems: number;
-}
-
-
-export interface Product extends Node {
-  
-  id: string;
-  
-  createdAt: DateTime;
-  
-  updatedAt: DateTime;
-  
-  languageCode: LanguageCode;
-  
-  name: string;
-  
-  slug: string;
-  
-  description: string;
-  
-  featuredAsset?: Maybe<Asset>;
-  
-  assets: Asset[];
-  
-  variants: ProductVariant[];
-  
-  optionGroups: ProductOptionGroup[];
-  
-  facetValues: FacetValue[];
-  
-  translations: ProductTranslation[];
-  
-  collections: Collection[];
-  
-  customFields?: Maybe<Json>;
-}
-
-
-export interface ProductTranslation {
-  
-  id: string;
-  
-  createdAt: DateTime;
-  
-  updatedAt: DateTime;
-  
-  languageCode: LanguageCode;
-  
-  name: string;
-  
-  slug: string;
-  
-  description: string;
-}
-
-
 export interface SearchResponse {
   
   items: SearchResult[];
@@ -5529,6 +5483,8 @@ export interface SearchResult {
   collectionIds: string[];
   /** A relevence score for the result. Differs between database implementations */
   score: number;
+  
+  enabled: boolean;
 }
 
 /** The price range where the result has more than one price */
@@ -5554,6 +5510,68 @@ export interface FacetValueResult {
 }
 
 
+export interface ProductList extends PaginatedList {
+  
+  items: Product[];
+  
+  totalItems: number;
+}
+
+
+export interface Product extends Node {
+  
+  id: string;
+  
+  createdAt: DateTime;
+  
+  updatedAt: DateTime;
+  
+  languageCode: LanguageCode;
+  
+  name: string;
+  
+  slug: string;
+  
+  description: string;
+  
+  featuredAsset?: Maybe<Asset>;
+  
+  assets: Asset[];
+  
+  variants: ProductVariant[];
+  
+  optionGroups: ProductOptionGroup[];
+  
+  facetValues: FacetValue[];
+  
+  translations: ProductTranslation[];
+  
+  collections: Collection[];
+  
+  enabled: boolean;
+  
+  customFields?: Maybe<Json>;
+}
+
+
+export interface ProductTranslation {
+  
+  id: string;
+  
+  createdAt: DateTime;
+  
+  updatedAt: DateTime;
+  
+  languageCode: LanguageCode;
+  
+  name: string;
+  
+  slug: string;
+  
+  description: string;
+}
+
+
 export interface Promotion extends Node {
   
   id: string;
@@ -5671,18 +5689,6 @@ export interface Mutation {
   addCustomersToGroup: CustomerGroup;
   /** Remove Customers from a CustomerGroup */
   removeCustomersFromGroup: CustomerGroup;
-  /** Create a new Facet */
-  createFacet: Facet;
-  /** Update an existing Facet */
-  updateFacet: Facet;
-  /** Delete an existing Facet */
-  deleteFacet: DeletionResponse;
-  /** Create one or more FacetValues */
-  createFacetValues: FacetValue[];
-  /** Update one or more FacetValues */
-  updateFacetValues: FacetValue[];
-  /** Delete one or more FacetValues */
-  deleteFacetValues: DeletionResponse[];
   /** 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 */
@@ -5695,6 +5701,18 @@ export interface Mutation {
   updateCustomerAddress: Address;
   /** Update an existing Address */
   deleteCustomerAddress: boolean;
+  /** Create a new Facet */
+  createFacet: Facet;
+  /** Update an existing Facet */
+  updateFacet: Facet;
+  /** Delete an existing Facet */
+  deleteFacet: DeletionResponse;
+  /** Create one or more FacetValues */
+  createFacetValues: FacetValue[];
+  /** Update one or more FacetValues */
+  updateFacetValues: FacetValue[];
+  /** Delete one or more FacetValues */
+  deleteFacetValues: DeletionResponse[];
   
   updateGlobalSettings: GlobalSettings;
   
@@ -5705,6 +5723,8 @@ export interface Mutation {
   createProductOptionGroup: ProductOptionGroup;
   /** Update an existing ProductOptionGroup */
   updateProductOptionGroup: ProductOptionGroup;
+  
+  reindex: SearchReindexResponse;
   /** Create a new Product */
   createProduct: Product;
   /** Update an existing Product */
@@ -5720,8 +5740,6 @@ export interface Mutation {
   /** Update existing ProductVariants */
   updateProductVariants: (Maybe<ProductVariant>)[];
   
-  reindex: SearchReindexResponse;
-  
   createPromotion: Promotion;
   
   updatePromotion: Promotion;
@@ -5859,6 +5877,14 @@ export interface CustomerGroupQueryArgs {
   
   id: string;
 }
+export interface CustomersQueryArgs {
+  
+  options?: Maybe<CustomerListOptions>;
+}
+export interface CustomerQueryArgs {
+  
+  id: string;
+}
 export interface FacetsQueryArgs {
   
   languageCode?: Maybe<LanguageCode>;
@@ -5871,14 +5897,6 @@ export interface FacetQueryArgs {
   
   languageCode?: Maybe<LanguageCode>;
 }
-export interface CustomersQueryArgs {
-  
-  options?: Maybe<CustomerListOptions>;
-}
-export interface CustomerQueryArgs {
-  
-  id: string;
-}
 export interface OrderQueryArgs {
   
   id: string;
@@ -5907,6 +5925,10 @@ export interface ProductOptionGroupQueryArgs {
   
   languageCode?: Maybe<LanguageCode>;
 }
+export interface SearchQueryArgs {
+  
+  input: SearchInput;
+}
 export interface ProductsQueryArgs {
   
   languageCode?: Maybe<LanguageCode>;
@@ -5919,10 +5941,6 @@ export interface ProductQueryArgs {
   
   languageCode?: Maybe<LanguageCode>;
 }
-export interface SearchQueryArgs {
-  
-  input: SearchInput;
-}
 export interface PromotionQueryArgs {
   
   id: string;
@@ -6049,34 +6067,6 @@ export interface RemoveCustomersFromGroupMutationArgs {
   
   customerIds: string[];
 }
-export interface CreateFacetMutationArgs {
-  
-  input: CreateFacetInput;
-}
-export interface UpdateFacetMutationArgs {
-  
-  input: UpdateFacetInput;
-}
-export interface DeleteFacetMutationArgs {
-  
-  id: string;
-  
-  force?: Maybe<boolean>;
-}
-export interface CreateFacetValuesMutationArgs {
-  
-  input: CreateFacetValueInput[];
-}
-export interface UpdateFacetValuesMutationArgs {
-  
-  input: UpdateFacetValueInput[];
-}
-export interface DeleteFacetValuesMutationArgs {
-  
-  ids: string[];
-  
-  force?: Maybe<boolean>;
-}
 export interface CreateCustomerMutationArgs {
   
   input: CreateCustomerInput;
@@ -6105,6 +6095,34 @@ export interface DeleteCustomerAddressMutationArgs {
   
   id: string;
 }
+export interface CreateFacetMutationArgs {
+  
+  input: CreateFacetInput;
+}
+export interface UpdateFacetMutationArgs {
+  
+  input: UpdateFacetInput;
+}
+export interface DeleteFacetMutationArgs {
+  
+  id: string;
+  
+  force?: Maybe<boolean>;
+}
+export interface CreateFacetValuesMutationArgs {
+  
+  input: CreateFacetValueInput[];
+}
+export interface UpdateFacetValuesMutationArgs {
+  
+  input: UpdateFacetValueInput[];
+}
+export interface DeleteFacetValuesMutationArgs {
+  
+  ids: string[];
+  
+  force?: Maybe<boolean>;
+}
 export interface UpdateGlobalSettingsMutationArgs {
   
   input: UpdateGlobalSettingsInput;

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

@@ -0,0 +1,203 @@
+/* tslint:disable:no-non-null-assertion */
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { CREATE_COLLECTION } from '../../../admin-ui/src/app/data/definitions/collection-definitions';
+import { GET_PRODUCT_WITH_VARIANTS, UPDATE_PRODUCT_VARIANTS } from '../../../admin-ui/src/app/data/definitions/product-definitions';
+import { ConfigArgType, CreateCollection, LanguageCode } from '../../common/lib/generated-types';
+import { GetProductWithVariants, UpdateProductVariants } from '../../common/src/generated-types';
+import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { TestAdminClient, TestShopClient } from './test-client';
+import { TestServer } from './test-server';
+
+describe('Shop catalog', () => {
+    const shopClient = new TestShopClient();
+    const adminClient = new TestAdminClient();
+    const server = new TestServer();
+
+    beforeAll(async () => {
+        const token = await server.init({
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await shopClient.init();
+        await adminClient.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('products', () => {
+
+        beforeAll(async () => {
+            // disable the first product
+            await adminClient.query(DISABLE_PRODUCT, { id: 'T_1' });
+
+            const monitorProduct = await adminClient
+                .query<GetProductWithVariants.Query, GetProductWithVariants.Variables>(GET_PRODUCT_WITH_VARIANTS, {
+                    id: 'T_2',
+                });
+            if (monitorProduct.product) {
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(UPDATE_PRODUCT_VARIANTS, {
+                    input: [
+                        {
+                            id: monitorProduct.product.variants[0].id,
+                            enabled: false,
+                        },
+                    ],
+                });
+            }
+        });
+
+        it('products list omits disabled products', async () => {
+            const result = await shopClient.query(gql`{
+                products(options: { take: 3 }) {
+                    items { id }
+                }
+            }`);
+
+            expect(result.products.items.map((item: any) => item.id)).toEqual([ 'T_2', 'T_3', 'T_4']);
+        });
+
+        it('product returns null for disabled product', async () => {
+            const result = await shopClient.query(gql`{
+                product(id: "T_1") { id }
+            }`);
+
+            expect(result.product).toBeNull();
+        });
+
+        it('omits disabled variants from product response', async () => {
+            const result = await shopClient.query(gql`{
+                product(id: "T_2") {
+                    id
+                    variants {
+                        id
+                        name
+                    }
+                }
+            }`);
+
+            expect(result.product.variants).toEqual([
+                { id: 'T_6', name: 'Curvy Monitor 27 inch'},
+            ]);
+        });
+    });
+
+    describe('collections', () => {
+
+        let collection: CreateCollection.CreateCollection;
+
+        beforeAll(async () => {
+            const result = await adminClient.query(gql`{
+                facets {
+                    items {
+                        id
+                        name
+                        values {
+                            id
+                        }
+                    }
+                }
+            }`);
+            const category = result.facets.items[0];
+            const { createCollection } = await adminClient.query<CreateCollection.Mutation, CreateCollection.Variables>(
+                CREATE_COLLECTION,
+                {
+                    input: {
+                        filters: [
+                            {
+                                code: facetValueCollectionFilter.code,
+                                arguments: [
+                                    {
+                                        name: 'facetValueIds',
+                                        value: `["${category.values[3].id}"]`,
+                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                    },
+                                ],
+                            },
+                        ],
+                        translations: [
+                            { languageCode: LanguageCode.en, name: 'My Collection', description: '' },
+                        ],
+                    },
+                },
+            );
+            collection = createCollection;
+        });
+
+        it('returns collection with variants', async () => {
+            const result = await shopClient.query(GET_COLLECTION_VARIANTS, { id: collection.id });
+            expect(result.collection.productVariants.items).toEqual([
+                { id: 'T_22', name: 'Road Bike' },
+                { id: 'T_23', name: 'Skipping Rope' },
+                { id: 'T_24', name: 'Boxing Gloves' },
+                { id: 'T_25', name: 'Tent' },
+                { id: 'T_26', name: 'Cruiser Skateboard' },
+                { id: 'T_27', name: 'Football' },
+                { id: 'T_28', name: 'Running Shoe Size 40' },
+                { id: 'T_29', name: 'Running Shoe Size 42' },
+                { id: 'T_30', name: 'Running Shoe Size 44' },
+                { id: 'T_31', name: 'Running Shoe Size 46' },
+            ]);
+        });
+
+        it('omits variants from disabled products', async () => {
+            await adminClient.query(DISABLE_PRODUCT, { id: 'T_17' });
+
+            const result = await shopClient.query(GET_COLLECTION_VARIANTS, { id: collection.id });
+            expect(result.collection.productVariants.items).toEqual([
+                { id: 'T_22', name: 'Road Bike' },
+                { id: 'T_23', name: 'Skipping Rope' },
+                { id: 'T_24', name: 'Boxing Gloves' },
+                { id: 'T_25', name: 'Tent' },
+                { id: 'T_26', name: 'Cruiser Skateboard' },
+                { id: 'T_27', name: 'Football' },
+            ]);
+        });
+
+        it('omits variants from disabled products', async () => {
+            await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(UPDATE_PRODUCT_VARIANTS, {
+                input: [
+                    { id: 'T_22', enabled: false },
+                ],
+            });
+
+            const result = await shopClient.query(GET_COLLECTION_VARIANTS, { id: collection.id });
+            expect(result.collection.productVariants.items).toEqual([
+                { id: 'T_23', name: 'Skipping Rope' },
+                { id: 'T_24', name: 'Boxing Gloves' },
+                { id: 'T_25', name: 'Tent' },
+                { id: 'T_26', name: 'Cruiser Skateboard' },
+                { id: 'T_27', name: 'Football' },
+            ]);
+        });
+    });
+});
+
+const DISABLE_PRODUCT = gql`
+    mutation DisableProduct($id: ID!) {
+        updateProduct(input: {
+            id: $id
+            enabled: false
+        }) {
+            id
+        }
+    }
+`;
+
+const GET_COLLECTION_VARIANTS = gql`
+    query GetCollectionVariants($id: ID!) {
+        collection(id: $id) {
+            productVariants {
+                items {
+                    id
+                    name
+                }
+            }
+        }
+    }
+`;

+ 17 - 0
packages/core/src/api/decorators/api.decorator.ts

@@ -0,0 +1,17 @@
+import { createParamDecorator } from '@nestjs/common';
+import { GraphQLResolveInfo } from 'graphql';
+
+export type ApiType = 'admin' | 'shop';
+
+/**
+ * Resolver param decorator which returns which Api the request came though.
+ * This is useful because sometimes the same resolver will have different behaviour
+ * depending whether it is being called from the shop API or the admin API.
+ */
+export const Api = createParamDecorator((data, [root, args, ctx, info]) => {
+    const query = (info as GraphQLResolveInfo).schema.getQueryType();
+    if (query) {
+        return !!query.getFields().administrators ? 'admin' : 'shop';
+    }
+    return 'shop';
+});

+ 15 - 2
packages/core/src/api/resolvers/entity/collection-entity.resolver.ts

@@ -2,11 +2,13 @@ import { Args, Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 import { CollectionBreadcrumb, ProductVariantListOptions } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
+import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
-import { Collection, ProductVariant } from '../../../entity';
+import { Collection, Product, ProductVariant } from '../../../entity';
 import { CollectionService } from '../../../service/services/collection.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { RequestContext } from '../../common/request-context';
+import { Api, ApiType } from '../../decorators/api.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Collection')
@@ -21,8 +23,19 @@ export class CollectionEntityResolver {
         @Ctx() ctx: RequestContext,
         @Parent() collection: Collection,
         @Args() args: { options: ProductVariantListOptions },
+        @Api() apiType: ApiType,
     ): Promise<PaginatedList<Translated<ProductVariant>>> {
-        return this.productVariantService.getVariantsByCollectionId(ctx, collection.id, args.options);
+        let options: ListQueryOptions<Product> = args.options;
+        if (apiType === 'shop') {
+            options = {
+                ...args.options,
+                filter: {
+                    ...(args.options ? args.options.filter : {}),
+                    enabled: { eq: true },
+                },
+            };
+        }
+        return this.productVariantService.getVariantsByCollectionId(ctx, collection.id, options);
     }
 
     @ResolveProperty()

+ 6 - 1
packages/core/src/api/resolvers/entity/product-entity.resolver.ts

@@ -7,6 +7,7 @@ import { Product } from '../../../entity/product/product.entity';
 import { CollectionService } from '../../../service/services/collection.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { RequestContext } from '../../common/request-context';
+import { Api, ApiType } from '../../decorators/api.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Product')
@@ -20,8 +21,12 @@ export class ProductEntityResolver {
     async variants(
         @Ctx() ctx: RequestContext,
         @Parent() product: Product,
+        @Api() apiType: ApiType,
     ): Promise<Array<Translated<ProductVariant>>> {
-        return this.productVariantService.getVariantsByProductId(ctx, product.id);
+        // In the shop URL, the parent (Product type) does not have the "enabled"
+        // field, in which case we should filter out any non-enabled variants too.
+        const variants = await this.productVariantService.getVariantsByProductId(ctx, product.id);
+        return variants.filter(v => apiType === 'admin' ? true : v.enabled);
     }
 
     @ResolveProperty()

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

@@ -10,6 +10,7 @@ import { Omit } from '@vendure/common/lib/omit';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { InternalServerError } from '../../../common/error/errors';
+import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
 import { Collection } from '../../../entity/collection/collection.entity';
 import { Product } from '../../../entity/product/product.entity';
@@ -34,7 +35,20 @@ export class ShopProductsResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: ProductsQueryArgs,
     ): Promise<PaginatedList<Translated<Product>>> {
-        return this.productService.findAll(ctx, args.options || undefined);
+
+        let options: ListQueryOptions<Product>;
+        if (args.options) {
+            options = {
+                ...args.options,
+                filter: {
+                    ...args.options.filter,
+                    enabled: { eq: true },
+                },
+            };
+        } else {
+            options = {};
+        }
+        return this.productService.findAll(ctx, options);
     }
 
     @Query()
@@ -42,7 +56,14 @@ export class ShopProductsResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: ProductQueryArgs,
     ): Promise<Translated<Product> | undefined> {
-        return this.productService.findOne(ctx, args.id);
+        const result = await this.productService.findOne(ctx, args.id);
+        if (!result) {
+            return;
+        }
+        if (result.enabled === false) {
+            return;
+        }
+        return result;
     }
 
     @Query()

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

@@ -5,3 +5,7 @@ type Query {
 type Mutation {
     reindex: SearchReindexResponse!
 }
+
+type SearchResult {
+    enabled: Boolean!
+}

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

@@ -26,6 +26,14 @@ type Mutation {
     updateProductVariants(input: [UpdateProductVariantInput!]!): [ProductVariant]!
 }
 
+type Product {
+    enabled: Boolean!
+}
+
+type ProductVariant {
+    enabled: Boolean!
+}
+
 # generated by generateListOptions function
 input ProductListOptions
 
@@ -46,6 +54,7 @@ input CreateProductInput {
 
 input UpdateProductInput {
     id: ID!
+    enabled: Boolean
     featuredAssetId: ID
     assetIds: [ID!]
     facetValueIds: [ID!]
@@ -71,6 +80,7 @@ input CreateProductVariantInput {
 
 input UpdateProductVariantInput {
     id: ID!
+    enabled: Boolean
     translations: [ProductVariantTranslationInput!]
     facetValueIds: [ID!]
     sku: String

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

@@ -33,7 +33,11 @@ export class ProductVariant extends VendureEntity implements Translatable, HasCu
 
     name: LocaleString;
 
-    @Column() sku: string;
+    @Column({ default: true })
+    enabled: boolean;
+
+    @Column()
+    sku: string;
 
     /**
      * A synthetic property which is populated with data from a ProductVariantPrice entity.

+ 3 - 0
packages/core/src/entity/product/product.entity.ts

@@ -36,6 +36,9 @@ export class Product extends VendureEntity
 
     description: LocaleString;
 
+    @Column({ default: true })
+    enabled: boolean;
+
     @ManyToOne(type => Asset)
     featuredAsset: Asset;
 

+ 9 - 3
packages/core/src/service/services/product-variant.service.ts

@@ -93,14 +93,20 @@ export class ProductVariantService {
         collectionId: ID,
         options: ListQueryOptions<ProductVariant>,
     ): Promise<PaginatedList<Translated<ProductVariant>>> {
-        return this.listQueryBuilder
+        const qb = this.listQueryBuilder
             .build(ProductVariant, options, {
                 relations: ['taxCategory'],
                 channelId: ctx.channelId,
             })
             .leftJoin('productvariant.collections', 'collection')
-            .andWhere('collection.id = :collectionId', { collectionId })
-            .getManyAndCount()
+            .andWhere('collection.id = :collectionId', { collectionId });
+
+        if (options.filter && options.filter.enabled && options.filter.enabled.eq === true) {
+            qb.leftJoin('productvariant.product', 'product')
+                .andWhere('product.enabled = :enabled', { enabled: true });
+        }
+
+        return qb.getManyAndCount()
             .then(async ([variants, totalItems]) => {
                 const items = variants.map(variant => {
                     const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);

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


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


File diff suppressed because it is too large
+ 420 - 453
schema.json


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