Skip to content
Merged
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
147 changes: 147 additions & 0 deletions src/api-client/entity-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
summarizeUserDetail,
summarizeSegment,
summarizeSegmentRow,
summarizeTagRow,
summarizeMetricCategoryRow,
summarizeNamedEntityRow,
summarizeWebhookRow,
} from './entity-summary.js';

describe('applyShowExclude', () => {
Expand Down Expand Up @@ -254,3 +258,146 @@ describe('summarizeSegmentRow', () => {
expect(result).toHaveProperty('attribute', 'device');
});
});

describe('summarizeGoal created_by field', () => {
it('returns full name when first and last are present', () => {
const result = summarizeGoal({
id: 1,
name: 'g',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
});
expect(result.created_by).toBe('Joe Bloggs');
});

it('returns email when name parts are missing', () => {
const result = summarizeGoal({
id: 1,
name: 'g',
created_by: { email: 'joe@example.com' },
});
expect(result.created_by).toBe('joe@example.com');
});

it('returns empty string for null created_by', () => {
const result = summarizeGoal({ id: 1, name: 'g', created_by: null });
expect(result.created_by).toBe('');
});
});

describe('summarizeTagRow', () => {
it('curates the simple tag columns and summarizes user fields', () => {
expect(
summarizeTagRow({
id: 7,
tag: 'top-priority',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 7,
tag: 'top-priority',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});

it('defaults archived to false when missing', () => {
expect(summarizeTagRow({ id: 1, tag: 't' }).archived).toBe(false);
});
});

describe('summarizeMetricCategoryRow', () => {
it('picks the useful columns and summarizes user fields', () => {
expect(
summarizeMetricCategoryRow({
id: 3,
name: 'Revenue',
description: 'all revenue metrics',
color: '#ff5733',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 3,
name: 'Revenue',
description: 'all revenue metrics',
color: '#ff5733',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});
});

describe('summarizeNamedEntityRow', () => {
it('curates id/name/description/archived plus user/time audit fields', () => {
expect(
summarizeNamedEntityRow({
id: 4,
name: 'production',
description: 'production env',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: '2024-05-23T10:29:34.375Z',
updated_by: { email: 'admin@x' },
})
).toEqual({
id: 4,
name: 'production',
description: 'production env',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '2024-05-23T10:29:34.375Z',
updated_by: 'admin@x',
});
});

it('keeps description empty if missing', () => {
expect(summarizeNamedEntityRow({ id: 1, name: 'x' }).description).toBe('');
});
});

describe('summarizeWebhookRow', () => {
it('curates webhook-specific columns and summarizes user fields', () => {
expect(
summarizeWebhookRow({
id: 2,
name: 'metrics-sync',
url: 'https://example.com/hooks/metrics',
enabled: true,
ordered: false,
max_retries: 5,
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 2,
name: 'metrics-sync',
url: 'https://example.com/hooks/metrics',
enabled: true,
ordered: false,
max_retries: 5,
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});
});
60 changes: 57 additions & 3 deletions src/api-client/entity-summary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { formatDate, resolveDotPath } from './format-helpers.js';
import { formatUserSummary } from '../lib/output/formatter.js';

export function applyShowExclude(
summary: Record<string, unknown>,
Expand Down Expand Up @@ -44,9 +45,7 @@ export function applyShowExclude(

function formatOwner(obj: Record<string, unknown> | undefined): string {
if (!obj) return '';
const first = (obj.first_name as string) ?? '';
const last = (obj.last_name as string) ?? '';
return [first, last].filter(Boolean).join(' ') || (obj.email as string) || '';
return formatUserSummary(obj) ?? '';
}

export function summarizeMetric(m: Record<string, unknown>): Record<string, unknown> {
Expand Down Expand Up @@ -229,3 +228,58 @@ export function summarizeCustomField(f: Record<string, unknown>): Record<string,
created_at: formatDate(f.created_at),
};
}

export function summarizeTagRow(t: Record<string, unknown>): Record<string, unknown> {
return {
id: t.id,
tag: t.tag ?? '',
archived: t.archived ?? false,
created_at: t.created_at ?? '',
created_by: formatOwner(t.created_by as Record<string, unknown> | undefined),
updated_at: t.updated_at ?? '',
updated_by: formatOwner(t.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeMetricCategoryRow(c: Record<string, unknown>): Record<string, unknown> {
return {
id: c.id,
name: c.name ?? '',
description: c.description ?? '',
color: c.color ?? '',
archived: c.archived ?? false,
created_at: c.created_at ?? '',
created_by: formatOwner(c.created_by as Record<string, unknown> | undefined),
updated_at: c.updated_at ?? '',
updated_by: formatOwner(c.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeNamedEntityRow(e: Record<string, unknown>): Record<string, unknown> {
return {
id: e.id,
name: e.name ?? '',
description: e.description ?? '',
archived: e.archived ?? false,
created_at: e.created_at ?? '',
created_by: formatOwner(e.created_by as Record<string, unknown> | undefined),
updated_at: e.updated_at ?? '',
updated_by: formatOwner(e.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeWebhookRow(w: Record<string, unknown>): Record<string, unknown> {
return {
id: w.id,
name: w.name ?? '',
url: w.url ?? '',
enabled: w.enabled ?? false,
ordered: w.ordered ?? false,
max_retries: w.max_retries ?? 0,
archived: w.archived ?? false,
created_at: w.created_at ?? '',
created_by: formatOwner(w.created_by as Record<string, unknown> | undefined),
updated_at: w.updated_at ?? '',
updated_by: formatOwner(w.updated_by as Record<string, unknown> | undefined),
};
}
48 changes: 47 additions & 1 deletion src/commands/apikeys/apikeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { apiKeysCommand } from './index.js';
import { apiKeysCommand, summarizeApiKeyRow } from './index.js';
import {
getAPIClientFromOptions,
getGlobalOptions,
Expand Down Expand Up @@ -105,3 +105,49 @@ describe('apikeys command', () => {
expect(mockClient.deleteApiKey).toHaveBeenCalledWith(5);
});
});

describe('summarizeApiKeyRow', () => {
it('picks curated columns and flattens created_by / updated_by', () => {
const raw = {
id: 36,
name: 'test new key',
description: 'new key',
hashed_key: 'TEST_HASHED_KEY_PLACEHOLDER',
key_ending: 'ouNy',
permissions: '',
used_at: null,
created_at: '2024-05-22T10:29:34.375Z',
updated_at: null,
created_by: { id: 4, first_name: 'Márcio', last_name: 'Martins', email: 'm@x' },
updated_by: null,
};
expect(summarizeApiKeyRow(raw)).toEqual({
id: 36,
name: 'test new key',
description: 'new key',
key_ending: 'ouNy',
permissions: '',
used_at: '',
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Márcio Martins',
updated_at: '',
updated_by: '',
});
});

it('returns empty strings for missing optional fields', () => {
const raw = { id: 1, name: 'k', key_ending: 'xxxx' };
expect(summarizeApiKeyRow(raw)).toEqual({
id: 1,
name: 'k',
description: '',
key_ending: 'xxxx',
permissions: '',
used_at: '',
created_at: '',
created_by: '',
updated_at: '',
updated_by: '',
});
});
});
18 changes: 18 additions & 0 deletions src/commands/apikeys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ import {
} from '../../lib/utils/api-helper.js';
import { parseApiKeyId } from '../../lib/utils/validators.js';
import { createListCommand } from '../../lib/utils/list-command.js';
import { formatUserSummary } from '../../lib/output/formatter.js';
import type { ApiKeyId } from '../../lib/api/branded-types.js';
import { getApiKey, createApiKey, updateApiKey, deleteApiKey } from '../../core/apikeys/index.js';

export const apiKeysCommand = new Command('api-keys')
.aliases(['apikeys', 'apikey', 'api-key'])
.description('API key commands');

export function summarizeApiKeyRow(item: Record<string, unknown>): Record<string, unknown> {
const summarizeUser = (v: unknown) => formatUserSummary(v) ?? '';
return {
id: item.id,
name: item.name ?? '',
description: item.description ?? '',
key_ending: item.key_ending ?? '',
permissions: item.permissions ?? '',
used_at: item.used_at ?? '',
created_at: item.created_at ?? '',
created_by: summarizeUser(item.created_by),
updated_at: item.updated_at ?? '',
updated_by: summarizeUser(item.updated_by),
};
}

const listCommand = createListCommand({
description: 'List all API keys',
fetch: (client, options) =>
Expand All @@ -28,6 +45,7 @@ const listCommand = createListCommand({
archived: options.archived as boolean,
ids: options.ids as string | undefined,
}),
summarizeRow: summarizeApiKeyRow,
});

const getCommand = new Command('get')
Expand Down
13 changes: 11 additions & 2 deletions src/commands/apps/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ describe('apps command', () => {
let processExitSpy: ReturnType<typeof vi.spyOn>;

const mockClient = {
listApplications: vi.fn().mockResolvedValue([{ id: 1, name: 'web' }]),
listApplications: vi.fn().mockResolvedValue([
{
id: 1,
name: 'web',
created_by: { first_name: 'Ada', last_name: 'Lovelace', email: 'ada@example.com' },
},
]),
getApplication: vi.fn().mockResolvedValue({ id: 1, name: 'web' }),
createApplication: vi.fn().mockResolvedValue({ id: 3, name: 'new-app' }),
updateApplication: vi.fn().mockResolvedValue({ id: 1, name: 'updated' }),
Expand Down Expand Up @@ -53,7 +59,10 @@ describe('apps command', () => {
await appsCommand.parseAsync(['node', 'test', 'list']);

expect(mockClient.listApplications).toHaveBeenCalled();
expect(printFormatted).toHaveBeenCalledWith([{ id: 1, name: 'web' }], expect.anything());
expect(printFormatted).toHaveBeenCalledWith(
[expect.objectContaining({ id: 1, name: 'web', created_by: 'Ada Lovelace' })],
expect.anything()
);
});

it('should get application by id', async () => {
Expand Down
11 changes: 2 additions & 9 deletions src/commands/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../lib/utils/api-helper.js';
import { parseApplicationId } from '../../lib/utils/validators.js';
import { createListCommand } from '../../lib/utils/list-command.js';
import { formatUserSummary } from '../../lib/output/formatter.js';
import type { ApplicationId } from '../../lib/api/branded-types.js';
import { getApp, createApp, updateApp, archiveApp } from '../../core/apps/index.js';

Expand Down Expand Up @@ -35,15 +36,7 @@ const listCommand = createListCommand({
description: item.description,
archived: item.archived,
created_at: item.created_at,
created_by:
item.created_by && typeof item.created_by === 'object'
? [
(item.created_by as Record<string, unknown>).first_name ?? '',
(item.created_by as Record<string, unknown>).last_name ?? '',
]
.filter(Boolean)
.join(' ')
: item.created_by,
created_by: formatUserSummary(item.created_by) ?? '',
}),
});

Expand Down
Loading
Loading