Browse Source

feat(core): Implement private Facets

Relates to #80
Michael Bromley 6 years ago
parent
commit
b6c3240596

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

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-04-25T13:30:55+02:00
+// Generated in 2019-04-25T20:45:59+02:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {

File diff suppressed because it is too large
+ 323 - 313
packages/common/src/generated-types.ts


+ 1 - 0
packages/core/e2e/__snapshots__/facet.e2e-spec.ts.snap

@@ -4,6 +4,7 @@ exports[`Facet resolver createFacet 1`] = `
 Object {
   "code": "speaker-type",
   "id": "T_2",
+  "isPrivate": false,
   "languageCode": "en",
   "name": "Speaker Type",
   "translations": Array [

+ 38 - 1
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -13,9 +13,10 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { CREATE_COLLECTION, UPDATE_COLLECTION } from '../../../admin-ui/src/app/data/definitions/collection-definitions';
+import { CREATE_FACET } from '../../../admin-ui/src/app/data/definitions/facet-definitions';
 import { SEARCH_PRODUCTS, UPDATE_PRODUCT, UPDATE_PRODUCT_VARIANTS } from '../../../admin-ui/src/app/data/definitions/product-definitions';
 import { UPDATE_TAX_RATE } from '../../../admin-ui/src/app/data/definitions/settings-definitions';
-import { UpdateProductVariants } from '../../common/src/generated-types';
+import { CreateFacet, UpdateProductVariants } from '../../common/src/generated-types';
 import { SimpleGraphQLClient } from '../mock-data/simple-graphql-client';
 import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
 import { DefaultSearchPlugin } from '../src/plugin/default-search-plugin/default-search-plugin';
@@ -203,6 +204,42 @@ describe('Default search plugin', () => {
             ]);
         });
 
+        it('omits facetValues of private facets', async () => {
+            const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
+                input: {
+                    code: 'profit-margin',
+                    isPrivate: true,
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'Profit Margin' },
+                    ],
+                    values: [
+                        { code: 'massive', translations: [{ languageCode: LanguageCode.en, name: 'massive' }] },
+                    ],
+                },
+            });
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                input: {
+                    id: 'T_2',
+                    // T_1 & T_2 are the existing facetValues (electronics & photo)
+                    facetValueIds: ['T_1', 'T_2', createFacet.values[0].id],
+                },
+            });
+
+            const result = await shopClient.query(SEARCH_GET_FACET_VALUES, {
+                input: {
+                    groupByProduct: true,
+                },
+            });
+            expect(result.search.facetValues).toEqual([
+                { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
+            ]);
+        });
+
         it('encodes the productId and productVariantId', async () => {
             const result = await shopClient.query<SearchProducts.Query, SearchProducts.Variables>(SEARCH_PRODUCTS_SHOP, {
                 input: {

+ 1 - 0
packages/core/e2e/facet.e2e-spec.ts

@@ -58,6 +58,7 @@ describe('Facet resolver', () => {
     it('createFacet', async () => {
         const result = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
             input: {
+                isPrivate: false,
                 code: 'speaker-type',
                 translations: [{ languageCode: LanguageCode.en, name: 'Speaker Type' }],
                 values: [

+ 82 - 3
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -3,9 +3,10 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { CREATE_COLLECTION, UPDATE_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, UpdateCollection } from '../../common/lib/generated-types';
-import { GetProductWithVariants, UpdateProductVariants } from '../../common/src/generated-types';
+import { CREATE_FACET } from '../../../admin-ui/src/app/data/definitions/facet-definitions';
+import { GET_PRODUCT_WITH_VARIANTS, UPDATE_PRODUCT, UPDATE_PRODUCT_VARIANTS } from '../../../admin-ui/src/app/data/definitions/product-definitions';
+import { ConfigArgType, CreateCollection, FacetWithValues, UpdateCollection } from '../../common/lib/generated-types';
+import { CreateFacet, GetProductWithVariants, LanguageCode, UpdateProduct, UpdateProductVariants } from '../../common/src/generated-types';
 import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
@@ -85,6 +86,57 @@ describe('Shop catalog', () => {
                 { id: 'T_6', name: 'Curvy Monitor 27 inch'},
             ]);
         });
+
+    });
+
+    describe('facets', () => {
+        let facetValue: FacetWithValues.Values;
+
+        beforeAll(async () => {
+            const result = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
+                input: {
+                    code: 'profit-margin',
+                    isPrivate: true,
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'Profit Margin' },
+                    ],
+                    values: [
+                        { code: 'massive', translations: [{ languageCode: LanguageCode.en, name: 'massive' }] },
+                    ],
+                },
+            });
+            facetValue = result.createFacet.values[0];
+
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                input: {
+                    id: 'T_2',
+                    facetValueIds: [facetValue.id],
+                },
+            });
+
+            await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(UPDATE_PRODUCT_VARIANTS, {
+                input: [{
+                    id: 'T_6',
+                    facetValueIds: [facetValue.id],
+                }],
+            });
+        });
+
+        it('omits private Product.facetValues', async () => {
+            const result = await shopClient.query(GET_PRODUCT_FACET_VALUES, {
+                id: 'T_2',
+            });
+
+            expect(result.product!.facetValues.map((fv: any) => fv.name)).toEqual([]);
+        });
+
+        it('omits private ProductVariant.facetValues', async () => {
+            const result = await shopClient.query(GET_PRODUCT_VARIANT_FACET_VALUES, {
+                id: 'T_2',
+            });
+
+            expect(result.product!.variants[0].facetValues.map((fv: any) => fv.name)).toEqual([]);
+        });
     });
 
     describe('collections', () => {
@@ -252,3 +304,30 @@ const GET_COLLECTION_LIST = gql`{
         }
     }
 }`;
+
+const GET_PRODUCT_FACET_VALUES = gql`
+    query ($id: ID!){
+        product(id: $id) {
+            id
+            name
+            facetValues {
+                name
+            }
+        }
+    }
+`;
+
+const GET_PRODUCT_VARIANT_FACET_VALUES = gql`
+    query ($id: ID!){
+        product(id: $id) {
+            id
+            name
+            variants {
+                id
+                facetValues {
+                    name
+                }
+            }
+        }
+    }
+`;

+ 10 - 2
packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts

@@ -5,6 +5,7 @@ import { FacetValue, ProductOption } from '../../../entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 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('ProductVariant')
@@ -26,10 +27,17 @@ export class ProductVariantEntityResolver {
     async facetValues(
         @Ctx() ctx: RequestContext,
         @Parent() productVariant: ProductVariant,
+        @Api() apiType: ApiType,
     ): Promise<Array<Translated<FacetValue>>> {
+        let facetValues: Array<Translated<FacetValue>>;
         if (productVariant.facetValues) {
-            return productVariant.facetValues as Array<Translated<FacetValue>>;
+            facetValues = productVariant.facetValues as Array<Translated<FacetValue>>;
+        } else {
+            facetValues = await this.productVariantService.getFacetValuesForVariant(ctx, productVariant.id);
         }
-        return this.productVariantService.getFacetValuesForVariant(ctx, productVariant.id);
+        if (apiType === 'shop') {
+            facetValues = facetValues.filter(fv => !fv.facet.isPrivate);
+        }
+        return facetValues;
     }
 }

+ 1 - 0
packages/core/src/api/resolvers/shop/shop-products.resolver.ts

@@ -57,6 +57,7 @@ export class ShopProductsResolver {
         if (result.enabled === false) {
             return;
         }
+        result.facetValues = result.facetValues.filter(fv => !fv.facet.isPrivate) as any;
         return result;
     }
 

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

@@ -16,7 +16,7 @@ type Mutation {
 }
 
 type Collection {
-    isPrivate: Boolean
+    isPrivate: Boolean!
 }
 
 # generated by generateListOptions function

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

@@ -23,6 +23,10 @@ type Mutation {
     deleteFacetValues(ids: [ID!]!, force: Boolean): [DeletionResponse!]!
 }
 
+type Facet {
+    isPrivate: Boolean!
+}
+
 # generated by generateListOptions function
 input FacetListOptions
 
@@ -34,12 +38,14 @@ input FacetTranslationInput {
 
 input CreateFacetInput {
     code: String!
+    isPrivate: Boolean!
     translations: [FacetTranslationInput!]!
     values: [CreateFacetValueWithFacetInput!]
 }
 
 input UpdateFacetInput {
     id: ID!
+    isPrivate: Boolean
     code: String
     translations: [FacetTranslationInput!]
 }

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

@@ -27,6 +27,9 @@ export class Facet extends VendureEntity implements Translatable, HasCustomField
 
     name: LocaleString;
 
+    @Column({ default: false })
+    isPrivate: boolean;
+
     @Column({ unique: true })
     code: string;
 

+ 2 - 1
packages/core/src/plugin/default-search-plugin/fulltext-search.resolver.ts

@@ -32,7 +32,8 @@ export class ShopFulltextSearchResolver implements Omit<BaseSearchResolver, 'rei
         @Ctx() ctx: RequestContext,
         @Context() context: any,
     ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
-        return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input, true);
+        const facetValues = await this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input, true);
+        return facetValues.filter(i => !i.facetValue.facet.isPrivate);
     }
 }
 

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


File diff suppressed because it is too large
+ 412 - 445
schema.json


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