diff --git a/src/commands/apps/list.ts b/src/commands/apps/list.ts index 7a38e96a..187c8520 100644 --- a/src/commands/apps/list.ts +++ b/src/commands/apps/list.ts @@ -39,6 +39,8 @@ export default class AppsList extends ControlBaseCommand { // Mark current app in JSON output const appsWithCurrentFlag = apps.map((app) => ({ ...app, + created: new Date(app.created).toISOString(), + modified: new Date(app.modified).toISOString(), isCurrent: app.id === currentAppId, })); diff --git a/src/commands/auth/keys/create.ts b/src/commands/auth/keys/create.ts index 34577f61..39616227 100644 --- a/src/commands/auth/keys/create.ts +++ b/src/commands/auth/keys/create.ts @@ -76,6 +76,8 @@ export default class KeysCreateCommand extends ControlBaseCommand { { key: { ...key, + created: new Date(key.created).toISOString(), + modified: new Date(key.modified).toISOString(), keyName: `${key.appId}.${key.id}`, }, }, diff --git a/src/commands/auth/keys/get.ts b/src/commands/auth/keys/get.ts index 63808b96..949d6a78 100644 --- a/src/commands/auth/keys/get.ts +++ b/src/commands/auth/keys/get.ts @@ -93,6 +93,8 @@ export default class KeysGetCommand extends ControlBaseCommand { { key: { ...key, + created: new Date(key.created).toISOString(), + modified: new Date(key.modified).toISOString(), keyName, ...(hasEnvOverride ? { diff --git a/src/commands/auth/keys/list.ts b/src/commands/auth/keys/list.ts index 59d8b3aa..3dda5492 100644 --- a/src/commands/auth/keys/list.ts +++ b/src/commands/auth/keys/list.ts @@ -64,6 +64,8 @@ export default class KeysListCommand extends ControlBaseCommand { const keyName = `${key.appId}.${key.id}`; return { ...key, + created: new Date(key.created).toISOString(), + modified: new Date(key.modified).toISOString(), current: keyName === currentKeyName, keyName, // Add the full key name }; diff --git a/src/commands/channels/history.ts b/src/commands/channels/history.ts index 104008eb..34f6bf36 100644 --- a/src/commands/channels/history.ts +++ b/src/commands/channels/history.ts @@ -129,7 +129,16 @@ export default class ChannelsHistory extends AblyBaseCommand { const lastTimestamp = messages.length > 0 ? messages.at(-1)!.timestamp : undefined; const next = buildPaginationNext(hasMore, lastTimestamp); - this.logJsonResult({ messages, hasMore, ...(next && { next }) }, flags); + const jsonMessages = messages.map((msg) => ({ + ...msg, + timestamp: msg.timestamp + ? new Date(msg.timestamp).toISOString() + : undefined, + })); + this.logJsonResult( + { messages: jsonMessages, hasMore, ...(next && { next }) }, + flags, + ); } else { if (messages.length === 0) { this.log("No messages found in the channel history."); diff --git a/src/commands/integrations/create.ts b/src/commands/integrations/create.ts index f6682b8d..6664811e 100644 --- a/src/commands/integrations/create.ts +++ b/src/commands/integrations/create.ts @@ -150,7 +150,16 @@ export default class IntegrationsCreateCommand extends ControlBaseCommand { ); if (this.shouldOutputJson(flags)) { - this.logJsonResult({ integration: createdIntegration }, flags); + this.logJsonResult( + { + integration: { + ...createdIntegration, + created: new Date(createdIntegration.created).toISOString(), + modified: new Date(createdIntegration.modified).toISOString(), + }, + }, + flags, + ); } else { this.log( formatSuccess( diff --git a/src/commands/integrations/get.ts b/src/commands/integrations/get.ts index 906c638b..990abae7 100644 --- a/src/commands/integrations/get.ts +++ b/src/commands/integrations/get.ts @@ -41,12 +41,13 @@ export default class IntegrationsGetCommand extends ControlBaseCommand { const rule = await controlApi.getRule(appId, args.ruleId); if (this.shouldOutputJson(flags)) { - this.logJsonResult( - { - rule: structuredClone(rule) as unknown as Record, - }, - flags, - ); + const ruleClone = structuredClone(rule) as unknown as Record< + string, + unknown + >; + ruleClone.created = new Date(rule.created).toISOString(); + ruleClone.modified = new Date(rule.modified).toISOString(); + this.logJsonResult({ rule: ruleClone }, flags); } else { this.log(formatHeading("Integration Rule Details")); this.log(`${formatLabel("ID")} ${rule.id}`); diff --git a/src/commands/integrations/update.ts b/src/commands/integrations/update.ts index 832fde4e..cc9feffc 100644 --- a/src/commands/integrations/update.ts +++ b/src/commands/integrations/update.ts @@ -107,7 +107,16 @@ export default class IntegrationsUpdateCommand extends ControlBaseCommand { ); if (this.shouldOutputJson(flags)) { - this.logJsonResult({ rule: updatedRule }, flags); + this.logJsonResult( + { + rule: { + ...updatedRule, + created: new Date(updatedRule.created).toISOString(), + modified: new Date(updatedRule.modified).toISOString(), + }, + }, + flags, + ); } else { this.log(formatSuccess("Integration rule updated.")); this.log(`ID: ${updatedRule.id}`); diff --git a/src/commands/rooms/messages/history.ts b/src/commands/rooms/messages/history.ts index 20197c58..64e2f8a2 100644 --- a/src/commands/rooms/messages/history.ts +++ b/src/commands/rooms/messages/history.ts @@ -165,7 +165,7 @@ export default class MessagesHistory extends ChatBaseCommand { messages: items.map((message) => ({ clientId: message.clientId, text: message.text, - timestamp: message.timestamp, + timestamp: message.timestamp.toISOString(), serial: message.serial, action: String(message.action), ...(message.metadata ? { metadata: message.metadata } : {}), diff --git a/src/commands/rooms/messages/subscribe.ts b/src/commands/rooms/messages/subscribe.ts index 8958dfce..acee6fb4 100644 --- a/src/commands/rooms/messages/subscribe.ts +++ b/src/commands/rooms/messages/subscribe.ts @@ -19,7 +19,7 @@ import { interface ChatMessage { clientId: string; text: string; - timestamp: number | Date; // Support both timestamp types + timestamp: string; serial: string; action: string; metadata?: Record; @@ -100,7 +100,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { const messageLog: ChatMessage = { clientId: message.clientId, text: message.text, - timestamp: message.timestamp, + timestamp: message.timestamp.toISOString(), serial: message.serial, action: String(messageEvent.type), ...(message.metadata ? { metadata: message.metadata } : {}), diff --git a/src/commands/spaces/get.ts b/src/commands/spaces/get.ts index 5044ee35..b92b42ff 100644 --- a/src/commands/spaces/get.ts +++ b/src/commands/spaces/get.ts @@ -9,7 +9,6 @@ import { formatHeading, formatIndex, formatLabel, - formatMessageTimestamp, formatProgress, formatResource, } from "../../utils/output.js"; @@ -116,7 +115,10 @@ export default class SpacesGet extends SpacesBaseCommand { isConnected: action !== "leave" && action !== "absent", profileData: item.data?.profileUpdate?.current ?? null, location: item.data?.locationUpdate?.current ?? null, - lastEvent: { name: action, timestamp: item.timestamp }, + lastEvent: { + name: action, + timestamp: new Date(item.timestamp).toISOString(), + }, }; }); @@ -163,7 +165,7 @@ export default class SpacesGet extends SpacesBaseCommand { ` ${formatLabel("Last Event")} ${formatEventType(member.lastEvent.name)}`, ); this.log( - ` ${formatLabel("Event Timestamp")} ${formatMessageTimestamp(member.lastEvent.timestamp)}`, + ` ${formatLabel("Event Timestamp")} ${member.lastEvent.timestamp}`, ); this.log(""); } diff --git a/src/control-base-command.ts b/src/control-base-command.ts index 8acf2151..f8db353f 100644 --- a/src/control-base-command.ts +++ b/src/control-base-command.ts @@ -58,7 +58,7 @@ export abstract class ControlBaseCommand extends AblyBaseCommand { } protected formatDate(timestamp: number): string { - return new Date(timestamp).toLocaleString(); + return new Date(timestamp).toISOString(); } /** diff --git a/src/utils/spaces-output.ts b/src/utils/spaces-output.ts index 32c559bc..67d871d1 100644 --- a/src/utils/spaces-output.ts +++ b/src/utils/spaces-output.ts @@ -16,7 +16,7 @@ export interface MemberOutput { isConnected: boolean; profileData: Record | null; location: unknown | null; - lastEvent: { name: string; timestamp: number }; + lastEvent: { name: string; timestamp: string }; } export interface CursorOutput { @@ -30,7 +30,7 @@ export interface LockOutput { id: string; status: string; member: MemberOutput; - timestamp: number; + timestamp: string; attributes: Record | null; reason: { message?: string; code?: number; statusCode?: number } | null; } @@ -51,7 +51,7 @@ export function formatMemberOutput(member: SpaceMember): MemberOutput { location: member.location ?? null, lastEvent: { name: member.lastEvent.name, - timestamp: member.lastEvent.timestamp, + timestamp: new Date(member.lastEvent.timestamp).toISOString(), }, }; } @@ -70,7 +70,7 @@ export function formatLockOutput(lock: Lock): LockOutput { id: lock.id, status: lock.status, member: formatMemberOutput(lock.member), - timestamp: lock.timestamp, + timestamp: new Date(lock.timestamp).toISOString(), attributes: (lock.attributes as Record) ?? null, reason: lock.reason ? { diff --git a/test/unit/commands/apps/create.test.ts b/test/unit/commands/apps/create.test.ts index 9e9e8d53..06d48ba7 100644 --- a/test/unit/commands/apps/create.test.ts +++ b/test/unit/commands/apps/create.test.ts @@ -67,6 +67,7 @@ describe("apps:create command", () => { expect(stdout).toContain(newAppId); expect(stdout).toContain(mockAppName); expect(stdout).toContain("Automatically switched to app"); + expect(stdout).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/); }); it("should create an app with TLS only flag", async () => { diff --git a/test/unit/commands/apps/list.test.ts b/test/unit/commands/apps/list.test.ts index da8a4790..0140cd50 100644 --- a/test/unit/commands/apps/list.test.ts +++ b/test/unit/commands/apps/list.test.ts @@ -106,6 +106,8 @@ describe("apps:list command", () => { "550e8400-e29b-41d4-a716-446655440001", ); expect(result.apps[1]).toHaveProperty("name", "Test App 2"); + expect(result.apps[0].created).toBe("2022-01-01T00:00:00.000Z"); + expect(result.apps[0].modified).toBe("2022-01-01T00:00:00.000Z"); }); it("should handle empty apps list", async () => { diff --git a/test/unit/commands/auth/keys/create.test.ts b/test/unit/commands/auth/keys/create.test.ts index e0cd071d..9770cf0c 100644 --- a/test/unit/commands/auth/keys/create.test.ts +++ b/test/unit/commands/auth/keys/create.test.ts @@ -147,6 +147,12 @@ describe("auth:keys:create command", () => { expect(result.key).toHaveProperty("id", mockKeyId); expect(result.key).toHaveProperty("name", "TestKey"); expect(result.key).toHaveProperty("key"); + expect(result.key.created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.key.modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); it("should use ABLY_ACCESS_TOKEN environment variable when provided", async () => { diff --git a/test/unit/commands/auth/keys/get.test.ts b/test/unit/commands/auth/keys/get.test.ts index b3a653cb..a04487c3 100644 --- a/test/unit/commands/auth/keys/get.test.ts +++ b/test/unit/commands/auth/keys/get.test.ts @@ -128,6 +128,12 @@ describe("auth:keys:get command", () => { expect(result).toHaveProperty("success", true); expect(result).toHaveProperty("key"); expect(result.key).toHaveProperty("id", mockKeyId); + expect(result.key.created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.key.modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); it("should show warning when fetched key is current and ABLY_API_KEY env var overrides it", async () => { diff --git a/test/unit/commands/auth/keys/list.test.ts b/test/unit/commands/auth/keys/list.test.ts index ba7c8544..66f8f9a0 100644 --- a/test/unit/commands/auth/keys/list.test.ts +++ b/test/unit/commands/auth/keys/list.test.ts @@ -158,6 +158,12 @@ describe("auth:keys:list command", () => { expect(result).toHaveProperty("keys"); expect(result.keys).toHaveLength(1); expect(result.keys[0]).toHaveProperty("name", "Test Key"); + expect(result.keys[0].created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.keys[0].modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); }); diff --git a/test/unit/commands/channels/history.test.ts b/test/unit/commands/channels/history.test.ts index 57d717c0..d4512b6a 100644 --- a/test/unit/commands/channels/history.test.ts +++ b/test/unit/commands/channels/history.test.ts @@ -182,6 +182,7 @@ describe("channels:history command", () => { expect(messages[0]).toHaveProperty("name", "test-event"); expect(messages[0]).toHaveProperty("data"); expect(messages[0].data).toEqual({ text: "Hello world" }); + expect(messages[0].timestamp).toBe("2023-11-14T22:13:20.000Z"); }); }); diff --git a/test/unit/commands/integrations/create.test.ts b/test/unit/commands/integrations/create.test.ts index aff8ac55..ba11738a 100644 --- a/test/unit/commands/integrations/create.test.ts +++ b/test/unit/commands/integrations/create.test.ts @@ -115,6 +115,8 @@ describe("integrations:create command", () => { url: "https://example.com/webhook", }, status: "disabled", + created: 1640995200000, + modified: 1640995200000, }); const { stdout } = await runCommand( @@ -159,6 +161,8 @@ describe("integrations:create command", () => { url: "https://example.com/webhook", }, status: "enabled", + created: 1640995200000, + modified: 1640995200000, }); const { stdout } = await runCommand( @@ -202,6 +206,8 @@ describe("integrations:create command", () => { format: "json", }, status: "enabled", + created: 1640995200000, + modified: 1640995200000, }); const { stdout } = await runCommand( @@ -227,6 +233,12 @@ describe("integrations:create command", () => { expect(result).toHaveProperty("integration"); expect(result.integration).toHaveProperty("id", mockRuleId); expect(result.integration).toHaveProperty("ruleType", "http"); + expect(result.integration.created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.integration.modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); }); @@ -352,6 +364,8 @@ describe("integrations:create command", () => { url: "https://example.com/webhook", }, status: "enabled", + created: 1640995200000, + modified: 1640995200000, }); const { stdout } = await runCommand( @@ -392,6 +406,8 @@ describe("integrations:create command", () => { url: "https://example.com/webhook", }, status: "enabled", + created: 1640995200000, + modified: 1640995200000, }); const { stdout } = await runCommand( diff --git a/test/unit/commands/integrations/get.test.ts b/test/unit/commands/integrations/get.test.ts index cfb3ac9c..d41613e3 100644 --- a/test/unit/commands/integrations/get.test.ts +++ b/test/unit/commands/integrations/get.test.ts @@ -95,6 +95,12 @@ describe("integrations:get command", () => { expect(result.rule).toHaveProperty("ruleType", "http"); expect(result.rule).toHaveProperty("source"); expect(result.rule.source).toHaveProperty("type", "channel.message"); + expect(result.rule.created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.rule.modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); it("should output pretty JSON when --pretty-json flag is used", async () => { diff --git a/test/unit/commands/integrations/update.test.ts b/test/unit/commands/integrations/update.test.ts index 0b99f22e..81f84242 100644 --- a/test/unit/commands/integrations/update.test.ts +++ b/test/unit/commands/integrations/update.test.ts @@ -167,6 +167,12 @@ describe("integrations:update command", () => { expect(result).toHaveProperty("success", true); expect(result).toHaveProperty("rule"); expect(result.rule).toHaveProperty("id", mockRuleId); + expect(result.rule.created).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); + expect(result.rule.modified).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/, + ); }); it("should update request mode", async () => {