Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/commands/apps/channel-rules/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
authenticated: flags.authenticated,
batchingEnabled: flags["batching-enabled"],
batchingInterval: flags["batching-interval"],
channelNamespace: flags.name,
id: flags.name,
conflationEnabled: flags["conflation-enabled"],
conflationInterval: flags["conflation-interval"],
conflationKey: flags["conflation-key"],
Expand Down Expand Up @@ -209,7 +209,7 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
);
}

if (createdNamespace.batchingInterval !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should these be falsy checks?

        if (!createdNamespace.batchingInterval) {

if (createdNamespace.batchingInterval != null) {
this.log(
`Batching Interval: ${chalk.green(createdNamespace.batchingInterval.toString())}`,
);
Expand All @@ -221,7 +221,7 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
);
}

if (createdNamespace.conflationInterval !== undefined) {
if (createdNamespace.conflationInterval != null) {
this.log(
`Conflation Interval: ${chalk.green(createdNamespace.conflationInterval.toString())}`,
);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/apps/channel-rules/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default class ChannelRulesDeleteCommand extends ControlBaseCommand {
);
}

if (namespace.batchingInterval !== undefined) {
if (namespace.batchingInterval != null) {
this.log(
`Batching Interval: ${chalk.green(namespace.batchingInterval.toString())}`,
);
Expand All @@ -147,7 +147,7 @@ export default class ChannelRulesDeleteCommand extends ControlBaseCommand {
);
}

if (namespace.conflationInterval !== undefined) {
if (namespace.conflationInterval != null) {
this.log(
`Conflation Interval: ${chalk.green(namespace.conflationInterval.toString())}`,
);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/apps/channel-rules/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {
);
}

if (updatedNamespace.batchingInterval !== undefined) {
if (updatedNamespace.batchingInterval != null) {
this.log(
`Batching Interval: ${chalk.green(updatedNamespace.batchingInterval.toString())}`,
);
Expand All @@ -317,7 +317,7 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {
);
}

if (updatedNamespace.conflationInterval !== undefined) {
if (updatedNamespace.conflationInterval != null) {
this.log(
`Conflation Interval: ${chalk.green(updatedNamespace.conflationInterval.toString())}`,
);
Expand Down
2 changes: 1 addition & 1 deletion src/services/control-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class ControlApi {
authenticated?: boolean;
batchingEnabled?: boolean;
batchingInterval?: number;
channelNamespace: string;
id: string;
conflationEnabled?: boolean;
conflationInterval?: number;
conflationKey?: string;
Comment on lines 224 to 230
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Now that the CLI explicitly handles batchingInterval/conflationInterval potentially being null, the Namespace model (and related method return types) should reflect that (number | null) rather than only number. Keeping the type accurate will prevent future code from accidentally calling number methods on null and removes the need for non-strict null checks in consumers.

Copilot uses AI. Check for mistakes.
Copy link

Choose a reason for hiding this comment

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

@coderabbitai do you think aboe suggestion is correct? Provide detailed explaination for the same

Copy link

Choose a reason for hiding this comment

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

@sacOO7, yes, Copilot's suggestion is correct. Here's a detailed explanation:

Current Type Definition

The Namespace interface currently defines these fields as:

batchingInterval?: number;
conflationInterval?: number;

The ? operator makes these optional properties, which means their type is number | undefined. A property is undefined when it's absent/not set.

The Problem

According to the PR objectives, the API can explicitly return null for these fields (not just omit them). However, null is not the same as undefined in TypeScript:

  • undefined = property is absent/not set
  • null = property is explicitly set to null

Since the current types are number | undefined, TypeScript won't catch type errors if code assumes these are always numbers when the property exists:

// This would compile fine with current types, but crash at runtime if value is null
const interval = namespace.batchingInterval; // Type: number | undefined
if (interval !== undefined) {
  console.log(interval.toFixed(2)); // ❌ Runtime error if interval is null
}

The Fix

The checks in your PR were changed from !== undefined to != null (loose equality) specifically because the API returns null. The types should match this reality:

batchingInterval?: number | null;
conflationInterval?: number | null;

This expands to number | null | undefined, which accurately represents:

  • Field might be absent (undefined)
  • Field might be explicitly null (null)
  • Field might contain a number

Recommendation

Update the Namespace interface to reflect the actual API behavior:

export interface Namespace {
  // ... other fields ...
  batchingInterval?: number | null;
  conflationInterval?: number | null;
  // ... other fields ...
}

This makes the type system more accurate and helps prevent runtime errors by forcing explicit null checks.

Copy link

Choose a reason for hiding this comment

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

@coderabbitai https://github.com/coderabbitai It's also suggested to change channelNamespace into id, is it expected or no?

Copy link

Choose a reason for hiding this comment

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

@sacOO7, yes, changing channelNamespace to id is expected and correct. Here's why:

The Namespace Interface Uses id

Looking at the Namespace interface (which is the return type of createNamespace), the field is defined as:

export interface Namespace {
  // ...
  id: string;  // ← The field name in the interface
  // ...
}

There is no channelNamespace field in the Namespace interface.

Input Should Match Output

The createNamespace method:

  • Accepts parameters to create a namespace
  • Returns a Namespace object

The parameter field names should align with the field names in the returned Namespace type. Since the API returns an object with an id field (not channelNamespace), the input parameter should also use id.

Bug Fix Per PR Objectives

The PR description explicitly states:

"Previously failed because it sent channelNamepsace instead of id in the request."

This confirms it was a bug - the API expects id as the field name, but the code was incorrectly sending channelNamespace.

Summary

The change is correct: The parameter should be id to match:

  1. The Namespace interface field name
  2. What the Control API expects
  3. Consistency between input parameters and output types

Expand Down
64 changes: 32 additions & 32 deletions test/integration/control-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Control API Integration Tests', () => {
this.skip();
return;
}

controlApi = new ControlApi({
accessToken,
logErrors: false
Expand Down Expand Up @@ -120,31 +120,31 @@ describe('Control API Integration Tests', () => {
};

const app = await controlApi.createApp(appData);

expect(app).to.have.property('id');
expect(app).to.have.property('name', appData.name);
expect(app).to.have.property('tlsOnly', false);
expect(app).to.have.property('accountId', testAccountId);

// Track for cleanup
createdResources.apps.push(app.id);
testAppId = app.id;
});

it('should list apps', async () => {
const apps = await controlApi.listApps();

expect(apps).to.be.an('array');
expect(apps.length).to.be.greaterThan(0);

// Should include our test app
const testApp = apps.find(app => app.id === testAppId);
expect(testApp).to.exist;
});

it('should get a specific app', async () => {
const app = await controlApi.getApp(testAppId);

expect(app).to.have.property('id', testAppId);
expect(app).to.have.property('accountId', testAccountId);
});
Expand All @@ -156,7 +156,7 @@ describe('Control API Integration Tests', () => {
};

const updatedApp = await controlApi.updateApp(testAppId, updateData);

expect(updatedApp).to.have.property('id', testAppId);
expect(updatedApp).to.have.property('name', updateData.name);
expect(updatedApp).to.have.property('tlsOnly', true);
Expand All @@ -175,30 +175,30 @@ describe('Control API Integration Tests', () => {
};

const key = await controlApi.createKey(testAppId, keyData);

expect(key).to.have.property('id');
expect(key).to.have.property('name', keyData.name);
expect(key).to.have.property('appId', testAppId);
expect(key).to.have.property('key');

testKeyId = key.id;
createdResources.keys.push(key.id);
});

it('should list API keys', async () => {
const keys = await controlApi.listKeys(testAppId);

expect(keys).to.be.an('array');
expect(keys.length).to.be.greaterThan(0);

// Should include our test key
const testKey = keys.find(key => key.id === testKeyId);
expect(testKey).to.exist;
});

it('should get a specific API key', async () => {
const key = await controlApi.getKey(testAppId, testKeyId);

expect(key).to.have.property('id', testKeyId);
expect(key).to.have.property('appId', testAppId);
});
Expand All @@ -213,7 +213,7 @@ describe('Control API Integration Tests', () => {
};

const updatedKey = await controlApi.updateKey(testAppId, testKeyId, updateData);

expect(updatedKey).to.have.property('id', testKeyId);
expect(updatedKey).to.have.property('name', updateData.name);
});
Expand All @@ -232,23 +232,23 @@ describe('Control API Integration Tests', () => {
};

const queue = await controlApi.createQueue(testAppId, queueData);

expect(queue).to.have.property('id');
expect(queue).to.have.property('name', testQueueName);
expect(queue).to.have.property('appId', testAppId);
expect(queue).to.have.property('maxLength', 1000);
expect(queue).to.have.property('ttl', 3600);
expect(queue).to.have.property('region', 'us-east-1-a');

createdResources.queues.push(testQueueName);
});

it('should list queues', async () => {
const queues = await controlApi.listQueues(testAppId);

expect(queues).to.be.an('array');
expect(queues.length).to.be.greaterThan(0);

// Should include our test queue
const testQueue = queues.find(queue => queue.name === testQueueName);
expect(testQueue).to.exist;
Expand Down Expand Up @@ -279,32 +279,32 @@ describe('Control API Integration Tests', () => {
};

const rule = await controlApi.createRule(testAppId, ruleData);

expect(rule).to.have.property('id');
expect(rule).to.have.property('appId', testAppId);
expect(rule).to.have.property('ruleType', 'http');
expect(rule).to.have.property('requestMode', 'single');
expect(rule).to.have.property('source');
expect(rule.source).to.have.property('channelFilter', 'test-channel');

testRuleId = rule.id;
createdResources.rules.push(rule.id);
});

it('should list integration rules', async () => {
const rules = await controlApi.listRules(testAppId);

expect(rules).to.be.an('array');
expect(rules.length).to.be.greaterThan(0);

// Should include our test rule
const testRule = rules.find(rule => rule.id === testRuleId);
expect(testRule).to.exist;
});

it('should get a specific integration rule', async () => {
const rule = await controlApi.getRule(testAppId, testRuleId);

expect(rule).to.have.property('id', testRuleId);
expect(rule).to.have.property('appId', testAppId);
});
Expand All @@ -321,7 +321,7 @@ describe('Control API Integration Tests', () => {
};

const updatedRule = await controlApi.updateRule(testAppId, testRuleId, updateData);

expect(updatedRule).to.have.property('id', testRuleId);
expect(updatedRule.source).to.have.property('channelFilter', 'updated-channel');
});
Expand All @@ -332,38 +332,38 @@ describe('Control API Integration Tests', () => {

it('should create a new namespace', async () => {
const namespaceData = {
channelNamespace: `test-namespace-${Date.now()}`,
id: `test-namespace-${Date.now()}`,
persisted: true,
pushEnabled: false,
tlsOnly: true
};

const namespace = await controlApi.createNamespace(testAppId, namespaceData);

expect(namespace).to.have.property('id');
expect(namespace).to.have.property('appId', testAppId);
expect(namespace).to.have.property('persisted', true);
expect(namespace).to.have.property('pushEnabled', false);
expect(namespace).to.have.property('tlsOnly', true);

testNamespaceId = namespace.id;
createdResources.namespaces.push(namespace.id);
});

it('should list namespaces', async () => {
const namespaces = await controlApi.listNamespaces(testAppId);

expect(namespaces).to.be.an('array');
expect(namespaces.length).to.be.greaterThan(0);

// Should include our test namespace
const testNamespace = namespaces.find(ns => ns.id === testNamespaceId);
expect(testNamespace).to.exist;
});

it('should get a specific namespace', async () => {
const namespace = await controlApi.getNamespace(testAppId, testNamespaceId);

expect(namespace).to.have.property('id', testNamespaceId);
expect(namespace).to.have.property('appId', testAppId);
});
Expand All @@ -377,7 +377,7 @@ describe('Control API Integration Tests', () => {
};

const updatedNamespace = await controlApi.updateNamespace(testAppId, testNamespaceId, updateData);

expect(updatedNamespace).to.have.property('id', testNamespaceId);
expect(updatedNamespace).to.have.property('persisted', false);
expect(updatedNamespace).to.have.property('pushEnabled', true);
Expand Down Expand Up @@ -434,7 +434,7 @@ describe('Control API Integration Tests', () => {
}

const results = await Promise.all(promises);

expect(results).to.have.length(5);
results.forEach(apps => {
expect(apps).to.be.an('array');
Expand All @@ -452,4 +452,4 @@ describe('Control API Integration Tests', () => {
}
});
});
});
});
Loading