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..fe087bf1 --- /dev/null +++ b/spec/src/modules/campaigns.js @@ -0,0 +1,901 @@ +/* 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; + } + }); + + // 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 () => { + const { campaigns } = 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(() => {}))); + }); + + 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, refinedQueries: [{ query: 'test-query' }] }).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, refinedQueries: [{ query: 'test-query' }] }).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`, 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(); + }); + }); + + 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`, 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(); + }); + }); + + 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`, 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'); + 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`, 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({ + ...invalidOptions, + fetch: fetchSpy, + }); + + 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`, 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({ + ...validOptions, + networkParameters: { timeout: 20 }, + }); + + return expect(campaigns.createCampaign({ name: `${createCampaignName}-global-timeout`, refinedQueries: [{ query: 'test-query' }] })).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()}`, refinedQueries: [{ query: 'test-query' }] }).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()}`, refinedQueries: [{ query: 'test-query' }] }); + + 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;