diff --git a/packages/client/src/builders/conditionBuilder.ts b/packages/client/src/builders/conditionBuilder.ts index 7aa30b9..70becc6 100644 --- a/packages/client/src/builders/conditionBuilder.ts +++ b/packages/client/src/builders/conditionBuilder.ts @@ -1,4 +1,6 @@ -import { ContainsCondition, DistinctCondition, EqualsCondition, GreaterThanCondition, LessThanCondition, ValueConditionCollection, DataValueBase, DataObjectFilterConditionBuilder, HasValueCondition, RelativeDateTimeCondition } from '..'; +import { ContainsCondition, DistinctCondition, EqualsCondition, GreaterThanCondition, LessThanCondition, HasValueCondition, RelativeDateTimeCondition, ValueConditionCollection } from '../models/data-contracts'; +import { DataValueBase } from '../models/dataValue'; +import { DataObjectFilterConditionBuilder } from './dataObjectFilterConditionBuilder'; export type Conditions = ContainsCondition | DistinctCondition | EqualsCondition | GreaterThanCondition | LessThanCondition | HasValueCondition | RelativeDateTimeCondition; diff --git a/packages/client/src/builders/relevanceModifierBuilder.ts b/packages/client/src/builders/relevanceModifierBuilder.ts index 2f23d31..91e98fa 100644 --- a/packages/client/src/builders/relevanceModifierBuilder.ts +++ b/packages/client/src/builders/relevanceModifierBuilder.ts @@ -1,4 +1,6 @@ -import { BrandIdRelevanceModifier, ConditionBuilder, ContentCategoryDataRelevanceModifier, ContentCategoryRecentlyViewedByUserRelevanceModifier, ContentDataRelevanceModifier, ContentRecentlyViewedByUserRelevanceModifier, DataDoubleSelector, FilterBuilder, FixedDoubleValueSelector, ProductAssortmentRelevanceModifier, ProductCategoryDataRelevanceModifier, ProductCategoryIdRelevanceModifier, ProductCategoryRecentlyViewedByUserRelevanceModifier, ProductDataRelevanceModifier, ProductIdRelevanceModifier, ProductListPriceRelevanceModifier, ProductRecentlyPurchasedByCompanyRelevanceModifier, ProductRecentlyPurchasedByUserCompanyRelevanceModifier, ProductRecentlyPurchasedByUserRelevanceModifier, ProductRecentlyViewedByCompanyRelevanceModifier, ProductRecentlyViewedByUserCompanyRelevanceModifier, ProductRecentlyViewedByUserRelevanceModifier, ProductSalesPriceRelevanceModifier, RelevanceModifierCollection, UserFavoriteProductRelevanceModifier, VariantAssortmentRelevanceModifier, VariantDataRelevanceModifier, VariantIdRelevanceModifier, VariantListPriceRelevanceModifier, VariantSalesPriceRelevanceModifier, VariantSpecificationsInCommonRelevanceModifier, VariantSpecificationValueRelevanceModifier } from '..'; +import { BrandIdRelevanceModifier, ContentCategoryDataRelevanceModifier, ContentCategoryRecentlyViewedByUserRelevanceModifier, ContentDataRelevanceModifier, ContentRecentlyViewedByUserRelevanceModifier, ProductAssortmentRelevanceModifier, ProductCategoryDataRelevanceModifier, ProductCategoryIdRelevanceModifier, ProductCategoryRecentlyViewedByUserRelevanceModifier, ProductDataRelevanceModifier, ProductIdRelevanceModifier, ProductListPriceRelevanceModifier, ProductRecentlyPurchasedByCompanyRelevanceModifier, ProductRecentlyPurchasedByUserCompanyRelevanceModifier, ProductRecentlyPurchasedByUserRelevanceModifier, ProductRecentlyViewedByCompanyRelevanceModifier, ProductRecentlyViewedByUserCompanyRelevanceModifier, ProductRecentlyViewedByUserRelevanceModifier, ProductSalesPriceRelevanceModifier, UserFavoriteProductRelevanceModifier, VariantAssortmentRelevanceModifier, VariantDataRelevanceModifier, VariantIdRelevanceModifier, VariantListPriceRelevanceModifier, VariantSalesPriceRelevanceModifier, VariantSpecificationsInCommonRelevanceModifier, VariantSpecificationValueRelevanceModifier, DataDoubleSelector, FixedDoubleValueSelector, RelevanceModifierCollection } from '../models/data-contracts'; +import { ConditionBuilder } from './conditionBuilder'; +import { FilterBuilder } from './filterBuilder'; export class RelevanceModifierBuilder { private modifiers: ( diff --git a/packages/client/src/builders/search/getProductFacet.ts b/packages/client/src/builders/search/getProductFacet.ts new file mode 100644 index 0000000..bb2fb23 --- /dev/null +++ b/packages/client/src/builders/search/getProductFacet.ts @@ -0,0 +1,248 @@ +import { BrandFacetResult, CategoryFacetResult, CategoryHierarchyFacetResult, PriceRangeFacetResult, PriceRangesFacetResult, ProductAssortmentFacetResult, ProductDataBooleanValueFacetResult, ProductDataDoubleRangeFacetResult, ProductDataDoubleRangesFacetResult, ProductDataDoubleValueFacetResult, ProductDataObjectFacetResult, ProductDataStringValueFacetResult, ProductFacetResult, VariantSpecificationFacetResult } from 'src/models/data-contracts'; + +export type DataSelectionStrategy = ProductDataDoubleRangeFacetResult['dataSelectionStrategy']; +export type PriceSelectionStrategy = PriceRangeFacetResult['priceSelectionStrategy']; + +export class GetProductFacet { + public static productAssortment(facets: ProductFacetResult, selectionStrategy: 'Product' | 'Variant' | 'VariantWithFallbackToProduct' | 'ProductWithFallbackToVariant'): ProductAssortmentFacetResult | null { + if (!facets?.items) return null; + + return facets + .items + .find((item): item is ProductAssortmentFacetResult => + '$type' in item && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.ProductAssortmentFacetResult, Relewise.Client' && + 'field' in item && + item.field === 'Assortment' && + 'assortmentSelectionStrategy' in item && + item.assortmentSelectionStrategy === selectionStrategy) || null; + } + + public static brand(facets: ProductFacetResult): BrandFacetResult | null { + if (!facets?.items) return null; + + return facets + .items + .find((item): item is BrandFacetResult => + '$type' in item && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.BrandFacetResult, Relewise.Client' && + 'field' in item && + item.field === 'Brand') || null; + } + + public static category(facets: ProductFacetResult, selectionStrategy: 'ImmediateParent' | 'Ancestors' | 'Descendants'): CategoryFacetResult | null { + if (!facets?.items) return null; + + return facets + .items + .find((item): item is CategoryFacetResult => + '$type' in item && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.CategoryFacetResult, Relewise.Client' && + 'categorySelectionStrategy' in item + && item.categorySelectionStrategy === selectionStrategy) || null; + } + + public static categoryHierarchy(facets: ProductFacetResult, selectionStrategy: 'ImmediateParent' | 'Ancestors' | 'Descendants'): CategoryHierarchyFacetResult | null { + if (!facets?.items) return null; + + return facets + .items + .find((item): item is CategoryHierarchyFacetResult => + '$type' in item && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.CategoryHierarchyFacet, Relewise.Client' && + 'categorySelectionStrategy' in item && + item.categorySelectionStrategy === selectionStrategy) || null; + } + + public static listPriceRange( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + ): PriceRangeFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangeFacetResult => + item.field === 'ListPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangeFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy, + ) || null; + } + + public static salesPriceRange( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + ): PriceRangeFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangeFacetResult => + item.field === 'SalesPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangeFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy, + ) || null; + } + + public static listPriceRanges( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + ): PriceRangesFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangesFacetResult => + item.field === 'ListPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangesFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy, + ) || null; + } + + public static listPriceRangesWithRange( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + expandedRangeSize: number | null, + ): PriceRangesFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangesFacetResult => + item.field === 'ListPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangesFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy && + 'expandedRangeSize' in item && + item.expandedRangeSize === expandedRangeSize, + ) || null; + } + + public static salesPriceRanges( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + ): PriceRangesFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangesFacetResult => + item.field === 'SalesPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangesFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy, + ) || null; + } + + public static salesPriceRangesWithRange( + facets: ProductFacetResult, + selectionStrategy: PriceSelectionStrategy, + expandedRangeSize: number | null, + ): PriceRangesFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is PriceRangesFacetResult => + item.field === 'SalesPrice' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.PriceRangesFacetResult, Relewise.Client' && + 'priceSelectionStrategy' in item && + item.priceSelectionStrategy === selectionStrategy && + 'expandedRangeSize' in item && + item.expandedRangeSize === expandedRangeSize, + ) || null; + } + + public static dataDoubleRange( + facets: ProductFacetResult, + selectionStrategy: DataSelectionStrategy, + key: string, + ): ProductDataDoubleRangeFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is ProductDataDoubleRangeFacetResult => + item.field === 'Data' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.ProductDataDoubleRangeFacetResult, Relewise.Client' && + 'dataSelectionStrategy' in item && + item.dataSelectionStrategy === selectionStrategy && + 'key' in item && + item.key === key, + ) || null; + } + + public static dataDoubleRanges( + facets: ProductFacetResult, + selectionStrategy: DataSelectionStrategy, + key: string, + ): ProductDataDoubleRangesFacetResult | null { + if (!facets?.items) return null; + + return facets.items.find((item): item is ProductDataDoubleRangesFacetResult => + item.field === 'Data' && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.ProductDataDoubleRangesFacetResult, Relewise.Client' && + 'dataSelectionStrategy' in item && + item.dataSelectionStrategy === selectionStrategy && + 'key' in item && + item.key === key, + ) || null; + } + + public static variantSpecification(facets: ProductFacetResult, key: string): VariantSpecificationFacetResult | null { + if (!facets?.items) return null; + + return facets + .items + .find((item): item is VariantSpecificationFacetResult => + '$type' in item && + item.$type === 'Relewise.Client.DataTypes.Search.items.Result.VariantSpecificationFacetResult, Relewise.Client' && + 'field' in item && + item.field === 'VariantSpecification' && + 'key' in item && item.key === key) || null; + } + + public static dataString( + facets: ProductFacetResult, + key: string, + selectionStrategy: DataSelectionStrategy, + ): ProductDataStringValueFacetResult | null { + return this.data(facets, 'Relewise.Client.DataTypes.Search.Facets.Result.ProductDataStringValueFacetResult, Relewise.Client', selectionStrategy, key); + } + + public static dataBoolean( + facets: ProductFacetResult, + key: string, + selectionStrategy: DataSelectionStrategy, + ): ProductDataBooleanValueFacetResult | null { + return this.data(facets, 'Relewise.Client.DataTypes.Search.Facets.Result.ProductDataBooleanValueFacetResult, Relewise.Client', selectionStrategy, key); + } + + public static dataNumber( + facets: ProductFacetResult, + key: string, + selectionStrategy: DataSelectionStrategy, + ): ProductDataDoubleValueFacetResult | null { + return this.data(facets, 'Relewise.Client.DataTypes.Search.Facets.Result.ProductDataDoubleValueFacetResult, Relewise.Client', selectionStrategy, key); + } + + public static dataObject( + facets: ProductFacetResult, + selectionStrategy: DataSelectionStrategy, + key: string, + ): ProductDataObjectFacetResult | null { + if (!facets?.items) return null; + + return (facets.items.find((a): a is ProductDataObjectFacetResult => + a.$type === 'Relewise.Client.DataTypes.Search.Facets.Result.ProductDataObjectFacetResult, Relewise.Client' && + a.field === 'Data' && + (a as ProductDataObjectFacetResult).key === key && + (a as ProductDataObjectFacetResult).dataSelectionStrategy === selectionStrategy, + ) || null) as ProductDataObjectFacetResult; + } + + private static data( + facets: ProductFacetResult, + $type: string, + selectionStrategy: DataSelectionStrategy, + key: string, + ): TFacetResult | null { + if (!facets?.items) return null; + + return (facets.items.find((a: any) => + a.$type === $type && + a.field === 'Data' && + 'dataSelectionStrategy' in a && a.dataSelectionStrategy === selectionStrategy && + 'key' in a && a.key === key, + ) || null) as TFacetResult | null; + } +}; \ No newline at end of file diff --git a/packages/client/src/builders/search/index.ts b/packages/client/src/builders/search/index.ts index c9b3267..3e082bc 100644 --- a/packages/client/src/builders/search/index.ts +++ b/packages/client/src/builders/search/index.ts @@ -10,4 +10,5 @@ export * from './productCategorySearchBuilder'; export * from './productSearchBuilder'; export * from './searchCollectionBuilder'; export * from './searchTermPredictionBuilder'; -export * from './dataObjectValueSelectorBuilder' \ No newline at end of file +export * from './dataObjectValueSelectorBuilder'; +export * from './getProductFacet'; \ No newline at end of file diff --git a/packages/client/src/factory/dataValue.factory.ts b/packages/client/src/factory/dataValue.factory.ts index a3dee54..b69538c 100644 --- a/packages/client/src/factory/dataValue.factory.ts +++ b/packages/client/src/factory/dataValue.factory.ts @@ -1,4 +1,5 @@ -import { StringDataValue, StringCollectionDataValue, NumberDataValue, DoubleCollectionDataValue, BooleanDataValue, BooleanCollectionDataValue, MultiCurrencyDataValue, MultilingualDataValue, ObjectDataValue, ObjectCollectionDataValue, DataValue, MultilingualCollectionDataValue } from '..'; +import { DataValue } from '../models/data-contracts'; +import { StringDataValue, StringCollectionDataValue, NumberDataValue, DoubleCollectionDataValue, BooleanDataValue, BooleanCollectionDataValue, MultiCurrencyDataValue, MultilingualDataValue, MultilingualCollectionDataValue, ObjectDataValue, ObjectCollectionDataValue } from '../models/dataValue'; export class DataValueFactory { static string(value: string): StringDataValue { diff --git a/packages/client/tests/integration-tests/productSearch.integration.test.ts b/packages/client/tests/integration-tests/productSearch.integration.test.ts index fcd2d33..ecf4efc 100644 --- a/packages/client/tests/integration-tests/productSearch.integration.test.ts +++ b/packages/client/tests/integration-tests/productSearch.integration.test.ts @@ -1,4 +1,4 @@ -import { Searcher, ProductSearchBuilder, ProductSearchRequest, UserFactory, ValueSelectorFactory, DataValueFactory } from '../../src'; +import { Searcher, ProductSearchBuilder, ProductSearchRequest, UserFactory, ValueSelectorFactory, DataValueFactory, GetProductFacet, ProductAssortmentFacet, ProductDataStringValueFacetResult, CategoryFacetResult } from '../../src'; import { test, expect } from '@jest/globals' const { npm_config_API_KEY: API_KEY, npm_config_DATASET_ID: DATASET_ID, npm_config_SERVER_URL: SERVER_URL } = process.env; @@ -58,5 +58,28 @@ test('Retail Media search', async() => { const result = await searcher.searchProducts(request); + expect(result?.hits).toBeGreaterThan(0); +}); + +test('Facet result', async() => { + const request: ProductSearchRequest = baseProductBuilder() + .facets(f => f.addProductAssortmentFacet('Product')) + .facets(f => f.addProductDataStringValueFacet('AnyString', 'Product')) + .facets(f => f.addCategoryFacet('ImmediateParent')) + .build(); + + const result = await searcher.searchProducts(request); + + if (result && result.facets) { + const facet: ProductAssortmentFacet | null = GetProductFacet.productAssortment(result.facets, 'Product'); + expect(facet).toBeDefined(); + + const facet2: ProductDataStringValueFacetResult | null = GetProductFacet.dataString(result.facets, 'AnyString', 'Product'); + expect(facet2).toBeDefined(); + + const facet3: CategoryFacetResult | null = GetProductFacet.category(result.facets, 'ImmediateParent'); + expect(facet3).toBeDefined(); + } + expect(result?.hits).toBeGreaterThan(0); }); \ No newline at end of file