فهرست منبع

feat(server): Expose price ranges in search results

Relates to #82
Michael Bromley 6 سال پیش
والد
کامیت
651f6e9120

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-admin.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-shop.json


+ 96 - 44
schema.json

@@ -298,22 +298,6 @@
             "isDeprecated": false,
             "deprecationReason": null
           },
-          {
-            "name": "config",
-            "description": null,
-            "args": [],
-            "type": {
-              "kind": "NON_NULL",
-              "name": null,
-              "ofType": {
-                "kind": "OBJECT",
-                "name": "Config",
-                "ofType": null
-              }
-            },
-            "isDeprecated": false,
-            "deprecationReason": null
-          },
           {
             "name": "countries",
             "description": null,
@@ -8531,29 +8515,6 @@
         "enumValues": null,
         "possibleTypes": null
       },
-      {
-        "kind": "OBJECT",
-        "name": "Config",
-        "description": null,
-        "fields": [
-          {
-            "name": "customFields",
-            "description": null,
-            "args": [],
-            "type": {
-              "kind": "SCALAR",
-              "name": "JSON",
-              "ofType": null
-            },
-            "isDeprecated": false,
-            "deprecationReason": null
-          }
-        ],
-        "inputFields": null,
-        "interfaces": [],
-        "enumValues": null,
-        "possibleTypes": null
-      },
       {
         "kind": "INPUT_OBJECT",
         "name": "CountryListOptions",
@@ -12318,8 +12279,8 @@
               "kind": "NON_NULL",
               "name": null,
               "ofType": {
-                "kind": "SCALAR",
-                "name": "Int",
+                "kind": "UNION",
+                "name": "SearchResultPrice",
                 "ofType": null
               }
             },
