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
26 changes: 25 additions & 1 deletion src/commands/apps/channel-rules/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Flags } from "@oclif/core";
import chalk from "chalk";

import { ControlBaseCommand } from "../../../control-base-command.js";
import { formatChannelRuleDetails } from "../../../utils/channel-rule-display.js";
Expand All @@ -10,6 +11,7 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {

static examples = [
'$ ably apps channel-rules create --name "chat" --persisted',
'$ ably apps channel-rules create --name "chat" --mutable-messages',
'$ ably apps channel-rules create --name "events" --push-enabled',
'$ ably apps channel-rules create --name "notifications" --persisted --push-enabled --app "My App"',
];
Expand Down Expand Up @@ -55,6 +57,11 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
"Whether to expose the time serial for messages on channels matching this rule",
required: false,
}),
"mutable-messages": Flags.boolean({
description:
"Whether messages on channels matching this rule can be updated or deleted after publishing. Automatically enables message persistence",
required: false,
}),
name: Flags.string({
description: "Name of the channel rule",
required: true,
Expand Down Expand Up @@ -96,6 +103,21 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
const controlApi = this.createControlApi(flags);

try {
// When mutableMessages is enabled, persisted must also be enabled
const mutableMessages = flags["mutable-messages"];
let persisted = flags.persisted;

if (mutableMessages) {
persisted = true;
if (!this.shouldOutputJson(flags)) {
this.logToStderr(
chalk.yellow(
"Warning: Message persistence is also enabled when mutableMessages rule is set.",
),
);
}
}

const namespaceData = {
authenticated: flags.authenticated,
batchingEnabled: flags["batching-enabled"],
Expand All @@ -105,8 +127,9 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
conflationInterval: flags["conflation-interval"],
conflationKey: flags["conflation-key"],
exposeTimeSerial: flags["expose-time-serial"],
mutableMessages,
persistLast: flags["persist-last"],
persisted: flags.persisted,
persisted,
populateChannelRegistry: flags["populate-channel-registry"],
pushEnabled: flags["push-enabled"],
tlsOnly: flags["tls-only"],
Expand All @@ -132,6 +155,7 @@ export default class ChannelRulesCreateCommand extends ControlBaseCommand {
created: new Date(createdNamespace.created).toISOString(),
exposeTimeSerial: createdNamespace.exposeTimeSerial,
id: createdNamespace.id,
mutableMessages: createdNamespace.mutableMessages,
name: flags.name,
persistLast: createdNamespace.persistLast,
persisted: createdNamespace.persisted,
Expand Down
1 change: 1 addition & 0 deletions src/commands/apps/channel-rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class ChannelRulesIndexCommand extends BaseTopicCommand {
static examples = [
"ably apps channel-rules list",
'ably apps channel-rules create --name "chat" --persisted',
"ably apps channel-rules update chat --mutable-messages",
"ably apps channel-rules update chat --push-enabled",
"ably apps channel-rules delete chat",
];
Expand Down
2 changes: 2 additions & 0 deletions src/commands/apps/channel-rules/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface ChannelRuleOutput {
exposeTimeSerial: boolean;
id: string;
modified: string;
mutableMessages: boolean;
persistLast: boolean;
persisted: boolean;
populateChannelRegistry: boolean;
Expand Down Expand Up @@ -69,6 +70,7 @@ export default class ChannelRulesListCommand extends ControlBaseCommand {
exposeTimeSerial: rule.exposeTimeSerial || false,
id: rule.id,
modified: new Date(rule.modified).toISOString(),
mutableMessages: rule.mutableMessages || false,
persistLast: rule.persistLast || false,
persisted: rule.persisted || false,
populateChannelRegistry:
Expand Down
47 changes: 47 additions & 0 deletions src/commands/apps/channel-rules/update.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";

import { ControlBaseCommand } from "../../../control-base-command.js";
import { formatChannelRuleDetails } from "../../../utils/channel-rule-display.js";
Expand All @@ -16,6 +17,7 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {

static examples = [
"$ ably apps channel-rules update chat --persisted",
"$ ably apps channel-rules update chat --mutable-messages",
"$ ably apps channel-rules update events --push-enabled=false",
'$ ably apps channel-rules update notifications --persisted --push-enabled --app "My App"',
];
Expand Down Expand Up @@ -65,6 +67,12 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {
"Whether to expose the time serial for messages on channels matching this rule",
required: false,
}),
"mutable-messages": Flags.boolean({
allowNo: true,
description:
"Whether messages on channels matching this rule can be updated or deleted after publishing. Automatically enables message persistence",
required: false,
}),
"persist-last": Flags.boolean({
allowNo: true,
description:
Expand Down Expand Up @@ -131,10 +139,48 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {
const updateData: Record<string, boolean | number | string | undefined> =
{};

// Validation for mutable-messages flag, checks with supplied/existing mutableMessages flag
if (
flags.persisted === false &&
(flags["mutable-messages"] || namespace.mutableMessages)
) {
const errorMsg =
"Cannot disable persistence when mutable messages is enabled. Mutable messages requires message persistence.";
if (this.shouldOutputJson(flags)) {
this.jsonError(
{
appId,
error: errorMsg,
ruleId: namespace.id,
status: "error",
success: false,
},
flags,
);
} else {
this.error(errorMsg);
}
return;
}

if (flags.persisted !== undefined) {
updateData.persisted = flags.persisted;
}

if (flags["mutable-messages"] !== undefined) {
updateData.mutableMessages = flags["mutable-messages"];
if (flags["mutable-messages"]) {
updateData.persisted = true;
if (!this.shouldOutputJson(flags)) {
this.logToStderr(
chalk.yellow(
"Warning: Message persistence is automatically enabled when mutable messages is enabled.",
),
);
}
}
}

if (flags["push-enabled"] !== undefined) {
updateData.pushEnabled = flags["push-enabled"];
}
Expand Down Expand Up @@ -224,6 +270,7 @@ export default class ChannelRulesUpdateCommand extends ControlBaseCommand {
exposeTimeSerial: updatedNamespace.exposeTimeSerial,
id: updatedNamespace.id,
modified: new Date(updatedNamespace.modified).toISOString(),
mutableMessages: updatedNamespace.mutableMessages,
persistLast: updatedNamespace.persistLast,
persisted: updatedNamespace.persisted,
populateChannelRegistry:
Expand Down
3 changes: 3 additions & 0 deletions src/services/control-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface Namespace {
exposeTimeSerial?: boolean;
id: string;
modified: number;
mutableMessages?: boolean;
persistLast?: boolean;
persisted: boolean;
populateChannelRegistry?: boolean;
Expand Down Expand Up @@ -231,6 +232,7 @@ export class ControlApi {
conflationInterval?: number;
conflationKey?: string;
exposeTimeSerial?: boolean;
mutableMessages?: boolean;
persistLast?: boolean;
persisted?: boolean;
populateChannelRegistry?: boolean;
Expand Down Expand Up @@ -457,6 +459,7 @@ export class ControlApi {
conflationInterval?: number;
conflationKey?: string;
exposeTimeSerial?: boolean;
mutableMessages?: boolean;
persistLast?: boolean;
persisted?: boolean;
populateChannelRegistry?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/utils/channel-rule-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export function formatChannelRuleDetails(
`${indent}Push Enabled: ${bool(rule.pushEnabled)}`,
);

if (rule.mutableMessages !== undefined) {
lines.push(`${indent}Mutable Messages: ${bool(rule.mutableMessages)}`);
}

if (rule.authenticated !== undefined) {
lines.push(`${indent}Authenticated: ${bool(rule.authenticated)}`);
}
Expand Down
67 changes: 67 additions & 0 deletions test/unit/commands/apps/channel-rules/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,73 @@ describe("apps:channel-rules:create command", () => {
expect(stdout).toContain("Push Enabled: Yes");
});

it("should create a channel rule with mutable-messages flag and auto-enable persisted", async () => {
const appId = getMockConfigManager().getCurrentAppId()!;
nock("https://control.ably.net")
.post(`/v1/apps/${appId}/namespaces`, (body) => {
return body.mutableMessages === true && body.persisted === true;
})
.reply(201, {
id: mockRuleId,
persisted: true,
pushEnabled: false,
mutableMessages: true,
created: Date.now(),
modified: Date.now(),
});

const { stdout, stderr } = await runCommand(
[
"apps:channel-rules:create",
"--name",
mockRuleName,
"--mutable-messages",
],
import.meta.url,
);

expect(stdout).toContain("Channel rule created.");
expect(stdout).toContain("Persisted: Yes");
expect(stdout).toContain("Mutable Messages: Yes");
expect(stderr).toContain(
"Warning: Message persistence is also enabled when mutableMessages rule is set.",
);
});

it("should include mutableMessages in JSON output when --mutable-messages is used", async () => {
const appId = getMockConfigManager().getCurrentAppId()!;
nock("https://control.ably.net")
.post(`/v1/apps/${appId}/namespaces`, (body) => {
return body.mutableMessages === true && body.persisted === true;
})
.reply(201, {
id: mockRuleId,
persisted: true,
pushEnabled: false,
mutableMessages: true,
created: Date.now(),
modified: Date.now(),
});

const { stdout, stderr } = await runCommand(
[
"apps:channel-rules:create",
"--name",
mockRuleName,
"--mutable-messages",
"--json",
],
import.meta.url,
);

const result = JSON.parse(stdout);
expect(result).toHaveProperty("success", true);
expect(result.rule).toHaveProperty("mutableMessages", true);
expect(result.rule).toHaveProperty("persisted", true);
// Warning should not appear in JSON mode
expect(stderr).not.toContain("Warning");
});

it("should output JSON format when --json flag is used", async () => {
const appId = getMockConfigManager().getCurrentAppId()!;
const mockRule = {
Expand Down
63 changes: 63 additions & 0 deletions test/unit/commands/apps/channel-rules/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,69 @@ describe("apps:channel-rules:list command", () => {
expect(stdout).toContain("Persisted: ✓ Yes");
expect(stdout).toContain("Push Enabled: ✓ Yes");
});

it("should display mutableMessages in rule details", async () => {
const appId = getMockConfigManager().getCurrentAppId()!;
const mockRules = [
{
id: "mutable-chat",
persisted: true,
pushEnabled: false,
mutableMessages: true,
created: Date.now(),
modified: Date.now(),
},
];

nock("https://control.ably.net")
.get(`/v1/apps/${appId}/namespaces`)
.reply(200, mockRules);

const { stdout } = await runCommand(
["apps:channel-rules:list"],
import.meta.url,
);

expect(stdout).toContain("Found 1 channel rules");
expect(stdout).toContain("mutable-chat");
expect(stdout).toContain("Mutable Messages: ✓ Yes");
});

it("should include mutableMessages in JSON output", async () => {
const appId = getMockConfigManager().getCurrentAppId()!;
const mockRules = [
{
id: "mutable-chat",
persisted: true,
pushEnabled: false,
mutableMessages: true,
created: Date.now(),
modified: Date.now(),
},
{
id: "regular-chat",
persisted: false,
pushEnabled: false,
created: Date.now(),
modified: Date.now(),
},
];

nock("https://control.ably.net")
.get(`/v1/apps/${appId}/namespaces`)
.reply(200, mockRules);

const { stdout } = await runCommand(
["apps:channel-rules:list", "--json"],
import.meta.url,
);

const result = JSON.parse(stdout);
expect(result).toHaveProperty("success", true);
expect(result.rules).toHaveLength(2);
expect(result.rules[0]).toHaveProperty("mutableMessages", true);
expect(result.rules[1]).toHaveProperty("mutableMessages", false);
});
});

describe("error handling", () => {
Expand Down
Loading
Loading