Skip to content

Implement campaigns module with support for the Campaigns API#257

Open
Sher-Bakhodirov wants to merge 2 commits into
masterfrom
cdx-225-node-sdk-support-for-campaigns-api
Open

Implement campaigns module with support for the Campaigns API#257
Sher-Bakhodirov wants to merge 2 commits into
masterfrom
cdx-225-node-sdk-support-for-campaigns-api

Conversation

@Sher-Bakhodirov

Copy link
Copy Markdown
Contributor

No description provided.

@Sher-Bakhodirov Sher-Bakhodirov requested a review from a team as a code owner June 5, 2026 11:55
Copilot AI review requested due to automatic review settings June 5, 2026 11:55
constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Campaigns module to the ConstructorIO Node client, exposing CRUD operations for the Campaigns (Searchandising) API and wiring it into both runtime code and TypeScript typings, with an accompanying test suite.

Changes:

  • Added Campaigns module (retrieveCampaigns, retrieveCampaign, createCampaign, updateCampaign, deleteCampaign) and registered it on the main ConstructorIO client.
  • Added TypeScript definitions for the Campaigns API and re-exported them from the types index.
  • Added a Mocha test suite covering the new campaigns endpoints plus updated cspell dictionary.

Reviewed changes

Copilot reviewed 4 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/types/index.d.ts Re-exports Campaigns typings.
src/types/constructorio.d.ts Adds campaigns property typing to the main client.
src/types/campaigns.d.ts Introduces Campaigns API parameter/response typings.
src/modules/campaigns.js Implements Campaigns API client methods and URL builder.
src/constructorio.js Wires campaigns module into the ConstructorIO client instance.
spec/src/modules/campaigns.js Adds integration tests for Campaigns CRUD + headers/timeout behaviors.
cspell.json Adds “timeofday” to allowlist.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/modules/campaigns.js
Comment on lines +111 to +124
// 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;
}
Comment on lines +41 to +46
// 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;
}
});
@Sher-Bakhodirov Sher-Bakhodirov requested a review from a team June 5, 2026 12:05

@constructor-claude-bedrock constructor-claude-bedrock Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR implements a well-structured campaigns module for the Constructor.io Campaigns API, following the existing module patterns, with comprehensive integration tests and TypeScript type definitions.

Inline comments: 8 discussions added

Overall Assessment: ⚠️ Needs Work

Comment thread src/types/campaigns.d.ts
numResultsPerPage?: number;
page?: number;
offset?: number;
refinedRecommendationContexts?: RefinedRecommendationContext;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important Issue: In RetrieveCampaignsParameters, refinedRecommendationContexts is typed as a single RefinedRecommendationContext object, but in CreateCampaignParameters and UpdateCampaignParameters the same field is typed as RecommendationContext[] (an array). This inconsistency is likely a bug — the JS implementation uses it as a single object for query params but an array for the POST/PATCH body. Confirm the intended shape for each operation and ensure types match the actual API behavior.

});
});

it('Should return error when deleting a campaign with an invalid API key', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important Issue: The deleteCampaign error-path tests (invalid API key / invalid API token) use the outer suite-level campaignId rather than creating a dedicated campaign to delete. This risks deleting the shared campaignId if the API returns a successful (2xx) response for some reason, or causes a 404 during subsequent tests that depend on campaignId still existing. Use createCampaignToDelete() (already defined in this describe block) consistently for the error-path tests as well, for proper test isolation.

@Mudaafi Mudaafi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty good! I just have some comments w.r.t. the types, but thanks for working on adding this in!

Comment thread src/types/campaigns.d.ts
section?: string;
}

export interface CreateCampaignParameters extends Record<string, any> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for extending Record<String, any> here? I know we use Record<string, any> in other type definitions, but we definitely don't want to continue this practice since it goes against the main benefit of typescript

Comment thread src/types/campaigns.d.ts
Comment on lines +155 to +172
export interface CampaignRuleInput extends Record<string, any> {
rule: Record<string, any>;
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a little verbose to repeat type declarations here. Can we use something like this instead? wdyt?

Suggested change
export interface CampaignRuleInput extends Record<string, any> {
rule: Record<string, any>;
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 CampaignRuleInput extends Partial<Omit<CampaignRule, 'created_at' | 'updated_at'>> {
id: number;
}

Comment thread src/types/campaigns.d.ts
Comment on lines +30 to +44
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a CampaignRule object since it's being re-used across multiple interfaces.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or actually we already have a Campaign object below. Can't we reuse that here rather than redefining all these fields?

Comment thread src/types/campaigns.d.ts

export interface RecommendationContext extends Record<string, any> {
pod_id: string;
condition: Record<string, any>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be defined into three types of conditions right? https://docs.constructor.com/reference/v1-searchandising-create-campaign

Comment thread src/types/campaigns.d.ts
condition: Record<string, any>;
}

export interface CampaignRule extends Record<string, any> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's generalize this to RuleObj since it's a shared object for searchandizing rules as well

Comment thread src/types/campaigns.d.ts

export interface CampaignRule extends Record<string, any> {
id: number;
rule: Record<string, any>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, we can define a generic rule object and branch it out to the different types by following the schema here: https://github.com/Constructor-io/autocomplete/blob/master/ingestion_service/src/ingestion_service/blueprints/serializers/rules.py#L162

Comment thread src/modules/campaigns.js
Comment on lines +53 to +58
* @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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is the order the documentation lists the parameters, but let's move the refinedQueries and refinedRecommendationContexts` up just for better QOL

Suggested change
* @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 {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`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can throw up a PR to the official docs to better improve the docs there too

Comment on lines +101 to +102
it('Should retrieve a list of campaigns given section', (done) => {
const section = 'Products';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to include this test, we should use a different section. Since the default is always Products, we won't know if this actually succeeds or if it's just using the default.

Note: This means upon starting the test-suite, we'll probably want to make sure a campaign exists in another section as well.

});

// 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(() => {})));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants