From 8ac722d8d47e9ce33cc86811b12e53395c64496e Mon Sep 17 00:00:00 2001 From: Andrei Andrukhovich Date: Tue, 30 Dec 2025 08:09:05 -0700 Subject: [PATCH 1/2] add v2 facets and searchabilities --- .../catalog-facet-configurations-v2.js | 587 +++++++++++ .../catalog/catalog-searchabilities-v2.js | 541 ++++++++++ src/modules/catalog.js | 991 ++++++++++++++++++ src/types/catalog.d.ts | 168 +++ src/types/index.d.ts | 63 ++ 5 files changed, 2350 insertions(+) create mode 100644 spec/src/modules/catalog/catalog-facet-configurations-v2.js create mode 100644 spec/src/modules/catalog/catalog-searchabilities-v2.js diff --git a/spec/src/modules/catalog/catalog-facet-configurations-v2.js b/spec/src/modules/catalog/catalog-facet-configurations-v2.js new file mode 100644 index 00000000..a5f30add --- /dev/null +++ b/spec/src/modules/catalog/catalog-facet-configurations-v2.js @@ -0,0 +1,587 @@ +/* eslint-disable no-unused-expressions, import/no-unresolved, no-restricted-syntax, max-nested-callbacks */ +const dotenv = require('dotenv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); +const { v4: uuidv4 } = require('uuid'); +const ConstructorIO = require('../../../../test/constructorio'); // eslint-disable-line import/extensions +const helpers = require('../../../mocha.helpers'); + +const nodeFetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); + +chai.use(chaiAsPromised); +chai.use(sinonChai); +dotenv.config(); + +const sendTimeout = 300; +const testApiKey = process.env.TEST_CATALOG_API_KEY; +const testApiToken = process.env.TEST_API_TOKEN; +const validOptions = { + apiKey: testApiKey, + apiToken: testApiToken, +}; +const skipNetworkTimeoutTests = process.env.SKIP_NETWORK_TIMEOUT_TESTS === 'true'; + +function createMockFacetConfigurationV2() { + const uuid = uuidv4(); + + return { + name: `facet-v2-${uuid}`, + pathInMetadata: `metadata.facet_v2_${uuid}`, + displayName: `Facet V2 ${uuid}`, + type: 'multiple', + }; +} + +describe('ConstructorIO - Catalog', () => { + const clientVersion = 'cio-mocha'; + let fetchSpy; + + beforeEach(() => { + global.CLIENT_VERSION = clientVersion; + fetchSpy = sinon.spy(nodeFetch); + }); + + afterEach((done) => { + delete global.CLIENT_VERSION; + + fetchSpy = null; + + // Add throttling between requests to avoid rate limiting + setTimeout(() => done(), sendTimeout); + }); + + describe('Facet Configurations V2', () => { + const facetConfigurations = []; + + after(async function afterHook() { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + // Clean up all the facet configurations that were created + // Increasing timeout, since cleanup is consistently taking longer than default 5 seconds + this.timeout(30000); + for await (const facetConfig of facetConfigurations) { + try { + await catalog.removeFacetConfigurationV2(facetConfig); + } catch (e) { + // Log warning for debugging but don't fail cleanup + console.warn(`Cleanup warning: failed to remove facet ${facetConfig.name}:`, e.message); + } + } + }); + + describe('addFacetConfigurationV2', () => { + const mockFacetConfiguration = createMockFacetConfigurationV2(); + + it('Should resolve when adding a facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.addFacetConfigurationV2(mockFacetConfiguration).then((response) => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(mockFacetConfiguration); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets'); + expect(response).to.have.property('name').to.equal(mockFacetConfiguration.name); + expect(response).to.have.property('path_in_metadata'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Backwards Compatibility `display_name` - Should resolve when adding a facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const mockConfig = createMockFacetConfigurationV2(); + // eslint-disable-next-line camelcase + const { displayName: display_name, pathInMetadata: path_in_metadata, ...rest } = mockConfig; + const newFacetConfiguration = { display_name, path_in_metadata, ...rest }; // eslint-disable-line camelcase + catalog.addFacetConfigurationV2(newFacetConfiguration).then((response) => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(newFacetConfiguration); + + expect(response).to.have.property('display_name').to.be.equal(newFacetConfiguration.display_name); + done(); + }); + }); + + it('Should return error when adding a facet configuration that already exists', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + // Grab a mock configuration that already exists and try to add it + const facetConfiguration = facetConfigurations[0]; + + return expect(catalog.addFacetConfigurationV2(facetConfiguration)).to.eventually.be.rejected; + }); + + it('Should return error when adding a facet configuration without required pathInMetadata', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const invalidConfig = { + name: `facet-v2-${uuidv4()}`, + type: 'multiple', + // Missing pathInMetadata + }; + + return expect(catalog.addFacetConfigurationV2(invalidConfig)).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.addFacetConfigurationV2(mockFacetConfiguration, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(catalog.addFacetConfigurationV2(mockFacetConfiguration)).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('getFacetConfigurationsV2', () => { + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + const mockFacetConfiguration = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(mockFacetConfiguration).then(() => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(mockFacetConfiguration); + done(); + }); + }); + + it('Should return a response when getting facet configurations', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.getFacetConfigurationsV2().then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets'); + expect(res).to.have.property('facets').to.be.an('array').length.gte(1); + expect(res).to.have.property('total_count'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return a response when getting facet configurations with pagination parameters', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.getFacetConfigurationsV2({ numResultsPerPage: 1, page: 1 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('facets').to.be.an('array').length(1); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('1'); + expect(requestedUrlParams).to.have.property('page').to.equal('1'); + done(); + }); + }); + + it('Should return a response when getting facet configurations with offset parameter', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.getFacetConfigurationsV2({ offset: 0, numResultsPerPage: 10 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('facets').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('offset').to.equal('0'); + done(); + }); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.getFacetConfigurationsV2({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(catalog.getFacetConfigurationsV2()).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('getFacetConfigurationV2', () => { + let existingFacetConfiguration; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + existingFacetConfiguration = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(existingFacetConfiguration).then(() => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(existingFacetConfiguration); + done(); + }); + }); + + it('Should return a response when getting a single facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.getFacetConfigurationV2({ name: existingFacetConfiguration.name }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets/'); + expect(res).to.have.property('name').to.equal(existingFacetConfiguration.name); + expect(res).to.have.property('path_in_metadata'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return error when getting a facet configuration that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.getFacetConfigurationV2({ name: 'non-existent-facet' })).to.eventually.be.rejected; + }); + + it('Should return error when name parameter is missing', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.getFacetConfigurationV2({})).to.eventually.be.rejectedWith('name is a required parameter of type string'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.getFacetConfigurationV2({ name: existingFacetConfiguration.name }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('modifyFacetConfigurationsV2', () => { + let existingFacetConfiguration; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + existingFacetConfiguration = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(existingFacetConfiguration).then(() => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(existingFacetConfiguration); + done(); + }); + }); + + it('Should return a response when modifying multiple facet configurations', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const updateData = { + facetConfigurations: [ + { + name: existingFacetConfiguration.name, + displayName: 'Updated Display Name V2', + }, + ], + }; + + catalog.modifyFacetConfigurationsV2(updateData).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets'); + expect(res).to.have.property('facets').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return error when facetConfigurations parameter is missing', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.modifyFacetConfigurationsV2({})).to.eventually.be.rejectedWith('facetConfigurations is a required parameter of type array'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.modifyFacetConfigurationsV2({ facetConfigurations: [{ name: 'test' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('modifyFacetConfigurationV2', () => { + let existingFacetConfiguration; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + existingFacetConfiguration = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(existingFacetConfiguration).then(() => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(existingFacetConfiguration); + done(); + }); + }); + + it('Should return a response when modifying a single facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const updateData = { + name: existingFacetConfiguration.name, + displayName: 'Updated Single Display Name V2', + sortOrder: 'value', + }; + + catalog.modifyFacetConfigurationV2(updateData).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets/'); + expect(res).to.have.property('name').to.equal(existingFacetConfiguration.name); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return error when modifying a facet configuration that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.modifyFacetConfigurationV2({ name: 'non-existent-facet', displayName: 'Test' })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.modifyFacetConfigurationV2({ name: existingFacetConfiguration.name, displayName: 'Test' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('replaceFacetConfigurationV2', () => { + let existingFacetConfiguration; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + existingFacetConfiguration = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(existingFacetConfiguration).then(() => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(existingFacetConfiguration); + done(); + }); + }); + + it('Should return a response when replacing a facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const replaceData = { + name: existingFacetConfiguration.name, + pathInMetadata: existingFacetConfiguration.pathInMetadata, + type: 'multiple', + displayName: 'Replaced Display Name V2', + }; + + catalog.replaceFacetConfigurationV2(replaceData).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets/'); + expect(res).to.have.property('name').to.equal(existingFacetConfiguration.name); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + const replaceData = { + name: existingFacetConfiguration.name, + pathInMetadata: existingFacetConfiguration.pathInMetadata, + type: 'multiple', + }; + + return expect(catalog.replaceFacetConfigurationV2(replaceData, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('createOrReplaceFacetConfigurationsV2', () => { + it('Should return a response when creating or replacing facet configurations', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const newConfig = createMockFacetConfigurationV2(); + const createOrReplaceData = { + facetConfigurations: [newConfig], + }; + + catalog.createOrReplaceFacetConfigurationsV2(createOrReplaceData).then((res) => { + // Push mock facet configuration into saved list to be cleaned up afterwards + facetConfigurations.push(newConfig); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets'); + expect(res).to.have.property('facets').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.createOrReplaceFacetConfigurationsV2({ facetConfigurations: [] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('removeFacetConfigurationV2', () => { + let facetToRemove; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + facetToRemove = createMockFacetConfigurationV2(); + + catalog.addFacetConfigurationV2(facetToRemove).then(() => { + done(); + }); + }); + + it('Should return a response when removing a facet configuration', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.removeFacetConfigurationV2({ name: facetToRemove.name }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/facets/'); + expect(res).to.have.property('name').to.equal(facetToRemove.name); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + done(); + }); + }); + + it('Should return error when removing a facet configuration that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.removeFacetConfigurationV2({ name: 'non-existent-facet' })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.removeFacetConfigurationV2({ name: 'test-facet' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + }); +}); diff --git a/spec/src/modules/catalog/catalog-searchabilities-v2.js b/spec/src/modules/catalog/catalog-searchabilities-v2.js new file mode 100644 index 00000000..bad9900d --- /dev/null +++ b/spec/src/modules/catalog/catalog-searchabilities-v2.js @@ -0,0 +1,541 @@ +/* eslint-disable no-unused-expressions, import/no-unresolved, no-restricted-syntax, max-nested-callbacks */ +const dotenv = require('dotenv'); +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); +const { v4: uuidv4 } = require('uuid'); +const ConstructorIO = require('../../../../test/constructorio'); // eslint-disable-line import/extensions +const helpers = require('../../../mocha.helpers'); + +const nodeFetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); + +chai.use(chaiAsPromised); +chai.use(sinonChai); +dotenv.config(); + +const sendTimeout = 300; +const testApiKey = process.env.TEST_CATALOG_API_KEY; +const testApiToken = process.env.TEST_API_TOKEN; +const validOptions = { + apiKey: testApiKey, + apiToken: testApiToken, +}; +const skipNetworkTimeoutTests = process.env.SKIP_NETWORK_TIMEOUT_TESTS === 'true'; + +function createMockSearchabilityConfigurationV2() { + const uuid = uuidv4().substring(0, 8); + + return { + name: `test_searchability_v2_${uuid}`, + fuzzySearchable: false, + exactSearchable: true, + displayable: true, + hidden: false, + }; +} + +describe('ConstructorIO - Catalog', () => { + const clientVersion = 'cio-mocha'; + let fetchSpy; + + beforeEach(() => { + global.CLIENT_VERSION = clientVersion; + fetchSpy = sinon.spy(nodeFetch); + }); + + afterEach((done) => { + delete global.CLIENT_VERSION; + + fetchSpy = null; + + // Add throttling between requests to avoid rate limiting + setTimeout(done, sendTimeout); + }); + + describe('Searchabilities V2', () => { + const searchabilitiesToCleanup = []; + + after(async function afterHook() { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + // Clean up all the searchabilities that were created + this.timeout(30000); + if (searchabilitiesToCleanup.length > 0) { + try { + await catalog.deleteSearchabilitiesV2({ + searchabilities: searchabilitiesToCleanup.map((s) => ({ name: s.name })), + }); + } catch (e) { + // Log warning for debugging but don't fail cleanup + const names = searchabilitiesToCleanup.map((s) => s.name).join(', '); + console.warn(`Cleanup warning: failed to remove searchabilities [${names}]:`, e.message); + } + } + }); + + describe('retrieveSearchabilitiesV2', () => { + it('Should return a response when retrieving searchabilities', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.retrieveSearchabilitiesV2().then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities'); + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(res).to.have.property('total_count'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return a response when retrieving searchabilities with pagination parameters', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.retrieveSearchabilitiesV2({ numResultsPerPage: 5, page: 1 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('5'); + expect(requestedUrlParams).to.have.property('page').to.equal('1'); + done(); + }); + }); + + it('Should return a response when retrieving searchabilities with filter parameters', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.retrieveSearchabilitiesV2({ displayable: true }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('displayable').to.equal('true'); + done(); + }); + }); + + it('Should return a response when retrieving searchabilities with sort parameters', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.retrieveSearchabilitiesV2({ sortBy: 'name', sortOrder: 'ascending' }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('sort_by').to.equal('name'); + expect(requestedUrlParams).to.have.property('sort_order').to.equal('ascending'); + done(); + }); + }); + + it('Should return a response when retrieving searchabilities with name filter', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.retrieveSearchabilitiesV2({ name: 'keywords' }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('name').to.equal('keywords'); + done(); + }); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.retrieveSearchabilitiesV2({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(catalog.retrieveSearchabilitiesV2()).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('patchSearchabilitiesV2', () => { + it('Should return a response when patching searchabilities', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const mockSearchability = createMockSearchabilityConfigurationV2(); + const searchabilityConfigurations = [mockSearchability]; + + catalog.patchSearchabilitiesV2({ searchabilities: searchabilityConfigurations }).then((res) => { + // Track for cleanup + searchabilitiesToCleanup.push(mockSearchability); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities'); + expect(res).to.have.property('searchabilities').to.be.an('array').length.gte(1); + expect(res).to.have.property('total_count'); + + const searchabilitiesResponse = res.searchabilities; + const createdSearchability = searchabilitiesResponse.find((s) => s.name === mockSearchability.name); + + expect(createdSearchability).to.have.property('name').to.equal(mockSearchability.name); + expect(createdSearchability).to.have.property('exact_searchable').to.equal(mockSearchability.exactSearchable); + expect(createdSearchability).to.have.property('created_at'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('section'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return a response when patching searchabilities with skipRebuild', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const mockSearchability = createMockSearchabilityConfigurationV2(); + const searchabilityConfigurations = [mockSearchability]; + + const params = { searchabilities: searchabilityConfigurations, skipRebuild: true }; + catalog.patchSearchabilitiesV2(params).then((res) => { + // Track for cleanup + searchabilitiesToCleanup.push(mockSearchability); + + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); + done(); + }); + }); + + it('Should return error when patching searchabilities with unsupported values', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + const badSearchabilityConfigurations = [ + { + name: 'keywords', + nonExistentField: false, + }, + ]; + + const params = { searchabilities: badSearchabilityConfigurations }; + return expect(catalog.patchSearchabilitiesV2(params)).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + const params = { searchabilities: [{ name: 'test' }] }; + return expect(catalog.patchSearchabilitiesV2(params, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + const params = { searchabilities: [{ name: 'test' }] }; + return expect(catalog.patchSearchabilitiesV2(params)).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('getSearchabilityV2', () => { + it('Should return a response when getting a single searchability', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + // Use a known searchability that should exist + catalog.getSearchabilityV2({ name: 'keywords' }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities/'); + expect(res).to.have.property('name').to.equal('keywords'); + expect(res).to.have.property('fuzzy_searchable'); + expect(res).to.have.property('exact_searchable'); + expect(res).to.have.property('displayable'); + expect(res).to.have.property('hidden'); + expect(res).to.have.property('created_at'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return error when getting a searchability that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.getSearchabilityV2({ name: 'non_existent_searchability_xyz123' })).to.eventually.be.rejected; + }); + + it('Should return error when name parameter is missing', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.getSearchabilityV2({})).to.eventually.be.rejectedWith('name is a required parameter of type string'); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.getSearchabilityV2({ name: 'keywords' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('patchSearchabilityV2', () => { + let existingSearchability; + + before((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + existingSearchability = createMockSearchabilityConfigurationV2(); + + catalog.patchSearchabilitiesV2({ searchabilities: [existingSearchability] }).then(() => { + searchabilitiesToCleanup.push(existingSearchability); + done(); + }); + }); + + it('Should return a response when patching a single searchability', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.patchSearchabilityV2({ + name: existingSearchability.name, + displayable: false, + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities/'); + expect(res).to.have.property('name').to.equal(existingSearchability.name); + expect(res).to.have.property('displayable').to.equal(false); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); + done(); + }); + }); + + it('Should return a response when patching a single searchability with skipRebuild', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.patchSearchabilityV2({ + name: existingSearchability.name, + hidden: true, + skipRebuild: true, + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('name').to.equal(existingSearchability.name); + expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); + done(); + }); + }); + + it('Should return error when patching a searchability that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.patchSearchabilityV2({ name: 'non_existent_searchability_xyz123', displayable: false })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.patchSearchabilityV2({ name: existingSearchability.name, displayable: false }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('deleteSearchabilitiesV2', () => { + let searchabilityToDelete; + + beforeEach((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchabilityToDelete = createMockSearchabilityConfigurationV2(); + + catalog.patchSearchabilitiesV2({ searchabilities: [searchabilityToDelete] }).then(() => { + done(); + }); + }); + + it('Should return a response when deleting searchabilities', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.deleteSearchabilitiesV2({ + searchabilities: [{ name: searchabilityToDelete.name }], + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities'); + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(res).to.have.property('total_count'); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + done(); + }); + }); + + it('Should return a response when deleting searchabilities with skipRebuild', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.deleteSearchabilitiesV2({ + searchabilities: [{ name: searchabilityToDelete.name }], + skipRebuild: true, + }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('searchabilities').to.be.an('array'); + expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); + done(); + }); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.deleteSearchabilitiesV2({ searchabilities: [{ name: 'test' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('deleteSearchabilityV2', () => { + let searchabilityToDelete; + + beforeEach((done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchabilityToDelete = createMockSearchabilityConfigurationV2(); + + catalog.patchSearchabilitiesV2({ searchabilities: [searchabilityToDelete] }).then(() => { + done(); + }); + }); + + it('Should return a response when deleting a single searchability', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.deleteSearchabilityV2({ name: searchabilityToDelete.name }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestUrl = fetchSpy.args[0][0]; + + expect(requestUrl).to.include('/v2/searchabilities/'); + expect(res).to.have.property('name').to.equal(searchabilityToDelete.name); + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + done(); + }); + }); + + it('Should return a response when deleting a single searchability with skipRebuild', (done) => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + catalog.deleteSearchabilityV2({ name: searchabilityToDelete.name, skipRebuild: true }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + expect(res).to.have.property('name').to.equal(searchabilityToDelete.name); + expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); + done(); + }); + }); + + it('Should return error when deleting a searchability that does not exist', () => { + const { catalog } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + return expect(catalog.deleteSearchabilityV2({ name: 'non_existent_searchability_xyz123' })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { catalog } = new ConstructorIO(validOptions); + + return expect(catalog.deleteSearchabilityV2({ name: searchabilityToDelete.name }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + }); +}); diff --git a/src/modules/catalog.js b/src/modules/catalog.js index c8b86d24..64506a0c 100644 --- a/src/modules/catalog.js +++ b/src/modules/catalog.js @@ -3530,6 +3530,997 @@ class Catalog { return helpers.throwHttpErrorFromResponse(new Error(), response); }); } + + // -------------------- V2 Facet Configuration Methods -------------------- + + /** + * Create a facet configuration (V2) + * + * @function addFacetConfigurationV2 + * @param {object} parameters - Additional parameters for facet configuration details + * @param {string} parameters.name - Unique facet name used to refer to the facet in your catalog + * @param {string} parameters.pathInMetadata - The path in metadata of each item where this facet is present + * @param {string} parameters.type - Type of facet. Must be one of multiple, hierarchical, or range + * @param {string} [parameters.displayName] - The name of the facet presented to the end users + * @param {string} [parameters.sortOrder] - Defines the criterion by which the options of this facet group are sorted + * @param {boolean} [parameters.sortDescending] - Set to true if the options should be sorted in descending order + * @param {string} [parameters.rangeType] - Should be 'static' if a facet is configured as range + * @param {string} [parameters.rangeFormat] - Format of range facets: 'boundaries' for sliders, 'options' for buckets + * @param {string} [parameters.rangeInclusive] - Used to create inclusive buckets: 'above', 'below', or null + * @param {number[]} [parameters.rangeLimits] - Defines the cut-off points for generating static range buckets + * @param {string} [parameters.matchType] - Specifies the behavior of filters: 'any', 'all', or 'none' + * @param {number} [parameters.position] - Slot facet groups to fixed positions + * @param {boolean} [parameters.hidden] - Specifies whether the facet is hidden from users + * @param {boolean} [parameters.protected] - Specifies whether the facet is protected from users + * @param {boolean} [parameters.countable] - Specifies whether counts for each facet option should be calculated + * @param {number} [parameters.optionsLimit] - Maximum number of options to return in search responses + * @param {object} [parameters.data] - Dictionary/Object with any extra facet data + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Facet configuration response with name, path_in_metadata, type, and other fields + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.addFacetConfigurationV2({ + * name: 'color', + * pathInMetadata: 'color', + * type: 'multiple', + * displayName: 'Color', + * }); + */ + addFacetConfigurationV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, ...rest } = parameters; + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl('facets', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + return fetch(requestUrl, { + method: 'POST', + body: JSON.stringify(toSnakeCaseKeys(rest)), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Get all facet configurations (V2) + * + * @function getFacetConfigurationsV2 + * @param {object} parameters - Additional parameters for retrieving facet configurations. + * @param {number} [parameters.page] - Page number you'd like to request. Defaults to 1. + * @param {number} [parameters.numResultsPerPage] - Number of facets per page in paginated response. Default value is 100. + * @param {number} [parameters.offset] - The number of results to skip from the beginning. Cannot be used together with page. + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{facets: object[], total_count: number}>} - Paginated list of facet configurations + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.getFacetConfigurationsV2({ + * page: 2, + * numResultsPerPage: 50, + * }); + */ + getFacetConfigurationsV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { num_results_per_page, numResultsPerPage = num_results_per_page, page, offset } = parameters; + const additionalQueryParams = { + section: parameters.section || 'Products', + }; + + if (numResultsPerPage) { + additionalQueryParams.num_results_per_page = numResultsPerPage; + } + + if (page) { + additionalQueryParams.page = page; + } + + if (!helpers.isNil(offset)) { + additionalQueryParams.offset = offset; + } + + try { + requestUrl = createCatalogUrl('facets', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Get a single facet's configuration (V2) + * + * @function getFacetConfigurationV2 + * @param {object} parameters - Additional parameters for retrieving a facet configuration. + * @param {string} parameters.name - Unique facet name used to refer to the facet in your catalog + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Facet configuration response with name, path_in_metadata, type, and other fields + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.getFacetConfigurationV2({ + * name: 'color', + * }); + */ + getFacetConfigurationV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, name } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl(`facets/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Create or replace facet configurations (V2) + * + * Caution: Replacing will overwrite all other configurations you may have defined for the facet group, + * resetting them to their defaults, except facet options - they will not be affected. + * + * @function createOrReplaceFacetConfigurationsV2 + * @param {object} parameters - Additional parameters for creating or replacing facet configurations + * @param {array} parameters.facetConfigurations - List of facet configurations to create or replace + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{facets: object[]}>} - List of created/replaced facet configurations + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.createOrReplaceFacetConfigurationsV2({ + * facetConfigurations: [ + * { + * name: 'color', + * pathInMetadata: 'color', + * type: 'multiple', + * displayName: 'Color', + * }, + * ], + * }); + */ + createOrReplaceFacetConfigurationsV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, facetConfigurations: facetConfigurationsRaw } = parameters; + + if (!facetConfigurationsRaw || !Array.isArray(facetConfigurationsRaw)) { + return Promise.reject(new Error('facetConfigurations is a required parameter of type array')); + } + + const facetConfigurations = facetConfigurationsRaw.map((config) => toSnakeCaseKeys(config)); + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl('facets', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PUT', + body: JSON.stringify({ facets: facetConfigurations }), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Modify the configurations of multiple facets (partially) at once (V2) + * + * @function modifyFacetConfigurationsV2 + * @param {object} parameters - Additional parameters for modifying facet configurations + * @param {array} parameters.facetConfigurations - List of facet configurations you would like to update + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{facets: object[]}>} - List of modified facet configurations + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.modifyFacetConfigurationsV2({ + * facetConfigurations: [ + * { + * name: 'color', + * displayName: 'Color', + * sortOrder: 'value', + * }, + * ], + * }); + */ + modifyFacetConfigurationsV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, facetConfigurations: facetConfigurationsRaw } = parameters; + + if (!facetConfigurationsRaw || !Array.isArray(facetConfigurationsRaw)) { + return Promise.reject(new Error('facetConfigurations is a required parameter of type array')); + } + + const facetConfigurations = facetConfigurationsRaw.map((config) => toSnakeCaseKeys(config)); + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl('facets', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PATCH', + body: JSON.stringify({ facets: facetConfigurations }), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Replace the configuration of a facet (completely) (V2) + * + * Caution: This will overwrite all other configurations you may have defined for the facet group, + * resetting them to their defaults, except facet options - they will not be affected. + * + * @function replaceFacetConfigurationV2 + * @param {object} parameters - Additional parameters for facet configuration details + * @param {string} parameters.name - Unique facet name used to refer to the facet in your catalog + * @param {string} parameters.pathInMetadata - The path in metadata of each item where this facet is present + * @param {string} parameters.type - Type of facet. Must be one of multiple, hierarchical, or range + * @param {string} [parameters.displayName] - The name of the facet presented to the end users + * @param {string} [parameters.sortOrder] - Defines the criterion by which the options are sorted + * @param {boolean} [parameters.sortDescending] - Set to true if the options should be sorted in descending order + * @param {string} [parameters.rangeType] - Should be 'static' if a facet is configured as range + * @param {string} [parameters.rangeFormat] - Format of range facets: 'boundaries' or 'options' + * @param {string} [parameters.rangeInclusive] - Used to create inclusive buckets: 'above', 'below', or null + * @param {number[]} [parameters.rangeLimits] - Defines the cut-off points for generating static range buckets + * @param {string} [parameters.matchType] - Specifies the behavior of filters: 'any', 'all', or 'none' + * @param {number} [parameters.position] - Slot facet groups to fixed positions + * @param {boolean} [parameters.hidden] - Specifies whether the facet is hidden from users + * @param {boolean} [parameters.protected] - Specifies whether the facet is protected from users + * @param {boolean} [parameters.countable] - Specifies whether counts for each facet option should be calculated + * @param {number} [parameters.optionsLimit] - Maximum number of options to return in search responses + * @param {object} [parameters.data] - Dictionary/Object with any extra facet data + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Replaced facet configuration response + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.replaceFacetConfigurationV2({ + * name: 'color', + * pathInMetadata: 'color', + * type: 'multiple', + * displayName: 'Color', + * }); + */ + replaceFacetConfigurationV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, name, ...rest } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl(`facets/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PUT', + body: JSON.stringify(toSnakeCaseKeys(rest)), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Modify the configuration of a facet (partially) (V2) + * + * @function modifyFacetConfigurationV2 + * @param {object} parameters - Additional parameters for facet configuration details + * @param {string} parameters.name - Unique facet name used to refer to the facet in your catalog + * @param {string} [parameters.pathInMetadata] - The path in metadata of each item where this facet is present + * @param {string} [parameters.type] - Type of facet. Must be one of multiple, hierarchical, or range + * @param {string} [parameters.displayName] - The name of the facet presented to the end users + * @param {string} [parameters.sortOrder] - Defines the criterion by which the options are sorted + * @param {boolean} [parameters.sortDescending] - Set to true if the options should be sorted in descending order + * @param {string} [parameters.rangeType] - Should be 'static' if a facet is configured as range + * @param {string} [parameters.rangeFormat] - Format of range facets: 'boundaries' or 'options' + * @param {string} [parameters.rangeInclusive] - Used to create inclusive buckets: 'above', 'below', or null + * @param {number[]} [parameters.rangeLimits] - Defines the cut-off points for generating static range buckets + * @param {string} [parameters.matchType] - Specifies the behavior of filters: 'any', 'all', or 'none' + * @param {number} [parameters.position] - Slot facet groups to fixed positions + * @param {boolean} [parameters.hidden] - Specifies whether the facet is hidden from users + * @param {boolean} [parameters.protected] - Specifies whether the facet is protected from users + * @param {boolean} [parameters.countable] - Specifies whether counts for each facet option should be calculated + * @param {number} [parameters.optionsLimit] - Maximum number of options to return in search responses + * @param {object} [parameters.data] - Dictionary/Object with any extra facet data + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Modified facet configuration response + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.modifyFacetConfigurationV2({ + * name: 'color', + * displayName: 'Color', + * sortOrder: 'num_matches', + * }); + */ + modifyFacetConfigurationV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, name, ...rest } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl(`facets/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PATCH', + body: JSON.stringify(toSnakeCaseKeys(rest)), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Remove a facet configuration (V2) + * + * Caution: Once a facet group's configuration is removed, all configurations will return to their default values. + * This includes all facet option configurations (display name, position, etc) you may have defined. + * + * @function removeFacetConfigurationV2 + * @param {object} parameters - Additional parameters for facet configuration details + * @param {string} parameters.name - Unique facet name used to refer to the facet in your catalog + * @param {string} [parameters.section] - The section in which your facet is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Removed facet configuration response + * @see https://docs.constructor.com/reference/configuration-facets + * @example + * constructorio.catalog.removeFacetConfigurationV2({ + * name: 'color', + * }); + */ + removeFacetConfigurationV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { section, name } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section: section || 'Products', + }; + + try { + requestUrl = createCatalogUrl(`facets/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'DELETE', + headers: { + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + // -------------------- V2 Searchabilities Methods -------------------- + + /** + * Retrieve all searchabilities (V2) + * + * @function retrieveSearchabilitiesV2 + * @param {object} parameters - Additional parameters for retrieving searchabilities + * @param {string} [parameters.name] - Name of searchability field to filter for + * @param {number} [parameters.page] - Page number you'd like to request + * @param {number} [parameters.offset] - The number of results to skip from the beginning + * @param {number} [parameters.numResultsPerPage] - Number of searchabilities per page. Default value is 20. + * @param {boolean} [parameters.fuzzySearchable] - Filter by fuzzy_searchable + * @param {boolean} [parameters.exactSearchable] - Filter by exact_searchable + * @param {boolean} [parameters.displayable] - Filter by displayable + * @param {string} [parameters.matchType] - Whether filters should be ANDed or ORed ('and' or 'or') + * @param {string} [parameters.sortBy] - The criteria by which searchabilities should be sorted ('name') + * @param {string} [parameters.sortOrder] - The sort order ('ascending' or 'descending') + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{searchabilities: object[], total_count: number}>} - Paginated list of searchability configurations + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.retrieveSearchabilitiesV2({ + * page: 1, + * numResultsPerPage: 50, + * }); + */ + retrieveSearchabilitiesV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + // Support both camelCase and snake_case for backwards compatibility + const { + name, + page, + offset, + num_results_per_page, + numResultsPerPage = num_results_per_page, + fuzzy_searchable, + fuzzySearchable = fuzzy_searchable, + exact_searchable, + exactSearchable = exact_searchable, + displayable, + match_type, + matchType = match_type, + sort_by, + sortBy = sort_by, + sort_order, + sortOrder = sort_order, + section = 'Products', + } = parameters; + const additionalQueryParams = {}; + + if (section) { + additionalQueryParams.section = section; + } + + if (!helpers.isNil(page)) { + additionalQueryParams.page = page; + } + + if (!helpers.isNil(offset)) { + additionalQueryParams.offset = offset; + } + + if (!helpers.isNil(numResultsPerPage)) { + additionalQueryParams.num_results_per_page = numResultsPerPage; + } + + if (name) { + additionalQueryParams.name = name; + } + + if (!helpers.isNil(fuzzySearchable)) { + additionalQueryParams.fuzzy_searchable = fuzzySearchable; + } + + if (!helpers.isNil(exactSearchable)) { + additionalQueryParams.exact_searchable = exactSearchable; + } + + if (!helpers.isNil(displayable)) { + additionalQueryParams.displayable = displayable; + } + + if (matchType) { + additionalQueryParams.match_type = matchType; + } + + if (sortBy) { + additionalQueryParams.sort_by = sortBy; + } + + if (sortOrder) { + additionalQueryParams.sort_order = sortOrder; + } + + try { + requestUrl = createCatalogUrl('searchabilities', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Get a single searchability configuration (V2) + * + * @function getSearchabilityV2 + * @param {object} parameters - Additional parameters for retrieving a searchability + * @param {string} parameters.name - Name of the searchability field + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Searchability configuration response + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.getSearchabilityV2({ + * name: 'keywords', + * }); + */ + getSearchabilityV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const { name, section = 'Products' } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section, + }; + + try { + requestUrl = createCatalogUrl(`searchabilities/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Create or update searchabilities (V2) + * + * @function patchSearchabilitiesV2 + * @param {object} parameters - Additional parameters for patching searchabilities + * @param {object[]} parameters.searchabilities - Array of searchabilities to create or update + * @param {boolean} [parameters.skipRebuild] - Skip index rebuild. Default value is false. + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{searchabilities: object[], total_count: number}>} - Updated searchability configurations + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.patchSearchabilitiesV2({ + * searchabilities: [ + * { + * name: 'style_id', + * exactSearchable: true, + * }, + * { + * name: 'keywords', + * fuzzySearchable: true, + * }, + * ], + * }); + */ + patchSearchabilitiesV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + // Support both camelCase and snake_case for backwards compatibility + const { skip_rebuild, skipRebuild = skip_rebuild } = parameters; + const { searchabilities: searchabilitiesRaw, section = 'Products' } = parameters; + + if (!searchabilitiesRaw || !Array.isArray(searchabilitiesRaw)) { + return Promise.reject(new Error('searchabilities is a required parameter of type array')); + } + + const searchabilities = searchabilitiesRaw.map((config) => toSnakeCaseKeys(config)); + const additionalQueryParams = { + section, + }; + + if (!helpers.isNil(skipRebuild)) { + additionalQueryParams.skip_rebuild = skipRebuild; + } + + try { + requestUrl = createCatalogUrl('searchabilities', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PATCH', + body: JSON.stringify({ searchabilities }), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Update a single searchability configuration (V2) + * + * @function patchSearchabilityV2 + * @param {object} parameters - Additional parameters for patching a searchability + * @param {string} parameters.name - Name of the searchability field + * @param {boolean} [parameters.fuzzySearchable] - Whether the field is fuzzy searchable + * @param {boolean} [parameters.exactSearchable] - Whether the field is exact searchable + * @param {boolean} [parameters.displayable] - Whether the field is displayable + * @param {boolean} [parameters.hidden] - Whether the field is hidden + * @param {boolean} [parameters.skipRebuild] - Skip index rebuild. Default value is false. + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Updated searchability configuration response + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.patchSearchabilityV2({ + * name: 'keywords', + * fuzzySearchable: true, + * }); + */ + patchSearchabilityV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + // Support both camelCase and snake_case for backwards compatibility + const { skip_rebuild, skipRebuild = skip_rebuild } = parameters; + const { name, skip_rebuild: _sr, skipRebuild: _srCamel, section = 'Products', ...rest } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section, + }; + + if (!helpers.isNil(skipRebuild)) { + additionalQueryParams.skip_rebuild = skipRebuild; + } + + try { + requestUrl = createCatalogUrl(`searchabilities/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PATCH', + body: JSON.stringify(toSnakeCaseKeys(rest)), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Delete searchabilities (V2) + * + * @function deleteSearchabilitiesV2 + * @param {object} parameters - Additional parameters for deleting searchabilities + * @param {object[]} parameters.searchabilities - Array of searchabilities names to delete + * @param {boolean} [parameters.skipRebuild] - Skip index rebuild. Default value is false. + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise<{searchabilities: object[], total_count: number}>} - Deleted searchability configurations + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.deleteSearchabilitiesV2({ + * searchabilities: [ + * { name: 'style_id' }, + * { name: 'keywords' }, + * ], + * }); + */ + deleteSearchabilitiesV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + // Support both camelCase and snake_case for backwards compatibility + const { skip_rebuild, skipRebuild = skip_rebuild } = parameters; + const { searchabilities: searchabilitiesRaw, section = 'Products' } = parameters; + + if (!searchabilitiesRaw || !Array.isArray(searchabilitiesRaw)) { + return Promise.reject(new Error('searchabilities is a required parameter of type array')); + } + + const searchabilities = searchabilitiesRaw.map((config) => toSnakeCaseKeys(config)); + const additionalQueryParams = { + section, + }; + + if (!helpers.isNil(skipRebuild)) { + additionalQueryParams.skip_rebuild = skipRebuild; + } + + try { + requestUrl = createCatalogUrl('searchabilities', this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'DELETE', + body: JSON.stringify({ searchabilities }), + headers: { + 'Content-Type': 'application/json', + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } + + /** + * Delete a single searchability configuration (V2) + * + * @function deleteSearchabilityV2 + * @param {object} parameters - Additional parameters for deleting a searchability + * @param {string} parameters.name - Name of the searchability field to delete + * @param {boolean} [parameters.skipRebuild] - Skip index rebuild. Default value is false. + * @param {string} [parameters.section] - The section in which the searchability is defined. Default value is Products. + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} - Deleted searchability configuration response + * @see https://docs.constructor.com/reference/configuration-searchabilities + * @example + * constructorio.catalog.deleteSearchabilityV2({ + * name: 'keywords', + * }); + */ + deleteSearchabilityV2(parameters = {}, networkParameters = {}) { + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + // Support both camelCase and snake_case for backwards compatibility + const { skip_rebuild, skipRebuild = skip_rebuild } = parameters; + const { name, section = 'Products' } = parameters; + + if (!name || typeof name !== 'string') { + return Promise.reject(new Error('name is a required parameter of type string')); + } + + const additionalQueryParams = { + section, + }; + + if (!helpers.isNil(skipRebuild)) { + additionalQueryParams.skip_rebuild = skipRebuild; + } + + try { + requestUrl = createCatalogUrl(`searchabilities/${encodeURIComponent(name)}`, this.options, additionalQueryParams, 'v2'); + } catch (e) { + return Promise.reject(e); + } + + // Handle network timeout if specified + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'DELETE', + headers: { + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } } module.exports = Catalog; diff --git a/src/types/catalog.d.ts b/src/types/catalog.d.ts index ff1af734..c72abd2b 100644 --- a/src/types/catalog.d.ts +++ b/src/types/catalog.d.ts @@ -15,6 +15,10 @@ import { SynonymGroup, SearchabilityConfiguration, SearchabilityConfigurationResponse, + FacetConfigurationV2, + FacetConfigurationV2Response, + SearchabilityConfigurationV2, + SearchabilityConfigurationV2Response, } from '.'; export default Catalog; @@ -272,6 +276,82 @@ export interface PatchSearchabilitiesParameters { section?: string; } +// V2 Facet Configuration Parameters +export interface GetFacetConfigurationsV2Parameters { + page?: number; + numResultsPerPage?: number; + offset?: number; + section?: string; +} + +export interface GetFacetConfigurationV2Parameters { + name: string; + section?: string; +} + +export interface CreateOrReplaceFacetConfigurationsV2Parameters { + facetConfigurations: FacetConfigurationV2[]; + section?: string; +} + +export interface ModifyFacetConfigurationsV2Parameters { + facetConfigurations: (Partial & { name: string })[]; + section?: string; +} + +export interface RemoveFacetConfigurationV2Parameters { + name: string; + section?: string; +} + +// V2 Searchabilities Parameters +export interface RetrieveSearchabilitiesV2Parameters { + name?: string; + page?: number; + offset?: number; + numResultsPerPage?: number; + fuzzySearchable?: boolean; + exactSearchable?: boolean; + displayable?: boolean; + matchType?: 'and' | 'or'; + sortBy?: 'name'; + sortOrder?: 'ascending' | 'descending'; + section?: string; +} + +export interface GetSearchabilityV2Parameters { + name: string; + section?: string; +} + +export interface PatchSearchabilitiesV2Parameters { + searchabilities: SearchabilityConfigurationV2[]; + skipRebuild?: boolean; + section?: string; +} + +export interface PatchSearchabilityV2Parameters { + name: string; + fuzzySearchable?: boolean; + exactSearchable?: boolean; + displayable?: boolean; + hidden?: boolean; + skipRebuild?: boolean; + section?: string; +} + +export interface DeleteSearchabilitiesV2Parameters { + searchabilities: { name: string }[]; + skipRebuild?: boolean; + section?: string; +} + +export interface DeleteSearchabilityV2Parameters { + name: string; + skipRebuild?: boolean; + section?: string; +} + interface CatalogMutationResponse { task_id: string; task_status_path: string; @@ -604,4 +684,92 @@ declare class Catalog { ): Promise<{ searchabilities: SearchabilityConfigurationResponse[]; }>; + + // V2 Facet Configuration Methods + addFacetConfigurationV2( + parameters: FacetConfigurationV2, + networkParameters?: NetworkParameters + ): Promise; + + getFacetConfigurationsV2( + parameters?: GetFacetConfigurationsV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + facets: FacetConfigurationV2Response[]; + total_count: number; + }>; + + getFacetConfigurationV2( + parameters: GetFacetConfigurationV2Parameters, + networkParameters?: NetworkParameters + ): Promise; + + createOrReplaceFacetConfigurationsV2( + parameters: CreateOrReplaceFacetConfigurationsV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + facets: FacetConfigurationV2Response[]; + }>; + + modifyFacetConfigurationsV2( + parameters: ModifyFacetConfigurationsV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + facets: FacetConfigurationV2Response[]; + }>; + + replaceFacetConfigurationV2( + parameters: FacetConfigurationV2, + networkParameters?: NetworkParameters + ): Promise; + + modifyFacetConfigurationV2( + parameters: Partial & { name: string }, + networkParameters?: NetworkParameters + ): Promise; + + removeFacetConfigurationV2( + parameters: RemoveFacetConfigurationV2Parameters, + networkParameters?: NetworkParameters + ): Promise; + + // V2 Searchabilities Methods + retrieveSearchabilitiesV2( + parameters?: RetrieveSearchabilitiesV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + searchabilities: SearchabilityConfigurationV2Response[]; + total_count: number; + }>; + + getSearchabilityV2( + parameters: GetSearchabilityV2Parameters, + networkParameters?: NetworkParameters + ): Promise; + + patchSearchabilitiesV2( + parameters: PatchSearchabilitiesV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + searchabilities: SearchabilityConfigurationV2Response[]; + total_count: number; + }>; + + patchSearchabilityV2( + parameters: PatchSearchabilityV2Parameters, + networkParameters?: NetworkParameters + ): Promise; + + deleteSearchabilitiesV2( + parameters: DeleteSearchabilitiesV2Parameters, + networkParameters?: NetworkParameters + ): Promise<{ + searchabilities: SearchabilityConfigurationV2Response[]; + total_count: number; + }>; + + deleteSearchabilityV2( + parameters: DeleteSearchabilityV2Parameters, + networkParameters?: NetworkParameters + ): Promise; } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 31bc5f83..6e98d9fd 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -335,6 +335,69 @@ export interface SearchabilityConfiguration { hidden?: boolean, } +// V2 Facet Types +export interface FacetConfigurationV2 { + name: string; + pathInMetadata: string; + type: 'multiple' | 'hierarchical' | 'range'; + displayName?: string; + sortOrder?: 'relevance' | 'value' | 'num_matches'; + sortDescending?: boolean; + rangeType?: 'static' | null; + rangeFormat?: 'boundaries' | 'options' | null; + rangeInclusive?: 'above' | 'below' | null; + rangeLimits?: number[]; + matchType?: 'any' | 'all' | 'none'; + position?: number | null; + hidden?: boolean; + protected?: boolean; + countable?: boolean; + optionsLimit?: number; + data?: Record; + section?: string; +} + +export interface FacetConfigurationV2Response { + name: string; + path_in_metadata: string; + type: 'multiple' | 'hierarchical' | 'range'; + display_name?: string | null; + sort_order?: 'relevance' | 'value' | 'num_matches'; + sort_descending?: boolean; + range_type?: 'static' | null; + range_format?: 'boundaries' | 'options' | null; + range_inclusive?: 'above' | 'below' | null; + range_limits?: number[] | null; + match_type?: 'any' | 'all' | 'none'; + position?: number | null; + hidden?: boolean; + protected?: boolean; + countable?: boolean; + options_limit?: number; + data?: Record; + created_at?: string; + updated_at?: string; +} + +// V2 Searchability Types +export interface SearchabilityConfigurationV2 { + name: string; + fuzzySearchable?: boolean; + exactSearchable?: boolean; + displayable?: boolean; + hidden?: boolean; +} + +export interface SearchabilityConfigurationV2Response { + name: string; + fuzzy_searchable: boolean; + exact_searchable: boolean; + displayable: boolean; + hidden: boolean; + created_at: string; + updated_at?: string; +} + export interface ItemTracked { itemName?: string; itemId?: string; From 0575461eefe1c79958a32fed435fe5491387a2a8 Mon Sep 17 00:00:00 2001 From: Andrei Andrukhovich Date: Tue, 30 Dec 2025 08:26:55 -0700 Subject: [PATCH 2/2] fix catch(done) --- .../catalog-facet-configurations-v2.js | 34 +++++++++---------- .../catalog/catalog-searchabilities-v2.js | 34 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/spec/src/modules/catalog/catalog-facet-configurations-v2.js b/spec/src/modules/catalog/catalog-facet-configurations-v2.js index a5f30add..8a6651a0 100644 --- a/spec/src/modules/catalog/catalog-facet-configurations-v2.js +++ b/spec/src/modules/catalog/catalog-facet-configurations-v2.js @@ -97,7 +97,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Backwards Compatibility `display_name` - Should resolve when adding a facet configuration', (done) => { @@ -116,7 +116,7 @@ describe('ConstructorIO - Catalog', () => { expect(response).to.have.property('display_name').to.be.equal(newFacetConfiguration.display_name); done(); - }); + }).catch(done); }); it('Should return error when adding a facet configuration that already exists', () => { @@ -176,7 +176,7 @@ describe('ConstructorIO - Catalog', () => { // Push mock facet configuration into saved list to be cleaned up afterwards facetConfigurations.push(mockFacetConfiguration); done(); - }); + }).catch(done); }); it('Should return a response when getting facet configurations', (done) => { @@ -196,7 +196,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return a response when getting facet configurations with pagination parameters', (done) => { @@ -214,7 +214,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('1'); expect(requestedUrlParams).to.have.property('page').to.equal('1'); done(); - }); + }).catch(done); }); it('Should return a response when getting facet configurations with offset parameter', (done) => { @@ -230,7 +230,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('offset').to.equal('0'); done(); - }); + }).catch(done); }); if (!skipNetworkTimeoutTests) { @@ -265,7 +265,7 @@ describe('ConstructorIO - Catalog', () => { // Push mock facet configuration into saved list to be cleaned up afterwards facetConfigurations.push(existingFacetConfiguration); done(); - }); + }).catch(done); }); it('Should return a response when getting a single facet configuration', (done) => { @@ -285,7 +285,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return error when getting a facet configuration that does not exist', () => { @@ -329,7 +329,7 @@ describe('ConstructorIO - Catalog', () => { // Push mock facet configuration into saved list to be cleaned up afterwards facetConfigurations.push(existingFacetConfiguration); done(); - }); + }).catch(done); }); it('Should return a response when modifying multiple facet configurations', (done) => { @@ -357,7 +357,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return error when facetConfigurations parameter is missing', () => { @@ -392,7 +392,7 @@ describe('ConstructorIO - Catalog', () => { // Push mock facet configuration into saved list to be cleaned up afterwards facetConfigurations.push(existingFacetConfiguration); done(); - }); + }).catch(done); }); it('Should return a response when modifying a single facet configuration', (done) => { @@ -417,7 +417,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return error when modifying a facet configuration that does not exist', () => { @@ -452,7 +452,7 @@ describe('ConstructorIO - Catalog', () => { // Push mock facet configuration into saved list to be cleaned up afterwards facetConfigurations.push(existingFacetConfiguration); done(); - }); + }).catch(done); }); it('Should return a response when replacing a facet configuration', (done) => { @@ -478,7 +478,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); if (!skipNetworkTimeoutTests) { @@ -521,7 +521,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); if (!skipNetworkTimeoutTests) { @@ -545,7 +545,7 @@ describe('ConstructorIO - Catalog', () => { catalog.addFacetConfigurationV2(facetToRemove).then(() => { done(); - }); + }).catch(done); }); it('Should return a response when removing a facet configuration', (done) => { @@ -563,7 +563,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('key'); done(); - }); + }).catch(done); }); it('Should return error when removing a facet configuration that does not exist', () => { diff --git a/spec/src/modules/catalog/catalog-searchabilities-v2.js b/spec/src/modules/catalog/catalog-searchabilities-v2.js index bad9900d..241e45a1 100644 --- a/spec/src/modules/catalog/catalog-searchabilities-v2.js +++ b/spec/src/modules/catalog/catalog-searchabilities-v2.js @@ -95,7 +95,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return a response when retrieving searchabilities with pagination parameters', (done) => { @@ -113,7 +113,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('5'); expect(requestedUrlParams).to.have.property('page').to.equal('1'); done(); - }); + }).catch(done); }); it('Should return a response when retrieving searchabilities with filter parameters', (done) => { @@ -129,7 +129,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('displayable').to.equal('true'); done(); - }); + }).catch(done); }); it('Should return a response when retrieving searchabilities with sort parameters', (done) => { @@ -146,7 +146,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('sort_by').to.equal('name'); expect(requestedUrlParams).to.have.property('sort_order').to.equal('ascending'); done(); - }); + }).catch(done); }); it('Should return a response when retrieving searchabilities with name filter', (done) => { @@ -162,7 +162,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('name').to.equal('keywords'); done(); - }); + }).catch(done); }); if (!skipNetworkTimeoutTests) { @@ -215,7 +215,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('section'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return a response when patching searchabilities with skipRebuild', (done) => { @@ -237,7 +237,7 @@ describe('ConstructorIO - Catalog', () => { expect(res).to.have.property('searchabilities').to.be.an('array'); expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); done(); - }); + }).catch(done); }); it('Should return error when patching searchabilities with unsupported values', () => { @@ -300,7 +300,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return error when getting a searchability that does not exist', () => { @@ -344,7 +344,7 @@ describe('ConstructorIO - Catalog', () => { catalog.patchSearchabilitiesV2({ searchabilities: [existingSearchability] }).then(() => { searchabilitiesToCleanup.push(existingSearchability); done(); - }); + }).catch(done); }); it('Should return a response when patching a single searchability', (done) => { @@ -367,7 +367,7 @@ describe('ConstructorIO - Catalog', () => { expect(requestedUrlParams).to.have.property('key'); expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion); done(); - }); + }).catch(done); }); it('Should return a response when patching a single searchability with skipRebuild', (done) => { @@ -386,7 +386,7 @@ describe('ConstructorIO - Catalog', () => { expect(res).to.have.property('name').to.equal(existingSearchability.name); expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); done(); - }); + }).catch(done); }); it('Should return error when patching a searchability that does not exist', () => { @@ -420,7 +420,7 @@ describe('ConstructorIO - Catalog', () => { catalog.patchSearchabilitiesV2({ searchabilities: [searchabilityToDelete] }).then(() => { done(); - }); + }).catch(done); }); it('Should return a response when deleting searchabilities', (done) => { @@ -441,7 +441,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('key'); done(); - }); + }).catch(done); }); it('Should return a response when deleting searchabilities with skipRebuild', (done) => { @@ -459,7 +459,7 @@ describe('ConstructorIO - Catalog', () => { expect(res).to.have.property('searchabilities').to.be.an('array'); expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); done(); - }); + }).catch(done); }); if (!skipNetworkTimeoutTests) { @@ -484,7 +484,7 @@ describe('ConstructorIO - Catalog', () => { catalog.patchSearchabilitiesV2({ searchabilities: [searchabilityToDelete] }).then(() => { done(); - }); + }).catch(done); }); it('Should return a response when deleting a single searchability', (done) => { @@ -502,7 +502,7 @@ describe('ConstructorIO - Catalog', () => { expect(fetchSpy).to.have.been.called; expect(requestedUrlParams).to.have.property('key'); done(); - }); + }).catch(done); }); it('Should return a response when deleting a single searchability with skipRebuild', (done) => { @@ -517,7 +517,7 @@ describe('ConstructorIO - Catalog', () => { expect(res).to.have.property('name').to.equal(searchabilityToDelete.name); expect(requestedUrlParams).to.have.property('skip_rebuild').to.equal('true'); done(); - }); + }).catch(done); }); it('Should return error when deleting a searchability that does not exist', () => {