From 88a480dda6ea9e73f644c4b84d788aae86b1d82e Mon Sep 17 00:00:00 2001 From: Sherzod Date: Fri, 5 Jun 2026 16:54:41 +0500 Subject: [PATCH 01/14] Implement campaigns module with support for the Campaigns API --- cspell.json | 3 +- spec/src/modules/campaigns.js | 890 ++++++++++++++++++++++++++++++++++ src/constructorio.js | 3 + src/modules/campaigns.js | 440 +++++++++++++++++ src/types/campaigns.d.ts | 215 ++++++++ src/types/constructorio.d.ts | 3 + src/types/index.d.ts | 1 + 7 files changed, 1554 insertions(+), 1 deletion(-) create mode 100644 spec/src/modules/campaigns.js create mode 100644 src/modules/campaigns.js create mode 100644 src/types/campaigns.d.ts diff --git a/cspell.json b/cspell.json index b24dc067..0c3caa7b 100644 --- a/cspell.json +++ b/cspell.json @@ -44,6 +44,7 @@ "forcesync", "forcedelta", "forcepatchdelta", - "skippable" + "skippable", + "timeofday" ] } diff --git a/spec/src/modules/campaigns.js b/spec/src/modules/campaigns.js new file mode 100644 index 00000000..f2eb9a3a --- /dev/null +++ b/spec/src/modules/campaigns.js @@ -0,0 +1,890 @@ +/* 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 cloneDeep = require('lodash.clonedeep'); +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'; + +describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { + // Ensure Mocha doesn't time out waiting for operation to complete + this.timeout(10000); + + const clientVersion = 'cio-mocha'; + // Track campaigns created during the suite so they can be cleaned up afterwards + const createdCampaignIds = []; + let fetchSpy; + let campaignId; + + before(async () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + }); + + // Save id of an existing campaign for the following tests below + await campaigns.retrieveCampaigns({ numResultsPerPage: 1 }).then((res) => { + if (res.campaigns && res.campaigns.length) { + campaignId = res.campaigns[0].id; + } + }); + }); + + after(async () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + }); + + // Clean up campaigns created during the suite + for (const id of createdCampaignIds) { + // eslint-disable-next-line no-await-in-loop + await campaigns.deleteCampaign({ id }).catch(() => {}); + } + }); + + beforeEach(() => { + global.CLIENT_VERSION = clientVersion; + fetchSpy = sinon.spy(nodeFetch); + }); + + afterEach((done) => { + delete global.CLIENT_VERSION; + + fetchSpy = null; + + setTimeout(done, sendTimeout); + }); + + describe('retrieveCampaigns', () => { + it('Should retrieve a list of campaigns', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns().then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + expect(res).to.have.property('total_count').to.be.a('number'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given section', (done) => { + const section = 'Products'; + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns({ section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('section').to.equal(section); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given page and results per page', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns({ page: 1, numResultsPerPage: 50 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('page').to.equal('1'); + expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('50'); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + expect(res.campaigns.length).to.be.lte(50); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given page and results per page when passing parameters snake_case', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns({ page: 1, num_results_per_page: 50 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('50'); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + expect(res.campaigns.length).to.be.lte(50); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given offset', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns({ offset: 1 }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('offset').to.equal('1'); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should pass the correct custom headers passed in function networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should pass the correct custom headers passed in global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + }, + }, + }); + + campaigns.retrieveCampaigns().then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + 'X-Constructor-IO-Test-Another': 'test', + }, + }, + }); + + campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test2'); + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test-Another').to.equal('test'); + done(); + }); + }); + + it('Should return error when retrieving a list of campaigns with an invalid API key', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiKey = 'notanapikey'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejected; + }); + + it('Should return error when retrieving a list of campaigns with an invalid API token', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiToken = 'notanapitoken'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO(validOptions); + + return expect(campaigns.retrieveCampaigns({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('retrieveCampaign', () => { + it('Should retrieve a campaign given a specific id', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaign({ id: campaignId }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + + // Response + expect(res).to.have.property('id').to.equal(campaignId); + + done(); + }); + }); + + it('Should retrieve a campaign given a specific id and section', (done) => { + const section = 'Products'; + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaign({ id: campaignId, section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('section').to.equal(section); + + // Response + expect(res).to.have.property('id').to.equal(campaignId); + + done(); + }); + }); + + it('Should pass the correct custom headers passed in function networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + + // Response + expect(res).to.have.property('id').to.equal(campaignId); + + done(); + }); + }); + + it('Should pass the correct custom headers passed in global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + }, + }, + }); + + campaigns.retrieveCampaign({ id: campaignId }).then((res) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + + // Response + expect(res).to.have.property('id').to.equal(campaignId); + + done(); + }); + }); + + it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + 'X-Constructor-IO-Test-Another': 'test', + }, + }, + }); + + campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test2'); + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test-Another').to.equal('test'); + + // Response + expect(res).to.have.property('id').to.equal(campaignId); + + done(); + }); + }); + + it('Should return error when retrieving a campaign given a specific id with an invalid API key', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiKey = 'notanapikey'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + }); + + it('Should return error when retrieving a campaign given a specific id with an invalid API token', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiToken = 'notanapitoken'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO(validOptions); + + return expect(campaigns.retrieveCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('createCampaign', () => { + const createCampaignName = `test-campaign-${Date.now()}`; + + it('Should create a campaign given a name', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.createCampaign({ name: createCampaignName }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + createdCampaignIds.push(res.id); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedBody).to.have.property('name').to.equal(createCampaignName); + + // Response + expect(res).to.have.property('id').to.be.a('number'); + expect(res).to.have.property('name').to.equal(createCampaignName); + + done(); + }); + }); + + it('Should create a campaign given a name, description and section', (done) => { + const section = 'Products'; + const description = 'Seasonal promotion campaign'; + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.createCampaign({ name: `${createCampaignName}-2`, description, section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + createdCampaignIds.push(res.id); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('section').to.equal(section); + expect(requestedBody).to.have.property('description').to.equal(description); + expect(requestedBody).to.not.have.property('section'); + + // Response + expect(res).to.have.property('id').to.be.a('number'); + expect(res).to.have.property('description').to.equal(description); + + done(); + }); + }); + + it('Should pass the correct custom headers passed in function networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.createCampaign({ name: `${createCampaignName}-4` }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should pass the correct custom headers passed in global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + }, + }, + }); + + campaigns.createCampaign({ name: `${createCampaignName}-5` }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + 'X-Constructor-IO-Test-Another': 'test', + }, + }, + }); + + campaigns.createCampaign({ name: `${createCampaignName}-6` }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test2'); + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test-Another').to.equal('test'); + done(); + }); + }); + + it('Should return error when creating a campaign with an invalid API key', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiKey = 'notanapikey'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-key` })).to.eventually.be.rejected; + }); + + it('Should return error when creating a campaign with an invalid API token', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiToken = 'notanapitoken'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-token` })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO(validOptions); + + return expect(campaigns.createCampaign({ name: `${createCampaignName}-timeout` }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.createCampaign({ name: `${createCampaignName}-global-timeout` })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('updateCampaign', () => { + let updateCampaignId; + + before(async () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + }); + + // Create a campaign to update in the following tests below + await campaigns.createCampaign({ name: `test-campaign-update-${Date.now()}` }).then((res) => { + updateCampaignId = res.id; + createdCampaignIds.push(res.id); + }); + }); + + it('Should update a campaign given an id and name', (done) => { + const updatedName = `updated-campaign-${Date.now()}`; + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.updateCampaign({ id: updateCampaignId, name: updatedName }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedBody).to.have.property('name').to.equal(updatedName); + expect(requestedBody).to.not.have.property('id'); + + // Response + expect(res).to.have.property('id').to.equal(updateCampaignId); + expect(res).to.have.property('name').to.equal(updatedName); + + done(); + }); + }); + + it('Should update a campaign given a description and section', (done) => { + const section = 'Products'; + const description = 'Updated seasonal promotion campaign'; + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.updateCampaign({ id: updateCampaignId, description, section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('section').to.equal(section); + expect(requestedBody).to.have.property('description').to.equal(description); + expect(requestedBody).to.not.have.property('section'); + expect(requestedBody).to.not.have.property('id'); + + // Response + expect(res).to.have.property('id').to.equal(updateCampaignId); + expect(res).to.have.property('description').to.equal(description); + + done(); + }); + }); + + it('Should pass the correct custom headers passed in function networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should pass the correct custom headers passed in global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + }, + }, + }); + + campaigns.updateCampaign({ id: updateCampaignId }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + done(); + }); + }); + + it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + 'X-Constructor-IO-Test-Another': 'test', + }, + }, + }); + + campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test2'); + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test-Another').to.equal('test'); + done(); + }); + }); + + it('Should return error when updating a campaign with an invalid API key', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiKey = 'notanapikey'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + }); + + it('Should return error when updating a campaign with an invalid API token', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiToken = 'notanapitoken'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO(validOptions); + + return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); + + describe('deleteCampaign', () => { + // Create a fresh campaign before each test so it can be safely deleted + async function createCampaignToDelete() { + const { campaigns } = new ConstructorIO({ + ...validOptions, + }); + + const res = await campaigns.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}` }); + + return res.id; + } + + it('Should delete a campaign given an id', async () => { + const idToDelete = await createCampaignToDelete(); + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + await campaigns.deleteCampaign({ id: idToDelete }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + + // Response + expect(res).to.equal(undefined); + }); + }); + + it('Should delete a campaign given an id and section', async () => { + const section = 'Products'; + const idToDelete = await createCampaignToDelete(); + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + await campaigns.deleteCampaign({ id: idToDelete, section }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('section').to.equal(section); + + // Response + expect(res).to.equal(undefined); + }); + }); + + it('Should pass the correct custom headers passed in function networkParameters', async () => { + const idToDelete = await createCampaignToDelete(); + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + await campaigns.deleteCampaign({ id: idToDelete }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + }); + }); + + it('Should pass the correct custom headers passed in global networkParameters', async () => { + const idToDelete = await createCampaignToDelete(); + const { campaigns } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + networkParameters: { + headers: { + 'X-Constructor-IO-Test': 'test', + }, + }, + }); + + await campaigns.deleteCampaign({ id: idToDelete }).then(() => { + const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + + expect(fetchSpy).to.have.been.called; + expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); + }); + }); + + it('Should return error when deleting a campaign with an invalid API key', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiKey = 'notanapikey'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + }); + + it('Should return error when deleting a campaign with an invalid API token', () => { + const invalidOptions = cloneDeep(validOptions); + invalidOptions.apiToken = 'notanapitoken'; + + const { campaigns } = new ConstructorIO({ + ...invalidOptions, + fetch: fetchSpy, + }); + + return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO(validOptions); + + return expect(campaigns.deleteCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + + it('Should be rejected when global network request timeout is provided and reached', () => { + const { campaigns } = new ConstructorIO({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + }); + } + }); +}); diff --git a/src/constructorio.js b/src/constructorio.js index 9fff6963..23b841de 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -8,6 +8,7 @@ const Autocomplete = require('./modules/autocomplete'); const Recommendations = require('./modules/recommendations'); const Tasks = require('./modules/tasks'); const Quizzes = require('./modules/quizzes'); +const Campaigns = require('./modules/campaigns'); const { version: packageVersion } = require('../package.json'); const utils = require('./utils/helpers'); @@ -42,6 +43,7 @@ class ConstructorIO { * @property {object} catalog - Interface to {@link module:catalog} * @property {object} tasks - Interface to {@link module:tasks} * @property {object} quizzes - Interface to {@link module:quizzes} + * @property {object} campaigns - Interface to {@link module:campaigns} * @returns {class} */ constructor(options = {}) { @@ -83,6 +85,7 @@ class ConstructorIO { this.recommendations = new Recommendations(this.options); this.tasks = new Tasks(this.options); this.quizzes = new Quizzes(this.options); + this.campaigns = new Campaigns(this.options); } } diff --git a/src/modules/campaigns.js b/src/modules/campaigns.js new file mode 100644 index 00000000..74f72096 --- /dev/null +++ b/src/modules/campaigns.js @@ -0,0 +1,440 @@ +/* eslint-disable max-len */ +/* eslint-disable camelcase, no-underscore-dangle, no-unneeded-ternary, brace-style */ +const qs = require('qs'); +const { AbortController } = require('node-abort-controller'); +const helpers = require('../utils/helpers'); + +// Create URL from supplied path and options +function createCampaignsUrl(path, options, additionalQueryParams = {}, apiVersion = 'v1') { + const { + apiKey, + serviceUrl, + version, + } = options; + let queryParams = { + c: version, + ...additionalQueryParams, + }; + + // Validate path is provided + if (!path || typeof path !== 'string') { + throw new Error('path is a required parameter of type string'); + } + + queryParams.key = apiKey; + queryParams = helpers.cleanParams(queryParams); + + const queryString = qs.stringify(queryParams, { indices: false }); + + const encodedPath = path.split('/').map(encodeURIComponent).join('/'); + + return `${serviceUrl}/${encodeURIComponent(apiVersion)}/${encodedPath}?${queryString}`; +} + +/** + * Interface to searchandizing campaign related API calls + * + * @module campaigns + * @inner + * @returns {object} + */ +class Campaigns { + constructor(options) { + this.options = options || {}; + } + + /** + * Retrieve campaigns + * + * @function retrieveCampaigns + * @param {object} [parameters] - Additional parameters for retrieving campaigns + * @param {string} [parameters.section='Products'] - The section of the index to use + * @param {number|number[]} [parameters.id] - The ID(s) of campaigns to filter by + * @param {object} [parameters.refinedFilters] - An object of refined filters to filter by + * @param {number} [parameters.numResultsPerPage=20] - The number of campaigns to return - maximum value 100 + * @param {number} [parameters.page] - The page of results to return + * @param {number} [parameters.offset] - The number of results to skip from the beginning - cannot be used together with `page` + * @param {object} [parameters.refinedRecommendationContexts] - A filter for refined recommendation contexts + * @param {string[]} [parameters.refinedQueries] - A list of refined queries to filter by + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} + * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaigns + * @example + * constructorio.campaigns.retrieveCampaigns({ + * section: 'Products', + * numResultsPerPage: 50, + * page: 1, + * }); + */ + retrieveCampaigns(parameters = {}, networkParameters = {}) { + const queryParams = {}; + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const headers = { + 'Content-Type': 'application/json', + }; + + if (parameters) { + const { + section, + id, + refined_filters, + refinedFilters = refined_filters, + num_results_per_page, + numResultsPerPage = num_results_per_page, + page, + offset, + refined_recommendation_contexts, + refinedRecommendationContexts = refined_recommendation_contexts, + refined_queries, + refinedQueries = refined_queries, + } = parameters; + + // Pull section from parameters + if (section) { + queryParams.section = section; + } + + // Pull id from parameters + if (id) { + queryParams.id = id; + } + + // Pull refined filters from parameters + if (refinedFilters) { + queryParams.refined_filters = refinedFilters; + } + + // Pull number of results per page from parameters + if (numResultsPerPage) { + queryParams.num_results_per_page = numResultsPerPage; + } + + // Pull page from parameters + if (page) { + queryParams.page = page; + } + + // Pull offset from parameters + if (offset) { + queryParams.offset = offset; + } + + // Pull refined recommendation contexts from parameters + if (refinedRecommendationContexts) { + queryParams.refined_recommendation_contexts = refinedRecommendationContexts; + } + + // Pull refined queries from parameters + if (refinedQueries) { + queryParams.refined_queries = refinedQueries; + } + } + + try { + requestUrl = createCampaignsUrl('campaigns', this.options, queryParams); + } catch (e) { + return Promise.reject(e); + } + + Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters)); + + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + ...headers, + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }).then((json) => json); + } + + /** + * Retrieve a campaign given a specific id + * + * @function retrieveCampaign + * @param {object} parameters - Additional parameters for retrieving a campaign + * @param {number} parameters.id - The ID of the campaign to be retrieved + * @param {string} [parameters.section='Products'] - The section of the index to use + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} + * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaign + * @example + * constructorio.campaigns.retrieveCampaign({ + * id: 42, + * section: 'Products', + * }); + */ + retrieveCampaign(parameters = {}, networkParameters = {}) { + const queryParams = {}; + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const headers = { + 'Content-Type': 'application/json', + }; + const { id, section } = parameters; + + // Pull section from parameters + if (section) { + queryParams.section = section; + } + + try { + requestUrl = createCampaignsUrl(`campaigns/${id}`, this.options, queryParams); + } catch (e) { + return Promise.reject(e); + } + + Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters)); + + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'GET', + headers: { + ...headers, + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }).then((json) => json); + } + + /** + * Create a campaign + * + * @function createCampaign + * @param {object} parameters - Additional parameters for the campaign to be created + * @param {string} parameters.name - The name of the campaign + * @param {string} [parameters.section='Products'] - The section of the index to use + * @param {string} [parameters.description] - The description of the campaign + * @param {string} [parameters.requestTagName] - Request tag name used to activate this campaign for, used only with `request_tag_value` + * @param {string} [parameters.requestTagValue] - Request tag value used to activate this campaign for, used only with `request_tag_name` + * @param {string} [parameters.startTime] - The start time of the campaign (ISO 8601 date-time) + * @param {string} [parameters.endTime] - The end time of the campaign (ISO 8601 date-time) + * @param {object[]} [parameters.refinedQueries] - A list of refined queries + * @param {object[]} [parameters.refinedFilters] - A list of refined filters + * @param {object[]} [parameters.refinedRecommendationContexts] - A list of refined recommendation contexts + * @param {object[]} [parameters.boostRules] - A list of boost rules + * @param {object[]} [parameters.blacklistRules] - A list of blacklist rules + * @param {object[]} [parameters.slotRules] - A list of slot rules + * @param {object[]} [parameters.contentRules] - A list of content rules + * @param {object[]} [parameters.filtersSlotRules] - A list of filters slot rules + * @param {object} [parameters.whitelistRule] - A whitelist rule + * @param {object} [parameters.variationSlicingRule] - A variation slicing rule + * @param {object} [parameters.metadataJson] - Metadata related to the campaign + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} + * @see https://docs.constructor.com/reference/v1-searchandising-create-campaign + * @example + * constructorio.campaigns.createCampaign({ + * name: 'Spring Sale', + * section: 'Products', + * description: 'Seasonal promotion campaign', + * boostRules: [ + * { rule_type: 'boost', rule: { boost: 5, filters: { brand: ['Nike'] } } }, + * ], + * }); + */ + createCampaign(parameters = {}, networkParameters = {}) { + const queryParams = {}; + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const headers = { + 'Content-Type': 'application/json', + }; + const { section, ...body } = parameters; + + // Pull section from parameters + if (section) { + queryParams.section = section; + } + + try { + requestUrl = createCampaignsUrl('campaigns', this.options, queryParams); + } catch (e) { + return Promise.reject(e); + } + + Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters)); + + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'POST', + body: JSON.stringify(helpers.toSnakeCaseKeys(body, false)), + headers: { + ...headers, + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }).then((json) => json); + } + + /** + * Update a campaign - only the fields present in the request will be updated + * + * @function updateCampaign + * @param {object} parameters - Additional parameters for the campaign to be updated + * @param {number} parameters.id - The ID of the campaign to be updated + * @param {string} [parameters.section='Products'] - The section of the index to use + * @param {string} [parameters.name] - The name of the campaign + * @param {string} [parameters.description] - The description of the campaign + * @param {string} [parameters.requestTagName] - Request tag name used to activate this campaign for, used only with `request_tag_value` + * @param {string} [parameters.requestTagValue] - Request tag value used to activate this campaign for, used only with `request_tag_name` + * @param {string} [parameters.startTime] - The start time of the campaign (ISO 8601 date-time) + * @param {string} [parameters.endTime] - The end time of the campaign (ISO 8601 date-time) + * @param {object[]} [parameters.refinedQueries] - A list of refined queries + * @param {object[]} [parameters.refinedFilters] - A list of refined filters + * @param {object[]} [parameters.refinedRecommendationContexts] - A list of refined recommendation contexts + * @param {object[]} [parameters.boostRules] - A list of boost rules + * @param {object[]} [parameters.blacklistRules] - A list of blacklist rules + * @param {object[]} [parameters.slotRules] - A list of slot rules + * @param {object[]} [parameters.contentRules] - A list of content rules + * @param {object[]} [parameters.filtersSlotRules] - A list of filters slot rules + * @param {object} [parameters.whitelistRule] - A whitelist rule + * @param {object} [parameters.variationSlicingRule] - A variation slicing rule + * @param {object} [parameters.metadataJson] - Metadata related to the campaign + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} + * @see https://docs.constructor.com/reference/v1-searchandising-update-campaign + * @example + * constructorio.campaigns.updateCampaign({ + * id: 42, + * name: 'Spring Sale - Updated', + * description: 'Updated seasonal promotion campaign', + * }); + */ + updateCampaign(parameters = {}, networkParameters = {}) { + const queryParams = {}; + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const headers = { + 'Content-Type': 'application/json', + }; + const { id, section, ...body } = parameters; + + // Pull section from parameters + if (section) { + queryParams.section = section; + } + + try { + requestUrl = createCampaignsUrl(`campaigns/${id}`, this.options, queryParams); + } catch (e) { + return Promise.reject(e); + } + + Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters)); + + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'PATCH', + body: JSON.stringify(helpers.toSnakeCaseKeys(body, false)), + headers: { + ...headers, + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return response.json(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }).then((json) => json); + } + + /** + * Delete a campaign given a specific id + * + * @function deleteCampaign + * @param {object} parameters - Additional parameters for the campaign to be deleted + * @param {number} parameters.id - The ID of the campaign to be deleted + * @param {string} [parameters.section='Products'] - The section of the index to use + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {Promise} + * @see https://docs.constructor.com/reference/v1-searchandising-delete-campaign + * @example + * constructorio.campaigns.deleteCampaign({ + * id: 42, + * section: 'Products', + * }); + */ + deleteCampaign(parameters = {}, networkParameters = {}) { + const queryParams = {}; + let requestUrl; + const { fetch } = this.options; + const controller = new AbortController(); + const { signal } = controller; + const headers = { + 'Content-Type': 'application/json', + }; + const { id, section } = parameters; + + // Pull section from parameters + if (section) { + queryParams.section = section; + } + + try { + requestUrl = createCampaignsUrl(`campaigns/${id}`, this.options, queryParams); + } catch (e) { + return Promise.reject(e); + } + + Object.assign(headers, helpers.combineCustomHeaders(this.options, networkParameters)); + + helpers.applyNetworkTimeout(this.options, networkParameters, controller); + + return fetch(requestUrl, { + method: 'DELETE', + headers: { + ...headers, + ...helpers.createAuthHeader(this.options), + }, + signal, + }).then((response) => { + if (response.ok) { + return Promise.resolve(); + } + + return helpers.throwHttpErrorFromResponse(new Error(), response); + }); + } +} + +module.exports = Campaigns; diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts new file mode 100644 index 00000000..270b698e --- /dev/null +++ b/src/types/campaigns.d.ts @@ -0,0 +1,215 @@ +import { ConstructorClientOptions, NetworkParameters } from '.'; + +export default Campaigns; + +export interface RetrieveCampaignsParameters { + section?: string; + id?: number | number[]; + refinedFilters?: Record; + numResultsPerPage?: number; + page?: number; + offset?: number; + refinedRecommendationContexts?: RefinedRecommendationContext; + refinedQueries?: string[]; +} + +export interface RefinedRecommendationContext { + pod_id?: string[]; + condition_type?: ('attribute' | 'item' | 'expression')[]; +} + +export interface RetrieveCampaignParameters { + id: number; + section?: string; +} + +export interface CreateCampaignParameters extends Record { + name: string; + section?: string; + description?: string; + requestTagName?: RequestTag; + requestTagValue?: string; + startTime?: string; + endTime?: string; + refinedQueries?: RefinedQuery[]; + refinedFilters?: RefinedFilter[]; + refinedRecommendationContexts?: RecommendationContext[]; + boostRules?: CampaignRuleInput[]; + blacklistRules?: CampaignRuleInput[]; + slotRules?: CampaignRuleInput[]; + contentRules?: CampaignRuleInput[]; + filtersSlotRules?: CampaignRuleInput[]; + whitelistRule?: CampaignRuleInput | null; + variationSlicingRule?: CampaignRuleInput | null; + metadataJson?: CampaignMetadata; +} + +export interface UpdateCampaignParameters extends Record { + id: number; + section?: string; + name?: string; + description?: string; + requestTagName?: RequestTag; + requestTagValue?: string; + startTime?: string; + endTime?: string; + refinedQueries?: RefinedQuery[]; + refinedFilters?: RefinedFilter[]; + refinedRecommendationContexts?: RecommendationContext[]; + boostRules?: CampaignRuleInput[]; + blacklistRules?: CampaignRuleInput[]; + slotRules?: CampaignRuleInput[]; + contentRules?: CampaignRuleInput[]; + filtersSlotRules?: CampaignRuleInput[]; + whitelistRule?: CampaignRuleInput | null; + variationSlicingRule?: CampaignRuleInput | null; + metadataJson?: CampaignMetadata; +} + +export interface DeleteCampaignParameters { + id: number; + section?: string; +} + +declare class Campaigns { + constructor(options: ConstructorClientOptions); + + options: ConstructorClientOptions; + + retrieveCampaigns( + parameters?: RetrieveCampaignsParameters, + networkParameters?: NetworkParameters + ): Promise; + + retrieveCampaign( + parameters: RetrieveCampaignParameters, + networkParameters?: NetworkParameters + ): Promise; + + createCampaign( + parameters: CreateCampaignParameters, + networkParameters?: NetworkParameters + ): Promise; + + updateCampaign( + parameters: UpdateCampaignParameters, + networkParameters?: NetworkParameters + ): Promise; + + deleteCampaign( + parameters: DeleteCampaignParameters, + networkParameters?: NetworkParameters + ): Promise; +} + +export type RequestTag = + | 'client_ip' + | 'client_version' + | 'geo_city' + | 'geo_country' + | 'geo_country_iso_code' + | 'geo_region' + | 'dt_weekday' + | 'dt_timeofday' + | 'user_segment' + | 'autogenerated_user_segment' + | 'dynamic_segment'; + +export interface RefinedQuery { + query: string; +} + +export interface RefinedFilter { + filter_name: string; + filter_value: string; +} + +export interface RecommendationContext extends Record { + pod_id: string; + condition: Record; +} + +export interface CampaignRule extends Record { + id: number; + rule: Record; + rule_type: + | 'boost' + | 'blacklist' + | 'slot' + | 'content' + | 'filters_slot' + | 'whitelist' + | 'variation_slicing'; + request_tag_name?: RequestTag; + request_tag_value?: string; + active?: boolean; + start_time?: string; + end_time?: string; + campaign_id?: number; + automatically_generated?: boolean; + created_at?: string; + updated_at?: string; +} + +/* rule definition supplied when creating or modifying a campaign */ +export interface CampaignRuleInput extends Record { + rule: Record; + rule_type?: + | 'boost' + | 'blacklist' + | 'slot' + | 'content' + | 'filters_slot' + | 'whitelist' + | 'variation_slicing'; + request_tag_name?: RequestTag; + request_tag_value?: string; + active?: boolean; + start_time?: string; + end_time?: string; + campaign_id?: number; + automatically_generated?: boolean; +} + +export interface CampaignMetadata { + goal?: string; + goal_description?: string; +} + +export interface Campaign extends Record { + id: number; + created_at: string; + updated_at: string; + name?: string; + description?: string; + request_tag_name?: RequestTag; + request_tag_value?: string; + start_time?: string; + end_time?: string; + refined_queries?: RefinedQuery[]; + refined_filters?: RefinedFilter[]; + refined_recommendation_contexts?: RecommendationContext[]; + boost_rules?: CampaignRule[]; + blacklist_rules?: CampaignRule[]; + slot_rules?: CampaignRule[]; + content_rules?: CampaignRule[]; + filters_slot_rules?: CampaignRule[]; + whitelist_rule?: CampaignRule; + variation_slicing_rule?: CampaignRule; + metadata_json?: CampaignMetadata; +} + +/* campaigns results returned from server */ +export interface CampaignListGetResponse { + campaigns: Campaign[]; + total_count: number; +} + +/* single campaign result returned from server */ +export type CampaignGetResponse = Campaign; + +/* campaign result returned from server after creation */ +export type CampaignPostResponse = Campaign; + +/* campaign result returned from server after update */ +export type CampaignPatchResponse = Campaign; diff --git a/src/types/constructorio.d.ts b/src/types/constructorio.d.ts index 0475d155..99912bb9 100644 --- a/src/types/constructorio.d.ts +++ b/src/types/constructorio.d.ts @@ -6,6 +6,7 @@ import Tracker from './tracker'; import Catalog from './catalog'; import Tasks from './tasks'; import Quizzes from './quizzes'; +import Campaigns from './campaigns'; import { ConstructorClientOptions } from '.'; @@ -31,4 +32,6 @@ declare class ConstructorIO { tasks: Tasks; quizzes: Quizzes; + + campaigns: Campaigns; } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 9c863457..744393d3 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -6,6 +6,7 @@ export * from './recommendations'; export * from './search'; export * from './tasks'; export * from './tracker'; +export * from './campaigns'; export interface NetworkParameters extends Record { timeout?: number; From 7a929de45499aea3817402dff1f7f535f22a8177 Mon Sep 17 00:00:00 2001 From: Sherzod Date: Fri, 5 Jun 2026 20:58:03 +0500 Subject: [PATCH 02/14] update delete campaign tests --- spec/src/modules/campaigns.js | 43 ++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/spec/src/modules/campaigns.js b/spec/src/modules/campaigns.js index f2eb9a3a..fe087bf1 100644 --- a/spec/src/modules/campaigns.js +++ b/spec/src/modules/campaigns.js @@ -44,6 +44,14 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { campaignId = res.campaigns[0].id; } }); + + // Create a campaign if none exists so the tests below have an id to work with + if (!campaignId) { + const created = await campaigns.createCampaign({ name: `test-campaign-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }); + + campaignId = created.id; + createdCampaignIds.push(created.id); + } }); after(async () => { @@ -51,11 +59,8 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { ...validOptions, }); - // Clean up campaigns created during the suite - for (const id of createdCampaignIds) { - // eslint-disable-next-line no-await-in-loop - await campaigns.deleteCampaign({ id }).catch(() => {}); - } + // Clean up campaigns created during the suite (in parallel to stay within the hook timeout) + await Promise.all(createdCampaignIds.map((id) => campaigns.deleteCampaign({ id }).catch(() => {}))); }); beforeEach(() => { @@ -446,7 +451,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { fetch: fetchSpy, }); - campaigns.createCampaign({ name: createCampaignName }).then((res) => { + campaigns.createCampaign({ name: createCampaignName, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -473,7 +478,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { fetch: fetchSpy, }); - campaigns.createCampaign({ name: `${createCampaignName}-2`, description, section }).then((res) => { + campaigns.createCampaign({ name: `${createCampaignName}-2`, description, section, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -500,9 +505,11 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { fetch: fetchSpy, }); - campaigns.createCampaign({ name: `${createCampaignName}-4` }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + campaigns.createCampaign({ name: `${createCampaignName}-4`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + createdCampaignIds.push(res.id); + expect(fetchSpy).to.have.been.called; expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); done(); @@ -520,9 +527,11 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.createCampaign({ name: `${createCampaignName}-5` }).then(() => { + campaigns.createCampaign({ name: `${createCampaignName}-5`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + createdCampaignIds.push(res.id); + expect(fetchSpy).to.have.been.called; expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test'); done(); @@ -541,9 +550,11 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.createCampaign({ name: `${createCampaignName}-6` }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + campaigns.createCampaign({ name: `${createCampaignName}-6`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); + createdCampaignIds.push(res.id); + expect(fetchSpy).to.have.been.called; expect(requestedHeaders).to.have.property('X-Constructor-IO-Test').to.equal('test2'); expect(requestedHeaders).to.have.property('X-Constructor-IO-Test-Another').to.equal('test'); @@ -560,7 +571,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { fetch: fetchSpy, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-key` })).to.eventually.be.rejected; + return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-key`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); it('Should return error when creating a campaign with an invalid API token', () => { @@ -572,14 +583,14 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { fetch: fetchSpy, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-token` })).to.eventually.be.rejected; + return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-token`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { campaigns } = new ConstructorIO(validOptions); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-timeout` }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(campaigns.createCampaign({ name: `${createCampaignName}-timeout`, refinedQueries: [{ query: 'test-query' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -588,7 +599,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { networkParameters: { timeout: 20 }, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-global-timeout` })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(campaigns.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -602,7 +613,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); // Create a campaign to update in the following tests below - await campaigns.createCampaign({ name: `test-campaign-update-${Date.now()}` }).then((res) => { + await campaigns.createCampaign({ name: `test-campaign-update-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { updateCampaignId = res.id; createdCampaignIds.push(res.id); }); @@ -766,7 +777,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { ...validOptions, }); - const res = await campaigns.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}` }); + const res = await campaigns.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}`, refinedQueries: [{ query: 'test-query' }] }); return res.id; } From c39f99cae1b40431ae70f09cc45fbbe8e8f63e4a Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 15:51:10 -0400 Subject: [PATCH 03/14] Move jsdocs --- src/modules/campaigns.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/campaigns.js b/src/modules/campaigns.js index 74f72096..82135f55 100644 --- a/src/modules/campaigns.js +++ b/src/modules/campaigns.js @@ -50,12 +50,12 @@ class Campaigns { * @param {object} [parameters] - Additional parameters for retrieving campaigns * @param {string} [parameters.section='Products'] - The section of the index to use * @param {number|number[]} [parameters.id] - The ID(s) of campaigns to filter by + * @param {string[]} [parameters.refinedQueries] - A list of refined queries to filter by * @param {object} [parameters.refinedFilters] - An object of refined filters to filter by + * @param {object} [parameters.refinedRecommendationContexts] - A filter for refined recommendation contexts * @param {number} [parameters.numResultsPerPage=20] - The number of campaigns to return - maximum value 100 * @param {number} [parameters.page] - The page of results to return * @param {number} [parameters.offset] - The number of results to skip from the beginning - cannot be used together with `page` - * @param {object} [parameters.refinedRecommendationContexts] - A filter for refined recommendation contexts - * @param {string[]} [parameters.refinedQueries] - A list of refined queries to filter by * @param {object} [networkParameters] - Parameters relevant to the network request * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) * @returns {Promise} From 24d09a341103d1fc99d410c60f373fabd882b401 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 15:54:42 -0400 Subject: [PATCH 04/14] Update RecommendationContext --- src/types/campaigns.d.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts index 270b698e..8e28f673 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/campaigns.d.ts @@ -124,9 +124,30 @@ export interface RefinedFilter { filter_value: string; } -export interface RecommendationContext extends Record { +export interface RecommendationContext { pod_id: string; - condition: Record; + condition: RecommendationContextCondition; +} + +export type RecommendationContextCondition = + | RecommendationContextItemCondition + | RecommendationContextAttributeCondition + | RecommendationContextExpressionCondition; + +export interface RecommendationContextItemCondition { + type: 'item'; + item_id?: string; +} + +export interface RecommendationContextAttributeCondition { + type: 'attribute'; + filter_name?: string; + filter_value?: string; +} + +export interface RecommendationContextExpressionCondition { + type: 'expression'; + expression: Record; } export interface CampaignRule extends Record { From 7f5bc29115dbb76962f42001d8742a6de967afa2 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:09:53 -0400 Subject: [PATCH 05/14] Update CampaignRule types --- src/types/campaigns.d.ts | 117 +++++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts index 8e28f673..f63fb5a9 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/campaigns.d.ts @@ -150,17 +150,55 @@ export interface RecommendationContextExpressionCondition { expression: Record; } -export interface CampaignRule extends Record { +export type FiltersObj = Record; + +export interface PositionRange { + start: number; + end: number; +} + +export interface BoostRuleParameters { + filters?: FiltersObj; + item_ids?: string[]; + boost: number; +} + +export interface BlacklistRuleParameters { + filters?: FiltersObj; + item_ids?: string[]; +} + +export interface SlotRuleParameters { + item_id: string; + position: number; + variation_slice?: Record; + fuzzy_match?: boolean; + labels?: Record; +} + +export interface ContentRuleParameters { + data: Record; +} + +export interface FiltersSlotRuleParameters { + position_ranges: PositionRange[]; + filters?: FiltersObj; + filter_expression?: Record | string; + labels?: Record; +} + +export interface WhitelistRuleParameters { + filters: FiltersObj; +} + +export interface VariationSlicingRuleParameters { + facet_names: string[]; + filter_expression?: Record | string; +} + +/* shared fields present on every campaign rule */ +export interface RuleObjBase { id: number; - rule: Record; - rule_type: - | 'boost' - | 'blacklist' - | 'slot' - | 'content' - | 'filters_slot' - | 'whitelist' - | 'variation_slicing'; request_tag_name?: RequestTag; request_tag_value?: string; active?: boolean; @@ -172,6 +210,51 @@ export interface CampaignRule extends Record { updated_at?: string; } +export interface BoostRule extends RuleObjBase { + rule_type: 'boost'; + rule: BoostRuleParameters; +} + +export interface BlacklistRule extends RuleObjBase { + rule_type: 'blacklist'; + rule: BlacklistRuleParameters; +} + +export interface SlotRule extends RuleObjBase { + rule_type: 'slot'; + rule: SlotRuleParameters; + request_filters?: Record; +} + +export interface ContentRule extends RuleObjBase { + rule_type: 'content'; + rule: ContentRuleParameters; +} + +export interface FiltersSlotRule extends RuleObjBase { + rule_type: 'filters_slot'; + rule: FiltersSlotRuleParameters; +} + +export interface WhitelistRule extends RuleObjBase { + rule_type: 'whitelist'; + rule: WhitelistRuleParameters; +} + +export interface VariationSlicingRule extends RuleObjBase { + rule_type: 'variation_slicing'; + rule: VariationSlicingRuleParameters; +} + +export type RuleObj = + | BoostRule + | BlacklistRule + | SlotRule + | ContentRule + | FiltersSlotRule + | WhitelistRule + | VariationSlicingRule; + /* rule definition supplied when creating or modifying a campaign */ export interface CampaignRuleInput extends Record { rule: Record; @@ -210,13 +293,13 @@ export interface Campaign extends Record { refined_queries?: RefinedQuery[]; refined_filters?: RefinedFilter[]; refined_recommendation_contexts?: RecommendationContext[]; - boost_rules?: CampaignRule[]; - blacklist_rules?: CampaignRule[]; - slot_rules?: CampaignRule[]; - content_rules?: CampaignRule[]; - filters_slot_rules?: CampaignRule[]; - whitelist_rule?: CampaignRule; - variation_slicing_rule?: CampaignRule; + boost_rules?: BoostRule[]; + blacklist_rules?: BlacklistRule[]; + slot_rules?: SlotRule[]; + content_rules?: ContentRule[]; + filters_slot_rules?: FiltersSlotRule[]; + whitelist_rule?: WhitelistRule; + variation_slicing_rule?: VariationSlicingRule; metadata_json?: CampaignMetadata; } From b3d100511285432411fbbf51f6b88d159958cb04 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:21:17 -0400 Subject: [PATCH 06/14] Update CampaignRuleInput --- src/types/campaigns.d.ts | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts index f63fb5a9..5ba44461 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/campaigns.d.ts @@ -255,25 +255,19 @@ export type RuleObj = | WhitelistRule | VariationSlicingRule; -/* rule definition supplied when creating or modifying a campaign */ -export interface CampaignRuleInput extends Record { - rule: Record; - rule_type?: - | 'boost' - | 'blacklist' - | 'slot' - | 'content' - | 'filters_slot' - | 'whitelist' - | 'variation_slicing'; - request_tag_name?: RequestTag; - request_tag_value?: string; - active?: boolean; - start_time?: string; - end_time?: string; - campaign_id?: number; - automatically_generated?: boolean; -} +/* + * Omit the server-assigned fields from each rule. + * `Omit` does not distribute over unions - applied directly to RuleObj it would merge + * all members into one object and decouple rule_type from rule. The `T extends unknown` + * conditional forces distribution so each member is omitted individually and the + * rule_type/rule discrimination is preserved. + */ +type OmitServerFields = T extends unknown + ? Omit + : never; + +/* rule definition supplied when creating or modifying a campaign - server-assigned fields are omitted */ +export type CampaignRuleInput = OmitServerFields; export interface CampaignMetadata { goal?: string; From 72f7c4580e5133ac2baad797ffb72219dcc591e2 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:23:11 -0400 Subject: [PATCH 07/14] Drop record --- src/types/campaigns.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts index 5ba44461..26500eb7 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/campaigns.d.ts @@ -23,7 +23,7 @@ export interface RetrieveCampaignParameters { section?: string; } -export interface CreateCampaignParameters extends Record { +export interface CreateCampaignParameters { name: string; section?: string; description?: string; @@ -44,7 +44,7 @@ export interface CreateCampaignParameters extends Record { metadataJson?: CampaignMetadata; } -export interface UpdateCampaignParameters extends Record { +export interface UpdateCampaignParameters { id: number; section?: string; name?: string; From edc257e5b700e57ca97c6084a5400cf560c443b0 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:25:20 -0400 Subject: [PATCH 08/14] Update CampaignParametersBase --- src/types/campaigns.d.ts | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/types/campaigns.d.ts b/src/types/campaigns.d.ts index 26500eb7..17236c82 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/campaigns.d.ts @@ -23,9 +23,10 @@ export interface RetrieveCampaignParameters { section?: string; } -export interface CreateCampaignParameters { - name: string; +/* fields shared by create and update campaign requests */ +export interface CampaignParametersBase { section?: string; + name?: string; description?: string; requestTagName?: RequestTag; requestTagValue?: string; @@ -44,26 +45,12 @@ export interface CreateCampaignParameters { metadataJson?: CampaignMetadata; } -export interface UpdateCampaignParameters { +export interface CreateCampaignParameters extends CampaignParametersBase { + name: string; +} + +export interface UpdateCampaignParameters extends CampaignParametersBase { id: number; - section?: string; - name?: string; - description?: string; - requestTagName?: RequestTag; - requestTagValue?: string; - startTime?: string; - endTime?: string; - refinedQueries?: RefinedQuery[]; - refinedFilters?: RefinedFilter[]; - refinedRecommendationContexts?: RecommendationContext[]; - boostRules?: CampaignRuleInput[]; - blacklistRules?: CampaignRuleInput[]; - slotRules?: CampaignRuleInput[]; - contentRules?: CampaignRuleInput[]; - filtersSlotRules?: CampaignRuleInput[]; - whitelistRule?: CampaignRuleInput | null; - variationSlicingRule?: CampaignRuleInput | null; - metadataJson?: CampaignMetadata; } export interface DeleteCampaignParameters { From cf831d88f7d7a3dcc9319bc5331aa481c3302728 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:26:31 -0400 Subject: [PATCH 09/14] Update test --- spec/src/modules/campaigns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src/modules/campaigns.js b/spec/src/modules/campaigns.js index fe087bf1..80a12ae3 100644 --- a/spec/src/modules/campaigns.js +++ b/spec/src/modules/campaigns.js @@ -99,7 +99,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should retrieve a list of campaigns given section', (done) => { - const section = 'Products'; + const section = 'Search Suggestions'; const { campaigns } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, From 30f45183ca176e7730a7cb6aaad899baf6d68e93 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:30:22 -0400 Subject: [PATCH 10/14] Update type tests --- package-lock.json | 33 +++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 34 insertions(+) diff --git a/package-lock.json b/package-lock.json index 40ee4fb3..7dd00feb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "devDependencies": { "@cspell/eslint-plugin": "^6.8.2", "@types/events": "^3.0.0", + "@types/node": "^22.19.21", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.4", @@ -1408,6 +1409,16 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "node_modules/@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -7074,6 +7085,13 @@ "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==", "dev": true }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -8541,6 +8559,15 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -12717,6 +12744,12 @@ "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==", "dev": true }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", diff --git a/package.json b/package.json index 8dea0550..196c5587 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@cspell/eslint-plugin": "^6.8.2", "@types/events": "^3.0.0", + "@types/node": "^22.19.21", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", "chai": "^4.3.4", From 75c75dbd07dc512601ac74e683d4cb504cb07f3a Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 16 Jun 2026 16:51:08 -0400 Subject: [PATCH 11/14] Rename module --- cspell.json | 1 + .../{campaigns.js => searchandising.js} | 208 +++++++++--------- src/constructorio.js | 6 +- .../{campaigns.js => searchandising.js} | 18 +- src/types/constructorio.d.ts | 4 +- src/types/index.d.ts | 2 +- .../{campaigns.d.ts => searchandising.d.ts} | 4 +- 7 files changed, 122 insertions(+), 121 deletions(-) rename spec/src/modules/{campaigns.js => searchandising.js} (72%) rename src/modules/{campaigns.js => searchandising.js} (97%) rename src/types/{campaigns.d.ts => searchandising.d.ts} (99%) diff --git a/cspell.json b/cspell.json index 0c3caa7b..1ec8fb2d 100644 --- a/cspell.json +++ b/cspell.json @@ -39,6 +39,7 @@ "yoink", "searchandized", "searchandizing", + "searchandising", "patchdelta", "searchabilities", "forcesync", diff --git a/spec/src/modules/campaigns.js b/spec/src/modules/searchandising.js similarity index 72% rename from spec/src/modules/campaigns.js rename to spec/src/modules/searchandising.js index 80a12ae3..add4cf0b 100644 --- a/spec/src/modules/campaigns.js +++ b/spec/src/modules/searchandising.js @@ -23,7 +23,7 @@ const validOptions = { }; const skipNetworkTimeoutTests = process.env.SKIP_NETWORK_TIMEOUT_TESTS === 'true'; -describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { +describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising() { // Ensure Mocha doesn't time out waiting for operation to complete this.timeout(10000); @@ -34,12 +34,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { let campaignId; before(async () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, }); // Save id of an existing campaign for the following tests below - await campaigns.retrieveCampaigns({ numResultsPerPage: 1 }).then((res) => { + await searchandising.retrieveCampaigns({ numResultsPerPage: 1 }).then((res) => { if (res.campaigns && res.campaigns.length) { campaignId = res.campaigns[0].id; } @@ -47,7 +47,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { // Create a campaign if none exists so the tests below have an id to work with if (!campaignId) { - const created = await campaigns.createCampaign({ name: `test-campaign-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }); + const created = await searchandising.createCampaign({ name: `test-campaign-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }); campaignId = created.id; createdCampaignIds.push(created.id); @@ -55,12 +55,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); after(async () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, }); // Clean up campaigns created during the suite (in parallel to stay within the hook timeout) - await Promise.all(createdCampaignIds.map((id) => campaigns.deleteCampaign({ id }).catch(() => {}))); + await Promise.all(createdCampaignIds.map((id) => searchandising.deleteCampaign({ id }).catch(() => {}))); }); beforeEach(() => { @@ -78,12 +78,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { describe('retrieveCampaigns', () => { it('Should retrieve a list of campaigns', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns().then((res) => { + searchandising.retrieveCampaigns().then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -100,12 +100,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should retrieve a list of campaigns given section', (done) => { const section = 'Search Suggestions'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns({ section }).then((res) => { + searchandising.retrieveCampaigns({ section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -121,12 +121,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should retrieve a list of campaigns given page and results per page', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns({ page: 1, numResultsPerPage: 50 }).then((res) => { + searchandising.retrieveCampaigns({ page: 1, numResultsPerPage: 50 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -144,12 +144,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should retrieve a list of campaigns given page and results per page when passing parameters snake_case', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns({ page: 1, num_results_per_page: 50 }).then((res) => { + searchandising.retrieveCampaigns({ page: 1, num_results_per_page: 50 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -166,12 +166,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should retrieve a list of campaigns given offset', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns({ offset: 1 }).then((res) => { + searchandising.retrieveCampaigns({ offset: 1 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -187,12 +187,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in function networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + searchandising.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -202,7 +202,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -212,7 +212,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.retrieveCampaigns().then(() => { + searchandising.retrieveCampaigns().then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -222,7 +222,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -233,7 +233,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + searchandising.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -247,52 +247,52 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiKey = 'notanapikey'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejected; + return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejected; }); it('Should return error when retrieving a list of campaigns with an invalid API token', () => { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiToken = 'notanapitoken'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejected; + return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO(validOptions); + const { searchandising } = new ConstructorIO(validOptions); - return expect(campaigns.retrieveCampaigns({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.retrieveCampaigns({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, networkParameters: { timeout: 20 }, }); - return expect(campaigns.retrieveCampaigns()).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); describe('retrieveCampaign', () => { it('Should retrieve a campaign given a specific id', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaign({ id: campaignId }).then((res) => { + searchandising.retrieveCampaign({ id: campaignId }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -308,12 +308,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should retrieve a campaign given a specific id and section', (done) => { const section = 'Products'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaign({ id: campaignId, section }).then((res) => { + searchandising.retrieveCampaign({ id: campaignId, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -329,12 +329,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in function networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { + searchandising.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -349,7 +349,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -359,7 +359,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.retrieveCampaign({ id: campaignId }).then((res) => { + searchandising.retrieveCampaign({ id: campaignId }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -374,7 +374,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -385,7 +385,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { + searchandising.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -404,40 +404,40 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiKey = 'notanapikey'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; }); it('Should return error when retrieving a campaign given a specific id with an invalid API token', () => { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiToken = 'notanapitoken'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO(validOptions); + const { searchandising } = new ConstructorIO(validOptions); - return expect(campaigns.retrieveCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.retrieveCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, networkParameters: { timeout: 20 }, }); - return expect(campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -446,12 +446,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const createCampaignName = `test-campaign-${Date.now()}`; it('Should create a campaign given a name', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.createCampaign({ name: createCampaignName, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.createCampaign({ name: createCampaignName, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -473,12 +473,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should create a campaign given a name, description and section', (done) => { const section = 'Products'; const description = 'Seasonal promotion campaign'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.createCampaign({ name: `${createCampaignName}-2`, description, section, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.createCampaign({ name: `${createCampaignName}-2`, description, section, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -500,12 +500,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in function networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.createCampaign({ name: `${createCampaignName}-4`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { + searchandising.createCampaign({ name: `${createCampaignName}-4`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -517,7 +517,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -527,7 +527,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.createCampaign({ name: `${createCampaignName}-5`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.createCampaign({ name: `${createCampaignName}-5`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -539,7 +539,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -550,7 +550,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.createCampaign({ name: `${createCampaignName}-6`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { + searchandising.createCampaign({ name: `${createCampaignName}-6`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -566,40 +566,40 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiKey = 'notanapikey'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-key`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; + return expect(searchandising.createCampaign({ name: `${createCampaignName}-invalid-key`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); it('Should return error when creating a campaign with an invalid API token', () => { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiToken = 'notanapitoken'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-invalid-token`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; + return expect(searchandising.createCampaign({ name: `${createCampaignName}-invalid-token`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO(validOptions); + const { searchandising } = new ConstructorIO(validOptions); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-timeout`, refinedQueries: [{ query: 'test-query' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.createCampaign({ name: `${createCampaignName}-timeout`, refinedQueries: [{ query: 'test-query' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, networkParameters: { timeout: 20 }, }); - return expect(campaigns.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -608,12 +608,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { let updateCampaignId; before(async () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, }); // Create a campaign to update in the following tests below - await campaigns.createCampaign({ name: `test-campaign-update-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + await searchandising.createCampaign({ name: `test-campaign-update-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { updateCampaignId = res.id; createdCampaignIds.push(res.id); }); @@ -621,12 +621,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should update a campaign given an id and name', (done) => { const updatedName = `updated-campaign-${Date.now()}`; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.updateCampaign({ id: updateCampaignId, name: updatedName }).then((res) => { + searchandising.updateCampaign({ id: updateCampaignId, name: updatedName }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -647,12 +647,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should update a campaign given a description and section', (done) => { const section = 'Products'; const description = 'Updated seasonal promotion campaign'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.updateCampaign({ id: updateCampaignId, description, section }).then((res) => { + searchandising.updateCampaign({ id: updateCampaignId, description, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -672,12 +672,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in function networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + searchandising.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -687,7 +687,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should pass the correct custom headers passed in global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -697,7 +697,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.updateCampaign({ id: updateCampaignId }).then(() => { + searchandising.updateCampaign({ id: updateCampaignId }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -707,7 +707,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }); it('Should combine custom headers from function networkParameters and global networkParameters', (done) => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -718,7 +718,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + searchandising.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -732,40 +732,40 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiKey = 'notanapikey'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; }); it('Should return error when updating a campaign with an invalid API token', () => { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiToken = 'notanapitoken'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO(validOptions); + const { searchandising } = new ConstructorIO(validOptions); - return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, networkParameters: { timeout: 20 }, }); - return expect(campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -773,23 +773,23 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { describe('deleteCampaign', () => { // Create a fresh campaign before each test so it can be safely deleted async function createCampaignToDelete() { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, }); - const res = await campaigns.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}`, refinedQueries: [{ query: 'test-query' }] }); + const res = await searchandising.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}`, refinedQueries: [{ query: 'test-query' }] }); return res.id; } it('Should delete a campaign given an id', async () => { const idToDelete = await createCampaignToDelete(); - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - await campaigns.deleteCampaign({ id: idToDelete }).then((res) => { + await searchandising.deleteCampaign({ id: idToDelete }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -804,12 +804,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should delete a campaign given an id and section', async () => { const section = 'Products'; const idToDelete = await createCampaignToDelete(); - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - await campaigns.deleteCampaign({ id: idToDelete, section }).then((res) => { + await searchandising.deleteCampaign({ id: idToDelete, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -824,12 +824,12 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should pass the correct custom headers passed in function networkParameters', async () => { const idToDelete = await createCampaignToDelete(); - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, }); - await campaigns.deleteCampaign({ id: idToDelete }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + await searchandising.deleteCampaign({ id: idToDelete }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -839,7 +839,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { it('Should pass the correct custom headers passed in global networkParameters', async () => { const idToDelete = await createCampaignToDelete(); - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, fetch: fetchSpy, networkParameters: { @@ -849,7 +849,7 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { }, }); - await campaigns.deleteCampaign({ id: idToDelete }).then(() => { + await searchandising.deleteCampaign({ id: idToDelete }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -861,40 +861,40 @@ describe('ConstructorIO - Campaigns', function ConstructorIOCampaigns() { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiKey = 'notanapikey'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; }); it('Should return error when deleting a campaign with an invalid API token', () => { const invalidOptions = cloneDeep(validOptions); invalidOptions.apiToken = 'notanapitoken'; - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...invalidOptions, fetch: fetchSpy, }); - return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO(validOptions); + const { searchandising } = new ConstructorIO(validOptions); - return expect(campaigns.deleteCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.deleteCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { - const { campaigns } = new ConstructorIO({ + const { searchandising } = new ConstructorIO({ ...validOptions, networkParameters: { timeout: 20 }, }); - return expect(campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); diff --git a/src/constructorio.js b/src/constructorio.js index 23b841de..a8864197 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -8,7 +8,7 @@ const Autocomplete = require('./modules/autocomplete'); const Recommendations = require('./modules/recommendations'); const Tasks = require('./modules/tasks'); const Quizzes = require('./modules/quizzes'); -const Campaigns = require('./modules/campaigns'); +const Searchandising = require('./modules/searchandising'); const { version: packageVersion } = require('../package.json'); const utils = require('./utils/helpers'); @@ -43,7 +43,7 @@ class ConstructorIO { * @property {object} catalog - Interface to {@link module:catalog} * @property {object} tasks - Interface to {@link module:tasks} * @property {object} quizzes - Interface to {@link module:quizzes} - * @property {object} campaigns - Interface to {@link module:campaigns} + * @property {object} searchandising - Interface to {@link module:searchandising} * @returns {class} */ constructor(options = {}) { @@ -85,7 +85,7 @@ class ConstructorIO { this.recommendations = new Recommendations(this.options); this.tasks = new Tasks(this.options); this.quizzes = new Quizzes(this.options); - this.campaigns = new Campaigns(this.options); + this.searchandising = new Searchandising(this.options); } } diff --git a/src/modules/campaigns.js b/src/modules/searchandising.js similarity index 97% rename from src/modules/campaigns.js rename to src/modules/searchandising.js index 82135f55..d90c6cbd 100644 --- a/src/modules/campaigns.js +++ b/src/modules/searchandising.js @@ -32,13 +32,13 @@ function createCampaignsUrl(path, options, additionalQueryParams = {}, apiVersio } /** - * Interface to searchandizing campaign related API calls + * Interface to searchandising campaign related API calls * - * @module campaigns + * @module searchandising * @inner * @returns {object} */ -class Campaigns { +class Searchandising { constructor(options) { this.options = options || {}; } @@ -61,7 +61,7 @@ class Campaigns { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaigns * @example - * constructorio.campaigns.retrieveCampaigns({ + * constructorio.searchandising.retrieveCampaigns({ * section: 'Products', * numResultsPerPage: 50, * page: 1, @@ -172,7 +172,7 @@ class Campaigns { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaign * @example - * constructorio.campaigns.retrieveCampaign({ + * constructorio.searchandising.retrieveCampaign({ * id: 42, * section: 'Products', * }); @@ -247,7 +247,7 @@ class Campaigns { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-create-campaign * @example - * constructorio.campaigns.createCampaign({ + * constructorio.searchandising.createCampaign({ * name: 'Spring Sale', * section: 'Products', * description: 'Seasonal promotion campaign', @@ -328,7 +328,7 @@ class Campaigns { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-update-campaign * @example - * constructorio.campaigns.updateCampaign({ + * constructorio.searchandising.updateCampaign({ * id: 42, * name: 'Spring Sale - Updated', * description: 'Updated seasonal promotion campaign', @@ -389,7 +389,7 @@ class Campaigns { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-delete-campaign * @example - * constructorio.campaigns.deleteCampaign({ + * constructorio.searchandising.deleteCampaign({ * id: 42, * section: 'Products', * }); @@ -437,4 +437,4 @@ class Campaigns { } } -module.exports = Campaigns; +module.exports = Searchandising; diff --git a/src/types/constructorio.d.ts b/src/types/constructorio.d.ts index 99912bb9..8ee95b31 100644 --- a/src/types/constructorio.d.ts +++ b/src/types/constructorio.d.ts @@ -6,7 +6,7 @@ import Tracker from './tracker'; import Catalog from './catalog'; import Tasks from './tasks'; import Quizzes from './quizzes'; -import Campaigns from './campaigns'; +import Searchandising from './searchandising'; import { ConstructorClientOptions } from '.'; @@ -33,5 +33,5 @@ declare class ConstructorIO { quizzes: Quizzes; - campaigns: Campaigns; + searchandising: Searchandising; } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 744393d3..c760ac93 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -6,7 +6,7 @@ export * from './recommendations'; export * from './search'; export * from './tasks'; export * from './tracker'; -export * from './campaigns'; +export * from './searchandising'; export interface NetworkParameters extends Record { timeout?: number; diff --git a/src/types/campaigns.d.ts b/src/types/searchandising.d.ts similarity index 99% rename from src/types/campaigns.d.ts rename to src/types/searchandising.d.ts index 17236c82..1a2bafb5 100644 --- a/src/types/campaigns.d.ts +++ b/src/types/searchandising.d.ts @@ -1,6 +1,6 @@ import { ConstructorClientOptions, NetworkParameters } from '.'; -export default Campaigns; +export default Searchandising; export interface RetrieveCampaignsParameters { section?: string; @@ -58,7 +58,7 @@ export interface DeleteCampaignParameters { section?: string; } -declare class Campaigns { +declare class Searchandising { constructor(options: ConstructorClientOptions); options: ConstructorClientOptions; From 20379dd69f26b9626c8e9794a1aa02939a37da70 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 24 Jun 2026 10:27:11 -0400 Subject: [PATCH 12/14] Refactor searchandising into campaigns sub-module Split the flat searchandising module into a parent class that exposes a campaigns sub-module, so each searchandising entity lives in its own class and file. Consumers now call searchandising.campaigns.retrieveCampaigns(). Also drops the repetitive "Pull X from parameters" comments. --- spec/src/modules/searchandising.js | 104 +++++++++--------- .../campaigns.js} | 30 ++--- src/modules/searchandising/index.js | 18 +++ src/types/searchandising.d.ts | 10 +- 4 files changed, 88 insertions(+), 74 deletions(-) rename src/modules/{searchandising.js => searchandising/campaigns.js} (94%) create mode 100644 src/modules/searchandising/index.js diff --git a/spec/src/modules/searchandising.js b/spec/src/modules/searchandising.js index add4cf0b..56460eea 100644 --- a/spec/src/modules/searchandising.js +++ b/spec/src/modules/searchandising.js @@ -39,7 +39,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }); // Save id of an existing campaign for the following tests below - await searchandising.retrieveCampaigns({ numResultsPerPage: 1 }).then((res) => { + await searchandising.campaigns.retrieveCampaigns({ numResultsPerPage: 1 }).then((res) => { if (res.campaigns && res.campaigns.length) { campaignId = res.campaigns[0].id; } @@ -47,7 +47,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( // Create a campaign if none exists so the tests below have an id to work with if (!campaignId) { - const created = await searchandising.createCampaign({ name: `test-campaign-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }); + const created = await searchandising.campaigns.createCampaign({ name: `test-campaign-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }); campaignId = created.id; createdCampaignIds.push(created.id); @@ -60,7 +60,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }); // Clean up campaigns created during the suite (in parallel to stay within the hook timeout) - await Promise.all(createdCampaignIds.map((id) => searchandising.deleteCampaign({ id }).catch(() => {}))); + await Promise.all(createdCampaignIds.map((id) => searchandising.campaigns.deleteCampaign({ id }).catch(() => {}))); }); beforeEach(() => { @@ -83,7 +83,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns().then((res) => { + searchandising.campaigns.retrieveCampaigns().then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -105,7 +105,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns({ section }).then((res) => { + searchandising.campaigns.retrieveCampaigns({ section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -126,7 +126,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns({ page: 1, numResultsPerPage: 50 }).then((res) => { + searchandising.campaigns.retrieveCampaigns({ page: 1, numResultsPerPage: 50 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -149,7 +149,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns({ page: 1, num_results_per_page: 50 }).then((res) => { + searchandising.campaigns.retrieveCampaigns({ page: 1, num_results_per_page: 50 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -171,7 +171,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns({ offset: 1 }).then((res) => { + searchandising.campaigns.retrieveCampaigns({ offset: 1 }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -192,7 +192,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + searchandising.campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -212,7 +212,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.retrieveCampaigns().then(() => { + searchandising.campaigns.retrieveCampaigns().then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -233,7 +233,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + searchandising.campaigns.retrieveCampaigns({}, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -252,7 +252,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejected; + return expect(searchandising.campaigns.retrieveCampaigns()).to.eventually.be.rejected; }); it('Should return error when retrieving a list of campaigns with an invalid API token', () => { @@ -264,14 +264,14 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejected; + return expect(searchandising.campaigns.retrieveCampaigns()).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { searchandising } = new ConstructorIO(validOptions); - return expect(searchandising.retrieveCampaigns({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.retrieveCampaigns({}, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -280,7 +280,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( networkParameters: { timeout: 20 }, }); - return expect(searchandising.retrieveCampaigns()).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.retrieveCampaigns()).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -292,7 +292,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaign({ id: campaignId }).then((res) => { + searchandising.campaigns.retrieveCampaign({ id: campaignId }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -313,7 +313,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaign({ id: campaignId, section }).then((res) => { + searchandising.campaigns.retrieveCampaign({ id: campaignId, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -334,7 +334,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { + searchandising.campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -359,7 +359,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.retrieveCampaign({ id: campaignId }).then((res) => { + searchandising.campaigns.retrieveCampaign({ id: campaignId }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -385,7 +385,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { + searchandising.campaigns.retrieveCampaign({ id: campaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); // Request @@ -409,7 +409,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; }); it('Should return error when retrieving a campaign given a specific id with an invalid API token', () => { @@ -421,14 +421,14 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { searchandising } = new ConstructorIO(validOptions); - return expect(searchandising.retrieveCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.retrieveCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -437,7 +437,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( networkParameters: { timeout: 20 }, }); - return expect(searchandising.retrieveCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.retrieveCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -451,7 +451,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.createCampaign({ name: createCampaignName, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.campaigns.createCampaign({ name: createCampaignName, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -478,7 +478,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.createCampaign({ name: `${createCampaignName}-2`, description, section, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.campaigns.createCampaign({ name: `${createCampaignName}-2`, description, section, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -505,7 +505,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.createCampaign({ name: `${createCampaignName}-4`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { + searchandising.campaigns.createCampaign({ name: `${createCampaignName}-4`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -527,7 +527,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.createCampaign({ name: `${createCampaignName}-5`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + searchandising.campaigns.createCampaign({ name: `${createCampaignName}-5`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -550,7 +550,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.createCampaign({ name: `${createCampaignName}-6`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { + searchandising.campaigns.createCampaign({ name: `${createCampaignName}-6`, refinedQueries: [{ query: 'test-query' }] }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then((res) => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); createdCampaignIds.push(res.id); @@ -571,7 +571,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.createCampaign({ name: `${createCampaignName}-invalid-key`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; + return expect(searchandising.campaigns.createCampaign({ name: `${createCampaignName}-invalid-key`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); it('Should return error when creating a campaign with an invalid API token', () => { @@ -583,14 +583,14 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.createCampaign({ name: `${createCampaignName}-invalid-token`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; + return expect(searchandising.campaigns.createCampaign({ name: `${createCampaignName}-invalid-token`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { searchandising } = new ConstructorIO(validOptions); - return expect(searchandising.createCampaign({ name: `${createCampaignName}-timeout`, refinedQueries: [{ query: 'test-query' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.createCampaign({ name: `${createCampaignName}-timeout`, refinedQueries: [{ query: 'test-query' }] }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -599,7 +599,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( networkParameters: { timeout: 20 }, }); - return expect(searchandising.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -613,7 +613,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }); // Create a campaign to update in the following tests below - await searchandising.createCampaign({ name: `test-campaign-update-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { + await searchandising.campaigns.createCampaign({ name: `test-campaign-update-${Date.now()}`, refinedQueries: [{ query: 'test-query' }] }).then((res) => { updateCampaignId = res.id; createdCampaignIds.push(res.id); }); @@ -626,7 +626,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.updateCampaign({ id: updateCampaignId, name: updatedName }).then((res) => { + searchandising.campaigns.updateCampaign({ id: updateCampaignId, name: updatedName }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -652,7 +652,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.updateCampaign({ id: updateCampaignId, description, section }).then((res) => { + searchandising.campaigns.updateCampaign({ id: updateCampaignId, description, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); const requestedBody = helpers.extractBodyParamsFromFetch(fetchSpy); @@ -677,7 +677,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - searchandising.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + searchandising.campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -697,7 +697,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.updateCampaign({ id: updateCampaignId }).then(() => { + searchandising.campaigns.updateCampaign({ id: updateCampaignId }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -718,7 +718,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - searchandising.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { + searchandising.campaigns.updateCampaign({ id: updateCampaignId }, { headers: { 'X-Constructor-IO-Test': 'test2' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -737,7 +737,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + return expect(searchandising.campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; }); it('Should return error when updating a campaign with an invalid API token', () => { @@ -749,14 +749,14 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; + return expect(searchandising.campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { searchandising } = new ConstructorIO(validOptions); - return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -765,7 +765,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( networkParameters: { timeout: 20 }, }); - return expect(searchandising.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.updateCampaign({ id: updateCampaignId, name: 'updated' })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); @@ -777,7 +777,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( ...validOptions, }); - const res = await searchandising.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}`, refinedQueries: [{ query: 'test-query' }] }); + const res = await searchandising.campaigns.createCampaign({ name: `test-campaign-delete-${Date.now()}-${Math.random()}`, refinedQueries: [{ query: 'test-query' }] }); return res.id; } @@ -789,7 +789,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - await searchandising.deleteCampaign({ id: idToDelete }).then((res) => { + await searchandising.campaigns.deleteCampaign({ id: idToDelete }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -809,7 +809,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - await searchandising.deleteCampaign({ id: idToDelete, section }).then((res) => { + await searchandising.campaigns.deleteCampaign({ id: idToDelete, section }).then((res) => { const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); // Request @@ -829,7 +829,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - await searchandising.deleteCampaign({ id: idToDelete }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { + await searchandising.campaigns.deleteCampaign({ id: idToDelete }, { headers: { 'X-Constructor-IO-Test': 'test' } }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -849,7 +849,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }, }); - await searchandising.deleteCampaign({ id: idToDelete }).then(() => { + await searchandising.campaigns.deleteCampaign({ id: idToDelete }).then(() => { const requestedHeaders = helpers.extractHeadersFromFetch(fetchSpy); expect(fetchSpy).to.have.been.called; @@ -866,7 +866,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; }); it('Should return error when deleting a campaign with an invalid API token', () => { @@ -878,14 +878,14 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( fetch: fetchSpy, }); - return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; + return expect(searchandising.campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejected; }); if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', () => { const { searchandising } = new ConstructorIO(validOptions); - return expect(searchandising.deleteCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.deleteCampaign({ id: campaignId }, { timeout: 10 })).to.eventually.be.rejectedWith('The operation was aborted.'); }); it('Should be rejected when global network request timeout is provided and reached', () => { @@ -894,7 +894,7 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( networkParameters: { timeout: 20 }, }); - return expect(searchandising.deleteCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); + return expect(searchandising.campaigns.deleteCampaign({ id: campaignId })).to.eventually.be.rejectedWith('The operation was aborted.'); }); } }); diff --git a/src/modules/searchandising.js b/src/modules/searchandising/campaigns.js similarity index 94% rename from src/modules/searchandising.js rename to src/modules/searchandising/campaigns.js index d90c6cbd..b4d3c7e9 100644 --- a/src/modules/searchandising.js +++ b/src/modules/searchandising/campaigns.js @@ -2,7 +2,7 @@ /* eslint-disable camelcase, no-underscore-dangle, no-unneeded-ternary, brace-style */ const qs = require('qs'); const { AbortController } = require('node-abort-controller'); -const helpers = require('../utils/helpers'); +const helpers = require('../../utils/helpers'); // Create URL from supplied path and options function createCampaignsUrl(path, options, additionalQueryParams = {}, apiVersion = 'v1') { @@ -34,11 +34,11 @@ function createCampaignsUrl(path, options, additionalQueryParams = {}, apiVersio /** * Interface to searchandising campaign related API calls * - * @module searchandising + * @module searchandising/campaigns * @inner * @returns {object} */ -class Searchandising { +class Campaigns { constructor(options) { this.options = options || {}; } @@ -61,7 +61,7 @@ class Searchandising { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaigns * @example - * constructorio.searchandising.retrieveCampaigns({ + * constructorio.searchandising.campaigns.retrieveCampaigns({ * section: 'Products', * numResultsPerPage: 50, * page: 1, @@ -93,42 +93,34 @@ class Searchandising { refinedQueries = refined_queries, } = parameters; - // Pull section from parameters if (section) { queryParams.section = section; } - // Pull id from parameters if (id) { queryParams.id = id; } - // Pull refined filters from parameters if (refinedFilters) { queryParams.refined_filters = refinedFilters; } - // Pull number of results per page from parameters if (numResultsPerPage) { queryParams.num_results_per_page = numResultsPerPage; } - // Pull page from parameters if (page) { queryParams.page = page; } - // Pull offset from parameters if (offset) { queryParams.offset = offset; } - // Pull refined recommendation contexts from parameters if (refinedRecommendationContexts) { queryParams.refined_recommendation_contexts = refinedRecommendationContexts; } - // Pull refined queries from parameters if (refinedQueries) { queryParams.refined_queries = refinedQueries; } @@ -172,7 +164,7 @@ class Searchandising { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-retrieve-campaign * @example - * constructorio.searchandising.retrieveCampaign({ + * constructorio.searchandising.campaigns.retrieveCampaign({ * id: 42, * section: 'Products', * }); @@ -188,7 +180,6 @@ class Searchandising { }; const { id, section } = parameters; - // Pull section from parameters if (section) { queryParams.section = section; } @@ -247,7 +238,7 @@ class Searchandising { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-create-campaign * @example - * constructorio.searchandising.createCampaign({ + * constructorio.searchandising.campaigns.createCampaign({ * name: 'Spring Sale', * section: 'Products', * description: 'Seasonal promotion campaign', @@ -267,7 +258,6 @@ class Searchandising { }; const { section, ...body } = parameters; - // Pull section from parameters if (section) { queryParams.section = section; } @@ -328,7 +318,7 @@ class Searchandising { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-update-campaign * @example - * constructorio.searchandising.updateCampaign({ + * constructorio.searchandising.campaigns.updateCampaign({ * id: 42, * name: 'Spring Sale - Updated', * description: 'Updated seasonal promotion campaign', @@ -345,7 +335,6 @@ class Searchandising { }; const { id, section, ...body } = parameters; - // Pull section from parameters if (section) { queryParams.section = section; } @@ -389,7 +378,7 @@ class Searchandising { * @returns {Promise} * @see https://docs.constructor.com/reference/v1-searchandising-delete-campaign * @example - * constructorio.searchandising.deleteCampaign({ + * constructorio.searchandising.campaigns.deleteCampaign({ * id: 42, * section: 'Products', * }); @@ -405,7 +394,6 @@ class Searchandising { }; const { id, section } = parameters; - // Pull section from parameters if (section) { queryParams.section = section; } @@ -437,4 +425,4 @@ class Searchandising { } } -module.exports = Searchandising; +module.exports = Campaigns; diff --git a/src/modules/searchandising/index.js b/src/modules/searchandising/index.js new file mode 100644 index 00000000..d571640f --- /dev/null +++ b/src/modules/searchandising/index.js @@ -0,0 +1,18 @@ +/* eslint-disable max-len */ +const Campaigns = require('./campaigns'); + +/** + * Interface to searchandising related API calls + * + * @module searchandising + * @inner + * @returns {object} + */ +class Searchandising { + constructor(options) { + this.options = options || {}; + this.campaigns = new Campaigns(this.options); + } +} + +module.exports = Searchandising; diff --git a/src/types/searchandising.d.ts b/src/types/searchandising.d.ts index 1a2bafb5..bda3b987 100644 --- a/src/types/searchandising.d.ts +++ b/src/types/searchandising.d.ts @@ -58,7 +58,7 @@ export interface DeleteCampaignParameters { section?: string; } -declare class Searchandising { +declare class Campaigns { constructor(options: ConstructorClientOptions); options: ConstructorClientOptions; @@ -89,6 +89,14 @@ declare class Searchandising { ): Promise; } +declare class Searchandising { + constructor(options: ConstructorClientOptions); + + options: ConstructorClientOptions; + + campaigns: Campaigns; +} + export type RequestTag = | 'client_ip' | 'client_version' From b481ff9320fedf47125669cbc8763c4608c1e0fb Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 24 Jun 2026 10:36:58 -0400 Subject: [PATCH 13/14] Add retrieveCampaigns query param tests and type tests Cover query params extracted from parameters in retrieveCampaigns (id, refinedFilters, refinedRecommendationContexts, refinedQueries, plus their snake_case aliases) and add the missing searchandising type tests. --- spec/src/modules/searchandising.js | 175 +++++++++++++++++++++++ src/types/tests/searchandising.test-d.ts | 60 ++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/types/tests/searchandising.test-d.ts diff --git a/spec/src/modules/searchandising.js b/spec/src/modules/searchandising.js index 56460eea..dc15ed9c 100644 --- a/spec/src/modules/searchandising.js +++ b/spec/src/modules/searchandising.js @@ -186,6 +186,181 @@ describe('ConstructorIO - Searchandising', function ConstructorIOSearchandising( }); }); + it('Should retrieve a list of campaigns given a single id', (done) => { + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ id: campaignId }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('id').to.equal(String(campaignId)); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given multiple ids', (done) => { + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ id: [campaignId, campaignId] }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('id').to.deep.equal([String(campaignId), String(campaignId)]); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refinedFilters', (done) => { + const refinedFilters = { group_id: ['123', '456'] }; + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refinedFilters }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_filters'); + expect(requestedUrlParams.refined_filters).to.have.property('group_id').to.deep.equal(['123', '456']); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refined_filters (snake_case)', (done) => { + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refined_filters: { group_id: ['123', '456'] } }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_filters'); + expect(requestedUrlParams.refined_filters).to.have.property('group_id').to.deep.equal(['123', '456']); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refinedRecommendationContexts', (done) => { + const refinedRecommendationContexts = { pod_id: ['pod-a', 'pod-b'] }; + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refinedRecommendationContexts }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_recommendation_contexts'); + expect(requestedUrlParams.refined_recommendation_contexts).to.have.property('pod_id').to.deep.equal(['pod-a', 'pod-b']); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refined_recommendation_contexts (snake_case)', (done) => { + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refined_recommendation_contexts: { pod_id: ['pod-a', 'pod-b'] } }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_recommendation_contexts'); + expect(requestedUrlParams.refined_recommendation_contexts).to.have.property('pod_id').to.deep.equal(['pod-a', 'pod-b']); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refinedQueries', (done) => { + const refinedQueries = ['shoes', 'boots']; + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refinedQueries }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_queries').to.deep.equal(refinedQueries); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + + it('Should retrieve a list of campaigns given refined_queries (snake_case)', (done) => { + const { searchandising } = new ConstructorIO({ + ...validOptions, + fetch: fetchSpy, + }); + + searchandising.campaigns.retrieveCampaigns({ refined_queries: ['shoes', 'boots'] }).then((res) => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestedUrlParams).to.have.property('key'); + expect(requestedUrlParams).to.have.property('refined_queries').to.deep.equal(['shoes', 'boots']); + + // Response + expect(res).to.have.property('campaigns').to.be.an('array'); + + done(); + }); + }); + it('Should pass the correct custom headers passed in function networkParameters', (done) => { const { searchandising } = new ConstructorIO({ ...validOptions, diff --git a/src/types/tests/searchandising.test-d.ts b/src/types/tests/searchandising.test-d.ts new file mode 100644 index 00000000..c669a545 --- /dev/null +++ b/src/types/tests/searchandising.test-d.ts @@ -0,0 +1,60 @@ +import { expectAssignable } from 'tsd'; +import { + CampaignListGetResponse, + Campaign, + RetrieveCampaignsParameters, + CreateCampaignParameters, +} from '../searchandising'; + +expectAssignable({ + section: 'Products', + id: [1, 2], + refinedFilters: { group_id: ['123', '456'] }, + numResultsPerPage: 50, + page: 1, + refinedRecommendationContexts: { + pod_id: ['pod-a'], + condition_type: ['item'], + }, + refinedQueries: ['shoes', 'boots'], +}); + +expectAssignable({ + name: 'Spring Sale', + section: 'Products', + description: 'Seasonal promotion campaign', + refinedQueries: [{ query: 'shoes' }], + boostRules: [ + { + rule_type: 'boost', + rule: { boost: 5, filters: { brand: ['Nike'] } }, + }, + ], +}); + +expectAssignable({ + id: 42, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-02T00:00:00Z', + name: 'Spring Sale', + refined_queries: [{ query: 'shoes' }], + boost_rules: [ + { + id: 1, + rule_type: 'boost', + rule: { boost: 5, filters: { brand: ['Nike'] } }, + }, + ], +}); + +expectAssignable({ + campaigns: [ + { + id: 42, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-02T00:00:00Z', + name: 'Spring Sale', + }, + ], + total_count: 1, +}); From 25e74538d95dfbe9902c315f7024cd8ea8a726ae Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 24 Jun 2026 10:45:32 -0400 Subject: [PATCH 14/14] Type campaigns sub-module as an interface Use an interface for the campaigns sub-module so the type file keeps a single declared class, satisfying the max-classes-per-file lint rule that also runs over .d.ts files in the pre-push hook. --- src/types/searchandising.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/types/searchandising.d.ts b/src/types/searchandising.d.ts index bda3b987..7b0ccac4 100644 --- a/src/types/searchandising.d.ts +++ b/src/types/searchandising.d.ts @@ -58,9 +58,7 @@ export interface DeleteCampaignParameters { section?: string; } -declare class Campaigns { - constructor(options: ConstructorClientOptions); - +export interface Campaigns { options: ConstructorClientOptions; retrieveCampaigns(