@@ -12408,7 +12369,7 @@
           },
           {
             "name": "collectionIds",
-            "description": null,
+            "description": "An array of ids of the Collections in which this result appears",
             "args": [],
             "type": {
               "kind": "NON_NULL",
@@ -12432,7 +12393,7 @@
           },
           {
             "name": "score",
-            "description": null,
+            "description": "A relevence score for the result. Differs between database implementations.",
             "args": [],
             "type": {
               "kind": "NON_NULL",
@@ -12452,10 +12413,101 @@
         "enumValues": null,
         "possibleTypes": null
       },
+      {
+        "kind": "UNION",
+        "name": "SearchResultPrice",
+        "description": null,
+        "fields": null,
+        "inputFields": null,
+        "interfaces": null,
+        "enumValues": null,
+        "possibleTypes": [
+          {
+            "kind": "OBJECT",
+            "name": "PriceRange",
+            "ofType": null
+          },
+          {
+            "kind": "OBJECT",
+            "name": "SinglePrice",
+            "ofType": null
+          }
+        ]
+      },
       {
         "kind": "OBJECT",
-        "name": "FacetValueResult",
+        "name": "PriceRange",
+        "description": null,
+        "fields": [
+          {
+            "name": "min",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "Int",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "max",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "Int",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": [],
+        "enumValues": null,
+        "possibleTypes": null
+      },
+      {
+        "kind": "OBJECT",
+        "name": "SinglePrice",
         "description": null,
+        "fields": [
+          {
+            "name": "value",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "Int",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": [],
+        "enumValues": null,
+        "possibleTypes": null
+      },
+      {
+        "kind": "OBJECT",
+        "name": "FacetValueResult",
+        "description": "Which FacetValues are present in the products returned\nby the search, and in what quantity.",
         "fields": [
           {
             "name": "facetValue",

+ 55 - 0
server/e2e/default-search-plugin.e2e-spec.ts

@@ -10,6 +10,7 @@ import {
     ConfigArgType,
     CreateCollection,
     LanguageCode,
+    SearchInput,
     SearchProducts,
     UpdateCollection,
     UpdateProduct,
@@ -108,6 +109,34 @@ describe('Default search plugin', () => {
         ]);
     }
 
+    async function testSinglePrices(client: SimpleGraphQLClient) {
+        const result = await client.query(SEARCH_GET_PRICES, {
+            input: {
+                groupByProduct: false,
+                take: 3,
+            } as SearchInput,
+        });
+        expect(result.search.items.map(i => i.price)).toEqual([
+            { value: 129900 },
+            { value: 139900 },
+            { value: 219900 },
+        ]);
+    }
+
+    async function testPriceRanges(client: SimpleGraphQLClient) {
+        const result = await client.query(SEARCH_GET_PRICES, {
+            input: {
+                groupByProduct: true,
+                take: 3,
+            } as SearchInput,
+        });
+        expect(result.search.items.map(i => i.price)).toEqual([
+            { min: 129900, max: 229900 },
+            { min: 14374, max: 16994 },
+            { min: 93120, max: 109995 },
+        ]);
+    }
+
     describe('shop api', () => {
         it('group by product', () => testGroupByProduct(shopClient));
 
@@ -119,6 +148,10 @@ describe('Default search plugin', () => {
 
         it('matches by collectionId', () => testMatchCollectionId(shopClient));
 
+        it('single prices', () => testSinglePrices(shopClient));
+
+        it('price ranges', () => testPriceRanges(shopClient));
+
         it('returns correct facetValues when not grouped by product', async () => {
             const result = await shopClient.query(SEARCH_GET_FACET_VALUES, {
                 input: {
@@ -163,6 +196,10 @@ describe('Default search plugin', () => {
 
         it('matches by collectionId', () => testMatchCollectionId(adminClient));
 
+        it('single prices', () => testSinglePrices(shopClient));
+
+        it('price ranges', () => testPriceRanges(shopClient));
+
         it('updates index when a Product is changed', async () => {
             await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 input: {
@@ -292,3 +329,21 @@ export const SEARCH_GET_FACET_VALUES = gql`
         }
     }
 `;
+
+export const SEARCH_GET_PRICES = gql`
+    query SearchGetPrices($input: SearchInput!) {
+        search(input: $input) {
+            items {
+                price {
+                    ... on PriceRange {
+                        min
+                        max
+                    }
+                    ... on SinglePrice {
+                        value
+                    }
+                }
+            }
+        }
+    }
+`;

+ 5 - 0
server/src/api/config/configure-graphql-module.ts

@@ -77,6 +77,11 @@ function createGraphQLOptions(
             Node: dummyResolveType,
             PaginatedList: dummyResolveType,
             Upload: GraphQLUpload || dummyResolveType,
+            SearchResultPrice: {
+                __resolveType(value: any) {
+                    return value.hasOwnProperty('value') ? 'SinglePrice' : 'PriceRange';
+                },
+            },
         },
         uploads: {
             maxFileSize: configService.assetOptions.uploadMaxFileSize,

+ 14 - 1
server/src/api/schema/type/product-search.type.graphql

@@ -26,7 +26,7 @@ type SearchResult {
     productVariantId: ID!
     productVariantName: String!
     productVariantPreview: String!
-    price: Int!
+    price: SearchResultPrice!
     currencyCode: CurrencyCode!
     description: String!
     facetIds: [String!]!
@@ -36,3 +36,16 @@ type SearchResult {
     "A relevence score for the result. Differs between database implementations."
     score: Float!
 }
+
+union SearchResultPrice = PriceRange | SinglePrice
+
+"The price value where the result has a single price"
+type SinglePrice {
+    value: Int!
+}
+
+"The price range where the result has more than one price"
+type PriceRange {
+    min: Int!
+    max: Int!
+}

+ 3 - 0
server/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -37,6 +37,9 @@ export class MysqlSearchStrategy implements SearchStrategy {
         const skip = input.skip || 0;
         const sort = input.sort;
         const qb = this.connection.getRepository(SearchIndexItem).createQueryBuilder('si');
+        if (input.groupByProduct) {
+            qb.addSelect('MIN(price)', 'minPrice').addSelect('MAX(price)', 'maxPrice');
+        }
         this.applyTermAndFilters(qb, input);
         if (input.term && input.term.length > this.minTermLength) {
             qb.orderBy('score', 'DESC');

+ 6 - 3
server/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -21,12 +21,12 @@ export class PostgresSearchStrategy implements SearchStrategy {
         const facetValuesQb = this.connection
             .getRepository(SearchIndexItem)
             .createQueryBuilder('si')
-            .select(['"si.productId"', '"si.productVariantId"'])
-            .addSelect(`string_agg(si.facetValueIds,',')`, 'facetValues');
+            .select(['"si"."productId"', 'MAX("si"."productVariantId")'])
+            .addSelect(`string_agg("si"."facetValueIds",',')`, 'facetValues');
 
         this.applyTermAndFilters(facetValuesQb, input, true);
         if (!input.groupByProduct) {
-            facetValuesQb.groupBy('si.productVariantId');
+            facetValuesQb.groupBy('"si"."productVariantId", "si"."productId"');
         }
         const facetValuesResult = await facetValuesQb.getRawMany();
         return createFacetIdCountMap(facetValuesResult);
@@ -40,6 +40,9 @@ export class PostgresSearchStrategy implements SearchStrategy {
             .getRepository(SearchIndexItem)
             .createQueryBuilder('si')
             .select(this.createPostgresSelect(!!input.groupByProduct));
+        if (input.groupByProduct) {
+            qb.addSelect('MIN(price)', 'minPrice').addSelect('MAX(price)', 'maxPrice');
+        }
         this.applyTermAndFilters(qb, input);
         if (input.term && input.term.length > this.minTermLength) {
             qb.orderBy('score', 'DESC');

+ 6 - 2
server/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts

@@ -1,4 +1,4 @@
-import { CurrencyCode, SearchResult } from '../../../../../shared/generated-types';
+import { CurrencyCode, PriceRange, SearchResult, SinglePrice } from '../../../../../shared/generated-types';
 import { ID } from '../../../../../shared/shared-types';
 import { unique } from '../../../../../shared/unique';
 
@@ -6,10 +6,14 @@ import { unique } from '../../../../../shared/unique';
  * Maps a raw database result to a SearchResult.
  */
 export function mapToSearchResult(raw: any, currencyCode: CurrencyCode): SearchResult {
+    const price =
+        raw.minPrice !== undefined
+            ? ({ min: raw.minPrice, max: raw.maxPrice } as PriceRange)
+            : ({ value: raw.si_price } as SinglePrice);
     return {
         sku: raw.si_sku,
         slug: raw.si_slug,
-        price: raw.si_price,
+        price,
         currencyCode,
         productVariantId: raw.si_productVariantId,
         productId: raw.si_productId,

+ 3 - 0
server/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -38,6 +38,9 @@ export class SqliteSearchStrategy implements SearchStrategy {
         const skip = input.skip || 0;
         const sort = input.sort;
         const qb = this.connection.getRepository(SearchIndexItem).createQueryBuilder('si');
+        if (input.groupByProduct) {
+            qb.addSelect('MIN(price)', 'minPrice').addSelect('MAX(price)', 'maxPrice');
+        }
         this.applyTermAndFilters(qb, input);
         if (input.term && input.term.length > this.minTermLength) {
             qb.orderBy('score', 'DESC');

+ 94 - 75
shared/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-21T15:11:51+01:00
+// Generated in 2019-03-25T12:30:38+01:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -550,17 +550,6 @@ export enum LanguageCode {
     za = 'za',
     zu = 'zu',
 }
-
-export enum SortOrder {
-    ASC = 'ASC',
-    DESC = 'DESC',
-}
-
-export enum AssetType {
-    IMAGE = 'IMAGE',
-    VIDEO = 'VIDEO',
-    BINARY = 'BINARY',
-}
 /** ISO 4217 currency code */
 export enum CurrencyCode {
     AED = 'AED',
@@ -722,6 +711,17 @@ export enum CurrencyCode {
     ZWL = 'ZWL',
 }
 
+export enum SortOrder {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export enum AssetType {
+    IMAGE = 'IMAGE',
+    VIDEO = 'VIDEO',
+    BINARY = 'BINARY',
+}
+
 export enum AdjustmentType {
     TAX = 'TAX',
     PROMOTION = 'PROMOTION',
@@ -806,6 +806,8 @@ export interface PaginatedList {
 // ====================================================
 
 export interface Query {
+    activeChannel: Channel;
+
     activeCustomer?: Maybe<Customer>;
 
     activeOrder?: Maybe<Order>;
@@ -835,6 +837,66 @@ export interface Query {
     temp__?: Maybe<boolean>;
 }
 
+export interface Channel extends Node {
+    id: string;
+
+    createdAt: DateTime;
+
+    updatedAt: DateTime;
+
+    code: string;
+
+    token: string;
+
+    defaultTaxZone?: Maybe<Zone>;
+
+    defaultShippingZone?: Maybe<Zone>;
+
+    defaultLanguageCode: LanguageCode;
+
+    currencyCode: CurrencyCode;
+
+    pricesIncludeTax: boolean;
+}
+
+export interface Zone extends Node {
+    id: string;
+
+    createdAt: DateTime;
+
+    updatedAt: DateTime;
+
+    name: string;
+
+    members: Country[];
+}
+
+export interface Country extends Node {
+    id: string;
+
+    languageCode: LanguageCode;
+
+    code: string;
+
+    name: string;
+
+    enabled: boolean;
+
+    translations: CountryTranslation[];
+}
+
+export interface CountryTranslation {
+    id: string;
+
+    createdAt: DateTime;
+
+    updatedAt: DateTime;
+
+    languageCode: LanguageCode;
+
+    name: string;
+}
+
 export interface Customer extends Node {
     id: string;
 
@@ -893,32 +955,6 @@ export interface Address extends Node {
     customFields?: Maybe<Json>;
 }
 
-export interface Country extends Node {
-    id: string;
-
-    languageCode: LanguageCode;
-
-    code: string;
-
-    name: string;
-
-    enabled: boolean;
-
-    translations: CountryTranslation[];
-}
-
-export interface CountryTranslation {
-    id: string;
-
-    createdAt: DateTime;
-
-    updatedAt: DateTime;
-
-    languageCode: LanguageCode;
-
-    name: string;
-}
-
 export interface OrderList extends PaginatedList {
     items: Order[];
 
@@ -1099,18 +1135,6 @@ export interface TaxCategory extends Node {
     name: string;
 }
 
-export interface Zone extends Node {
-    id: string;
-
-    createdAt: DateTime;
-
-    updatedAt: DateTime;
-
-    name: string;
-
-    members: Country[];
-}
-
 export interface CustomerGroup extends Node {
     id: string;
 
@@ -1339,28 +1363,6 @@ export interface Role extends Node {
     channels: Channel[];
 }
 
-export interface Channel extends Node {
-    id: string;
-
-    createdAt: DateTime;
-
-    updatedAt: DateTime;
-
-    code: string;
-
-    token: string;
-
-    defaultTaxZone?: Maybe<Zone>;
-
-    defaultShippingZone?: Maybe<Zone>;
-
-    defaultLanguageCode: LanguageCode;
-
-    currencyCode: CurrencyCode;
-
-    pricesIncludeTax: boolean;
-}
-
 export interface CollectionList extends PaginatedList {
     items: Collection[];
 
@@ -1554,7 +1556,7 @@ export interface SearchResult {
 
     productVariantPreview: string;
 
-    price: number;
+    price: SearchResultPrice;
 
     currencyCode: CurrencyCode;
 
@@ -1563,12 +1565,23 @@ export interface SearchResult {
     facetIds: string[];
 
     facetValueIds: string[];
-
+    /** An array of ids of the Collections in which this result appears */
     collectionIds: string[];
-
+    /** A relevence score for the result. Differs between database implementations. */
     score: number;
 }
 
+export interface PriceRange {
+    min: number;
+
+    max: number;
+}
+
+export interface SinglePrice {
+    value: number;
+}
+
+/** Which FacetValues are present in the products returned by the search, and in what quantity. */
 export interface FacetValueResult {
     facetValue: FacetValue;
 
@@ -1877,3 +1890,9 @@ export interface ResetPasswordMutationArgs {
 
     password: string;
 }
+
+// ====================================================
+// Unions
+// ====================================================
+
+export type SearchResultPrice = PriceRange | SinglePrice;

+ 27 - 12
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-21T15:11:52+01:00
+// Generated in 2019-03-25T12:30:39+01:00
 export type Maybe<T> = T | null;
 
 
@@ -4538,8 +4538,6 @@ export interface Query {
   
   collectionFilters: ConfigurableOperation[];
   
-  config: Config;
-  
   countries: CountryList;
   
   country?: Maybe<Country>;
@@ -5088,12 +5086,6 @@ export interface ProductVariantTranslation {
 }
 
 
-export interface Config {
-  
-  customFields?: Maybe<Json>;
-}
-
-
 export interface CountryList extends PaginatedList {
   
   items: Country[];
@@ -5462,7 +5454,7 @@ export interface SearchResult {
   
   productVariantPreview: string;
   
-  price: number;
+  price: SearchResultPrice;
   
   currencyCode: CurrencyCode;
   
@@ -5471,13 +5463,27 @@ export interface SearchResult {
   facetIds: string[];
   
   facetValueIds: string[];
-  
+  /** An array of ids of the Collections in which this result appears */
   collectionIds: string[];
-  
+  /** A relevence score for the result. Differs between database implementations. */
   score: number;
 }
 
 
+export interface PriceRange {
+  
+  min: number;
+  
+  max: number;
+}
+
+
+export interface SinglePrice {
+  
+  value: number;
+}
+
+/** Which FacetValues are present in the products returned by the search, and in what quantity. */
 export interface FacetValueResult {
   
   facetValue: FacetValue;
@@ -6235,3 +6241,12 @@ export interface SetUiLanguageMutationArgs {
 }
 
 
+// ====================================================
+// Unions
+// ====================================================
+
+
+
+export type SearchResultPrice = PriceRange | SinglePrice;
+
+

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است