diff --git a/src/api-client/entity-summary.test.ts b/src/api-client/entity-summary.test.ts index 2fc24f2..305225b 100644 --- a/src/api-client/entity-summary.test.ts +++ b/src/api-client/entity-summary.test.ts @@ -11,6 +11,10 @@ import { summarizeUserDetail, summarizeSegment, summarizeSegmentRow, + summarizeTagRow, + summarizeMetricCategoryRow, + summarizeNamedEntityRow, + summarizeWebhookRow, } from './entity-summary.js'; describe('applyShowExclude', () => { @@ -190,3 +194,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: '', + }); + }); +}); diff --git a/src/api-client/entity-summary.ts b/src/api-client/entity-summary.ts index 8a2f72f..7f922fe 100644 --- a/src/api-client/entity-summary.ts +++ b/src/api-client/entity-summary.ts @@ -1,4 +1,5 @@ import { formatDate } from './format-helpers.js'; +import { formatUserSummary } from '../lib/output/formatter.js'; export function applyShowExclude( summary: Record, @@ -19,9 +20,7 @@ export function applyShowExclude( function formatOwner(obj: Record | 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): Record { @@ -204,3 +203,58 @@ export function summarizeCustomField(f: Record): Record): Record { + return { + id: t.id, + tag: t.tag ?? '', + archived: t.archived ?? false, + created_at: t.created_at ?? '', + created_by: formatOwner(t.created_by as Record | undefined), + updated_at: t.updated_at ?? '', + updated_by: formatOwner(t.updated_by as Record | undefined), + }; +} + +export function summarizeMetricCategoryRow(c: Record): Record { + 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 | undefined), + updated_at: c.updated_at ?? '', + updated_by: formatOwner(c.updated_by as Record | undefined), + }; +} + +export function summarizeNamedEntityRow(e: Record): Record { + 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 | undefined), + updated_at: e.updated_at ?? '', + updated_by: formatOwner(e.updated_by as Record | undefined), + }; +} + +export function summarizeWebhookRow(w: Record): Record { + 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 | undefined), + updated_at: w.updated_at ?? '', + updated_by: formatOwner(w.updated_by as Record | undefined), + }; +} diff --git a/src/commands/apikeys/apikeys.test.ts b/src/commands/apikeys/apikeys.test.ts index 902662c..d6c1c63 100644 --- a/src/commands/apikeys/apikeys.test.ts +++ b/src/commands/apikeys/apikeys.test.ts @@ -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, @@ -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: 'X99upHMkKKM2XUpRuZ53YWCDx5G93SgB043EXx3LG5k=', + 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: '', + }); + }); +}); diff --git a/src/commands/apikeys/index.ts b/src/commands/apikeys/index.ts index af8437e..ae4d1f4 100644 --- a/src/commands/apikeys/index.ts +++ b/src/commands/apikeys/index.ts @@ -9,6 +9,7 @@ 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'; @@ -16,6 +17,22 @@ export const apiKeysCommand = new Command('api-keys') .aliases(['apikeys', 'apikey', 'api-key']) .description('API key commands'); +export function summarizeApiKeyRow(item: Record): Record { + 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) => @@ -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') diff --git a/src/commands/apps/apps.test.ts b/src/commands/apps/apps.test.ts index 2876ece..fe0d602 100644 --- a/src/commands/apps/apps.test.ts +++ b/src/commands/apps/apps.test.ts @@ -53,7 +53,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' })], + expect.anything() + ); }); it('should get application by id', async () => { diff --git a/src/commands/apps/index.ts b/src/commands/apps/index.ts index d277de0..3854c80 100644 --- a/src/commands/apps/index.ts +++ b/src/commands/apps/index.ts @@ -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'; @@ -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).first_name ?? '', - (item.created_by as Record).last_name ?? '', - ] - .filter(Boolean) - .join(' ') - : item.created_by, + created_by: formatUserSummary(item.created_by) ?? '', }), }); diff --git a/src/commands/envs/envs.test.ts b/src/commands/envs/envs.test.ts index 771f7ec..67570bc 100644 --- a/src/commands/envs/envs.test.ts +++ b/src/commands/envs/envs.test.ts @@ -53,7 +53,21 @@ describe('envs command', () => { await envsCommand.parseAsync(['node', 'test', 'list']); expect(mockClient.listEnvironments).toHaveBeenCalled(); - expect(printFormatted).toHaveBeenCalledWith([{ id: 1, name: 'production' }], expect.anything()); + expect(printFormatted).toHaveBeenCalledWith( + [ + { + id: 1, + name: 'production', + description: '', + archived: false, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + }, + ], + expect.anything() + ); }); it('should get environment by id', async () => { diff --git a/src/commands/envs/index.ts b/src/commands/envs/index.ts index 7114e9e..3ea4111 100644 --- a/src/commands/envs/index.ts +++ b/src/commands/envs/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseEnvironmentId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeNamedEntityRow } from '../../api-client/entity-summary.js'; import type { EnvironmentId } from '../../lib/api/branded-types.js'; import { getEnv, createEnv, updateEnv, archiveEnv } from '../../core/envs/index.js'; @@ -29,6 +30,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeNamedEntityRow, }); const getCommand = new Command('get') diff --git a/src/commands/goaltags/index.ts b/src/commands/goaltags/index.ts index 81f4f3e..e6aca1e 100644 --- a/src/commands/goaltags/index.ts +++ b/src/commands/goaltags/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseTagId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeTagRow } from '../../api-client/entity-summary.js'; import type { TagId } from '../../lib/api/branded-types.js'; import { getGoalTag, @@ -34,6 +35,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeTagRow, }); const getCommand = new Command('get') diff --git a/src/commands/metriccategories/index.ts b/src/commands/metriccategories/index.ts index 0c68324..df633f2 100644 --- a/src/commands/metriccategories/index.ts +++ b/src/commands/metriccategories/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseTagId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeMetricCategoryRow } from '../../api-client/entity-summary.js'; import type { TagId } from '../../lib/api/branded-types.js'; import { getMetricCategory, @@ -35,6 +36,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeMetricCategoryRow, }); const getCommand = new Command('get') diff --git a/src/commands/metrictags/index.ts b/src/commands/metrictags/index.ts index 0e20273..21fdd68 100644 --- a/src/commands/metrictags/index.ts +++ b/src/commands/metrictags/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseTagId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeTagRow } from '../../api-client/entity-summary.js'; import type { TagId } from '../../lib/api/branded-types.js'; import { getMetricTag, @@ -34,6 +35,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeTagRow, }); const getCommand = new Command('get') diff --git a/src/commands/roles/index.ts b/src/commands/roles/index.ts index 9677a50..094f308 100644 --- a/src/commands/roles/index.ts +++ b/src/commands/roles/index.ts @@ -10,6 +10,7 @@ import { parseRoleId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; import type { RoleId } from '../../lib/api/branded-types.js'; import { getRole, createRole, updateRole, deleteRole } from '../../core/roles/index.js'; +import { summarizeNamedEntityRow } from '../../api-client/entity-summary.js'; export const rolesCommand = new Command('roles').alias('role').description('Role commands'); @@ -25,6 +26,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeNamedEntityRow, }); const getCommand = new Command('get') diff --git a/src/commands/roles/roles.test.ts b/src/commands/roles/roles.test.ts index fe72676..287cf38 100644 --- a/src/commands/roles/roles.test.ts +++ b/src/commands/roles/roles.test.ts @@ -6,6 +6,7 @@ import { printFormatted, } from '../../lib/utils/api-helper.js'; import { resetCommand } from '../../test/helpers/command-reset.js'; +import { summarizeNamedEntityRow } from '../../api-client/entity-summary.js'; vi.mock('../../lib/utils/api-helper.js', async (importOriginal) => { const actual = await importOriginal(); @@ -88,3 +89,17 @@ describe('roles command', () => { expect(output).toContain('Role 1 deleted'); }); }); + +describe('roles list summarizer', () => { + it('uses summarizeNamedEntityRow for clean columns', () => { + expect( + summarizeNamedEntityRow({ + id: 1, + name: 'admin', + description: 'all powerful', + archived: false, + created_by: { email: 'root@x' }, + }).created_by + ).toBe('root@x'); + }); +}); diff --git a/src/commands/tags/index.ts b/src/commands/tags/index.ts index 9abc3b3..09504f9 100644 --- a/src/commands/tags/index.ts +++ b/src/commands/tags/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseTagId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeTagRow } from '../../api-client/entity-summary.js'; import type { TagId } from '../../lib/api/branded-types.js'; import { getTag, createTag, updateTag, deleteTag } from '../../core/tags/index.js'; @@ -28,6 +29,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeTagRow, }); const getCommand = new Command('get') diff --git a/src/commands/units/index.ts b/src/commands/units/index.ts index fcade15..a41c07b 100644 --- a/src/commands/units/index.ts +++ b/src/commands/units/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseUnitTypeId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeNamedEntityRow } from '../../api-client/entity-summary.js'; import type { UnitTypeId } from '../../lib/api/branded-types.js'; import { getUnit, createUnit, updateUnit, archiveUnit } from '../../core/units/index.js'; @@ -26,6 +27,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeNamedEntityRow, }); const getCommand = new Command('get') diff --git a/src/commands/units/units.test.ts b/src/commands/units/units.test.ts index 3dd1d5b..0837c5b 100644 --- a/src/commands/units/units.test.ts +++ b/src/commands/units/units.test.ts @@ -53,7 +53,21 @@ describe('units command', () => { await unitsCommand.parseAsync(['node', 'test', 'list']); expect(mockClient.listUnitTypes).toHaveBeenCalled(); - expect(printFormatted).toHaveBeenCalledWith([{ id: 1, name: 'user_id' }], expect.anything()); + expect(printFormatted).toHaveBeenCalledWith( + [ + { + id: 1, + name: 'user_id', + description: '', + archived: false, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + }, + ], + expect.anything() + ); }); it('should get unit type by id', async () => { diff --git a/src/commands/webhooks/index.ts b/src/commands/webhooks/index.ts index 4d78a9b..2381867 100644 --- a/src/commands/webhooks/index.ts +++ b/src/commands/webhooks/index.ts @@ -8,6 +8,7 @@ import { } from '../../lib/utils/api-helper.js'; import { parseWebhookId } from '../../lib/utils/validators.js'; import { createListCommand } from '../../lib/utils/list-command.js'; +import { summarizeWebhookRow } from '../../api-client/entity-summary.js'; import type { WebhookId } from '../../lib/api/branded-types.js'; import { getWebhook } from '../../core/webhooks/get.js'; import { createWebhook } from '../../core/webhooks/create.js'; @@ -31,6 +32,7 @@ const listCommand = createListCommand({ archived: options.archived as boolean, ids: options.ids as string | undefined, }), + summarizeRow: summarizeWebhookRow, }); const getCommand = new Command('get') diff --git a/src/lib/output/formatter.test.ts b/src/lib/output/formatter.test.ts index 87c6eb4..af17f30 100644 --- a/src/lib/output/formatter.test.ts +++ b/src/lib/output/formatter.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { formatOutput, formatValue, truncateText } from './formatter.js'; +import { summarizeObjectValue, formatUserSummary } from './formatter.js'; describe('Output Formatter', () => { describe('formatValue', () => { @@ -40,6 +41,70 @@ describe('Output Formatter', () => { }); }); + describe('formatUserSummary', () => { + it('returns "First Last" when both names are present', () => { + expect(formatUserSummary({ first_name: 'Joe', last_name: 'Bloggs' })).toBe('Joe Bloggs'); + }); + + it('returns the email when names are missing', () => { + expect(formatUserSummary({ email: 'joe@example.com' })).toBe('joe@example.com'); + }); + + it('prefers full name over email when both are present', () => { + expect( + formatUserSummary({ first_name: 'Joe', last_name: 'Bloggs', email: 'joe@example.com' }) + ).toBe('Joe Bloggs'); + }); + + it('returns just the first name if last is missing', () => { + expect(formatUserSummary({ first_name: 'Joe' })).toBe('Joe'); + }); + + it('returns null when the value is not user-shaped', () => { + expect(formatUserSummary({ id: 1, name: 'thing' })).toBe(null); + expect(formatUserSummary({})).toBe(null); + expect(formatUserSummary(null)).toBe(null); + }); + }); + + describe('summarizeObjectValue', () => { + it('summarizes user-shaped objects via formatUserSummary', () => { + expect(summarizeObjectValue({ first_name: 'Joe', last_name: 'Bloggs', id: 4 })).toBe( + 'Joe Bloggs' + ); + }); + + it('falls back to the name field when no user shape', () => { + expect(summarizeObjectValue({ id: 7, name: 'Revenue' })).toBe('Revenue'); + }); + + it('falls back to the title field when there is no name', () => { + expect(summarizeObjectValue({ id: 9, title: 'Owner' })).toBe('Owner'); + }); + + it('renders an empty object as [object]', () => { + expect(summarizeObjectValue({})).toBe('[object]'); + }); + + it('renders an object with only opaque fields as [object]', () => { + expect(summarizeObjectValue({ foo: 1, bar: 2 })).toBe('[object]'); + }); + }); + + describe('formatValue (nested object handling)', () => { + it('summarizes nested user objects instead of JSON.stringify-ing them', () => { + expect(formatValue({ first_name: 'Joe', last_name: 'Bloggs' })).toBe('Joe Bloggs'); + }); + + it('summarizes nested named objects instead of JSON.stringify-ing them', () => { + expect(formatValue({ id: 1, name: 'Revenue' })).toBe('Revenue'); + }); + + it('renders other nested objects as [object] instead of JSON', () => { + expect(formatValue({ foo: 1, bar: 2 })).toBe('[object]'); + }); + }); + describe('truncateText', () => { it('should not truncate short text', () => { const text = 'short'; diff --git a/src/lib/output/formatter.ts b/src/lib/output/formatter.ts index 09c9750..8c741e0 100644 --- a/src/lib/output/formatter.ts +++ b/src/lib/output/formatter.ts @@ -237,6 +237,25 @@ function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +export function formatUserSummary(value: unknown): string | null { + if (!isObject(value)) return null; + const first = typeof value.first_name === 'string' ? value.first_name : ''; + const last = typeof value.last_name === 'string' ? value.last_name : ''; + const fullName = [first, last].filter(Boolean).join(' '); + if (fullName) return fullName; + if (typeof value.email === 'string' && value.email) return value.email; + return null; +} + +export function summarizeObjectValue(value: unknown): string { + if (!isObject(value)) return ''; + const user = formatUserSummary(value); + if (user !== null) return user; + if (typeof value.name === 'string' && value.name) return value.name; + if (typeof value.title === 'string' && value.title) return value.title; + return '[object]'; +} + export function formatValue(value: unknown, options: OutputOptions = {}): string { if (value === null || value === undefined) return ''; if (typeof value === 'boolean') return String(value); @@ -249,7 +268,7 @@ export function formatValue(value: unknown, options: OutputOptions = {}): string return text; } if (Array.isArray(value)) return value.map((v) => formatValue(v, options)).join(', '); - if (isObject(value)) return JSON.stringify(value); + if (isObject(value)) return summarizeObjectValue(value); return String(value); }