From 7c9cf2af504761cdad9e91947fb520de1b616359 Mon Sep 17 00:00:00 2001 From: Tarek Alqaddy Date: Fri, 9 Oct 2026 17:44:02 +0300 Subject: [PATCH] Align types with JS SDK --- spec/src/modules/browse.js | 40 +++++++++++++++++ .../catalog-facet-configurations-v2.js | 2 +- spec/src/modules/search.js | 24 ++++++++++ src/modules/browse.js | 8 ++++ src/modules/search.js | 7 +++ src/types/browse.d.ts | 21 +++++---- src/types/index.d.ts | 44 +++++++++++++++++-- src/types/search.d.ts | 3 ++ src/types/tests/browse.test-d.ts | 39 +++++++++++++++- src/types/tests/types.test-d.ts | 36 ++++++++++++++- 10 files changed, 206 insertions(+), 18 deletions(-) diff --git a/spec/src/modules/browse.js b/spec/src/modules/browse.js index 6c282457..d23d7538 100644 --- a/spec/src/modules/browse.js +++ b/spec/src/modules/browse.js @@ -207,6 +207,26 @@ describe('ConstructorIO - Browse', () => { }); }); + it('Should return a response with a valid filterName, filterValue, additional filters and filterMatchTypes', (done) => { + const filters = { keywords: ['battery-powered'] }; + const filterMatchTypes = { keywords: 'any' }; + const { browse } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue, { filters, filterMatchTypes }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('filter_match_types'); + expect(requestedUrlParams.filter_match_types).to.have.property('keywords').to.equal(filterMatchTypes.keywords); + done(); + }); + }); + it('Should return a response with a valid filterName, filterValue and additional fmtOptions', (done) => { const fmtOptions = { groups_max_depth: 2, groups_start: 'current' }; const { browse } = new ConstructorIO({ @@ -1053,6 +1073,26 @@ describe('ConstructorIO - Browse', () => { }); }); + it('Should return a response with valid ids, additional filters and filterMatchTypes', (done) => { + const filters = { keywords: ['battery-powered'] }; + const filterMatchTypes = { keywords: 'any' }; + const { browse } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + browse.getBrowseResultsForItemIds(ids, { filters, filterMatchTypes }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('filter_match_types'); + expect(requestedUrlParams.filter_match_types).to.have.property('keywords').to.equal(filterMatchTypes.keywords); + done(); + }); + }); + it('Should return a response with valid ids and additional fmtOptions', (done) => { const fmtOptions = { groups_max_depth: 2, groups_start: 'current' }; const { browse } = new ConstructorIO({ diff --git a/spec/src/modules/catalog/catalog-facet-configurations-v2.js b/spec/src/modules/catalog/catalog-facet-configurations-v2.js index dc7bde95..a0842d4e 100644 --- a/spec/src/modules/catalog/catalog-facet-configurations-v2.js +++ b/spec/src/modules/catalog/catalog-facet-configurations-v2.js @@ -66,7 +66,7 @@ describe('ConstructorIO - Catalog', () => { await catalog.removeFacetConfigurationV2(facetConfig); } catch (e) { // Log warning for debugging but don't fail cleanup - // eslint-disable-line no-console + // eslint-disable-next-line no-console console.warn(`Cleanup warning: failed to remove facet ${facetConfig.name}:`, e.message); } // eslint-disable-next-line no-await-in-loop, no-promise-executor-return diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index 99bdd941..f722eb5f 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -214,6 +214,30 @@ describe('ConstructorIO - Search', () => { }); }); + it('Should return a response with a valid query, section, filters and filterMatchTypes', (done) => { + const filters = { keywords: ['battery-powered'] }; + const filterMatchTypes = { keywords: 'any' }; + const { search } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { + section, + filters, + filterMatchTypes, + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('request').to.be.an('object'); + expect(res).to.have.property('response').to.be.an('object'); + expect(res).to.have.property('result_id').to.be.an('string'); + expect(requestedUrlParams).to.have.property('filter_match_types'); + expect(requestedUrlParams.filter_match_types).to.have.property('keywords').to.equal(filterMatchTypes.keywords); + done(); + }); + }); + it('Should return a response with a valid query, section, and fmtOptions', (done) => { const fmtOptions = { groups_max_depth: 2, groups_start: 'current' }; const { search } = new ConstructorIO({ diff --git a/src/modules/browse.js b/src/modules/browse.js index c1ee4991..b0066313 100644 --- a/src/modules/browse.js +++ b/src/modules/browse.js @@ -39,6 +39,7 @@ function createQueryParams(parameters, userParameters, options) { variationsMap, preFilterExpression, qsParam, + filterMatchTypes, } = parameters; // Pull page from parameters @@ -60,6 +61,11 @@ function createQueryParams(parameters, userParameters, options) { queryParams.filters = filters; } + // Pull filter match types from parameters + if (filterMatchTypes) { + queryParams.filter_match_types = filterMatchTypes; + } + // Pull sort by from parameters if (sortBy) { queryParams.sort_by = sortBy; @@ -263,6 +269,7 @@ class Browse { * @param {string[]} [parameters.hiddenFields] - Hidden metadata fields to return * @param {string[]} [parameters.hiddenFacets] - Hidden facet fields to return * @param {object} [parameters.variationsMap] - The variations map object to aggregate variations. Please refer to https://docs.constructor.com/reference/shared-variations-mapping for details + * @param {object} [parameters.filterMatchTypes] - An object specifying whether results must match `all`, `any` or `none` of a given filter * @param {object} [parameters.preFilterExpression] - Faceting expression to scope search results. Please refer to https://docs.constructor.com/reference/configuration-collections for details * @param {object} [parameters.qsParam] - Parameters listed above can be serialized into a JSON object and parsed through this parameter. Please refer to https://docs.constructor.com/reference/browse-browse-results for details * @param {object} [userParameters] - Parameters relevant to the user request @@ -351,6 +358,7 @@ class Browse { * @param {string[]} [parameters.hiddenFields] - Hidden metadata fields to return * @param {string[]} [parameters.hiddenFacets] - Hidden facet fields to return * @param {object} [parameters.variationsMap] - The variations map object to aggregate variations. Please refer to https://docs.constructor.com/reference/shared-variations-mapping for details + * @param {object} [parameters.filterMatchTypes] - An object specifying whether results must match `all`, `any` or `none` of a given filter * @param {object} [userParameters] - Parameters relevant to the user request * @param {number} [userParameters.sessionId] - Session ID, utilized to personalize results * @param {string} [userParameters.clientId] - Client ID, utilized to personalize results diff --git a/src/modules/search.js b/src/modules/search.js index 1350d0d4..64d60284 100644 --- a/src/modules/search.js +++ b/src/modules/search.js @@ -68,6 +68,7 @@ function createSearchUrl(query, parameters, userParameters, options, isVoiceSear variationsMap, preFilterExpression, qsParam, + filterMatchTypes, } = parameters; // Pull page from parameters @@ -90,6 +91,11 @@ function createSearchUrl(query, parameters, userParameters, options, isVoiceSear queryParams.filters = filters; } + // Pull filter match types from parameters + if (filterMatchTypes) { + queryParams.filter_match_types = filterMatchTypes; + } + // Pull sort by from parameters if (sortBy) { queryParams.sort_by = sortBy; @@ -184,6 +190,7 @@ class Search { * @param {string[]} [parameters.hiddenFields] - Hidden metadata fields to return * @param {string[]} [parameters.hiddenFacets] - Hidden facet fields to return * @param {object} [parameters.variationsMap] - The variations map object to aggregate variations. Please refer to https://docs.constructor.com/reference/shared-variations-mapping for details + * @param {object} [parameters.filterMatchTypes] - An object specifying whether results must match `all`, `any` or `none` of a given filter * @param {object} [parameters.preFilterExpression] - Faceting expression to scope search results. Please refer to https://docs.constructor.com/reference/configuration-collections for details * @param {object} [parameters.qsParam] - Parameters listed above can be serialized into a JSON object and parsed through this parameter. Please refer to https://docs.constructor.com/reference/search-search-resultsqueries for details * @param {object} [userParameters] - Parameters relevant to the user request diff --git a/src/types/browse.d.ts b/src/types/browse.d.ts index 1408f2f8..83b8fb8e 100644 --- a/src/types/browse.d.ts +++ b/src/types/browse.d.ts @@ -31,6 +31,7 @@ export interface BrowseParameters { hiddenFacets?: string[]; variationsMap?: VariationsMap; qsParam?: Record; + filterMatchTypes?: Record; } declare class Browse { @@ -78,9 +79,9 @@ declare class Browse { /* Browse results returned from server */ interface BrowseResponse extends Record { - request?: Partial; - response?: Partial; - result_id?: string; + request: Partial; + response: Partial; + result_id: string; ad_based?: boolean; } @@ -110,7 +111,9 @@ export interface GetBrowseResultsResponseData extends Record { refined_content: Record[]; total_num_results: number; features: Partial[]; - collection: Partial; + collection?: Partial; + related_searches?: Record[]; + related_browse_pages?: Record[]; } export interface BrowseResultData extends Record { @@ -122,15 +125,15 @@ export interface BrowseResultData extends Record { value: string; is_slotted: false; labels: Record; - variations: Record[]; - variations_map: Record | Record[]; + variations?: Record[]; + variations_map?: Record | Record[]; } export interface BrowseRequestType extends Record { browse_filter_name: string; browse_filter_value: string; - filter_match_types: Record; - filters: Record; + filter_match_types?: Record; + filters?: Record; fmt_options: Record; num_results_per_page: number; page: number; @@ -138,7 +141,7 @@ export interface BrowseRequestType extends Record { sort_by: string; sort_order: string; term: string; - query: string; + query?: string; features: Partial; feature_variants: Partial; searchandized_items: Record; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index c760ac93..428099bc 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -8,6 +8,10 @@ export * from './tasks'; export * from './tracker'; export * from './searchandising'; +type RequireAtLeastOne = + Pick> & + { [K in Keys]-?: Required> & Partial>> }[Keys]; + export interface NetworkParameters extends Record { timeout?: number; } @@ -148,16 +152,48 @@ export interface Variation extends Record { data?: ItemData; } +export interface VariationsMapSingleFilter { + field: string; + value: string | number | boolean; +} + +export interface VariationsMapRange { + field: string; + range: FilterExpressionRangeValue; +} + +export type FilterNode = VariationsMapSingleFilter | VariationsMapRange; + +export type FilterBy = RequireAtLeastOne<{ + and?: Array; + or?: Array; + not?: FilterNode | FilterBy; +}>; + +export type Aggregation = 'first' | 'min' | 'max' | 'all' | 'count' | 'field_count' | 'value_count'; + +export interface VariationsMapBaseValue { + aggregation: Aggregation; + field: string; +} + +export interface VariationsMapValueCount extends VariationsMapBaseValue { + aggregation: 'value_count'; + value: boolean | number | string; +} + +export interface VariationsMapStandardValue extends VariationsMapBaseValue { + aggregation: Exclude; +} + export interface VariationsMap { group_by: Array<{ name: string, field: string }>; + filter_by?: FilterBy; values: { - [key: string]: { - aggregation: 'first' | 'min' | 'max' | 'all', - field: string - }, + [key: string]: VariationsMapValueCount | VariationsMapStandardValue, }, dtype: 'array' | 'object' } diff --git a/src/types/search.d.ts b/src/types/search.d.ts index 7602aed7..d73b6712 100644 --- a/src/types/search.d.ts +++ b/src/types/search.d.ts @@ -30,6 +30,7 @@ export interface SearchParameters { hiddenFacets?: string[]; variationsMap?: VariationsMap; qsParam?: Record; + filterMatchTypes?: Record; } declare class Search { @@ -68,6 +69,8 @@ export interface Response extends Record { refined_content: Record[]; total_num_results: number; features: Partial[]; + related_searches?: Record[]; + related_browse_pages?: Record[]; } export interface SearchRequestType extends Record { diff --git a/src/types/tests/browse.test-d.ts b/src/types/tests/browse.test-d.ts index 4cb05b3b..64baddeb 100644 --- a/src/types/tests/browse.test-d.ts +++ b/src/types/tests/browse.test-d.ts @@ -1,5 +1,9 @@ -import { expectAssignable } from 'tsd'; -import { GetBrowseResultsResponse } from '../browse'; +import { expectAssignable, expectNotAssignable } from 'tsd'; +import { + BrowseParameters, + BrowseResultData, + GetBrowseResultsResponse, +} from '../browse'; expectAssignable({ response: { @@ -118,3 +122,34 @@ expectAssignable({ }, ad_based: true, }); + +expectAssignable({ + filters: { size: 'medium' }, + filterMatchTypes: { size: 'all' }, +}); + +expectNotAssignable({ + filterMatchTypes: { size: 'some' }, +}); + +expectNotAssignable({ + request: {}, + response: { results: [] }, +}); + +expectAssignable({ + request: {}, + response: { + related_searches: [{ term: 'dog toys' }], + related_browse_pages: [{ filter_name: 'group_id', filter_value: 'toys' }], + }, + result_id: 'e5941e13-f4ca-4efb-9326-893fd49b4e71', +}); + +expectAssignable({ + matched_terms: [], + data: { id: '123' }, + value: 'Name', + is_slotted: false, + labels: {}, +}); diff --git a/src/types/tests/types.test-d.ts b/src/types/tests/types.test-d.ts index ca3b7aa6..5e18a154 100644 --- a/src/types/tests/types.test-d.ts +++ b/src/types/tests/types.test-d.ts @@ -1,5 +1,5 @@ -import { expectAssignable } from 'tsd'; -import { FilterExpression } from '../index'; +import { expectAssignable, expectNotAssignable } from 'tsd'; +import { FilterExpression, VariationsMap } from '../index'; expectAssignable({ or: [ @@ -31,3 +31,35 @@ expectAssignable({ }, ], }); + +expectAssignable({ + group_by: [{ name: 'variation', field: 'data.variation_id' }], + filter_by: { + and: [ + { field: 'data.brand', value: 'Best' }, + { not: { field: 'data.price', range: [100, 'inf'] } }, + ], + }, + values: { + min_price: { aggregation: 'min', field: 'data.price' }, + total: { aggregation: 'count', field: 'data.variation_id' }, + sizes: { aggregation: 'field_count', field: 'data.size' }, + in_stock: { aggregation: 'value_count', field: 'data.in_stock', value: true }, + }, + dtype: 'object', +}); + +expectNotAssignable({ + group_by: [{ name: 'variation', field: 'data.variation_id' }], + values: { + in_stock: { aggregation: 'value_count', field: 'data.in_stock' }, + }, + dtype: 'object', +}); + +expectNotAssignable({ + group_by: [{ name: 'variation', field: 'data.variation_id' }], + filter_by: {}, + values: {}, + dtype: 'array', +});