diff --git a/src/__tests__/lib/command/util/apps-util.test.ts b/src/__tests__/lib/command/util/apps-util.test.ts index 716ecf2a..ca86474a 100644 --- a/src/__tests__/lib/command/util/apps-util.test.ts +++ b/src/__tests__/lib/command/util/apps-util.test.ts @@ -10,16 +10,17 @@ import { } from '@smartthings/core-sdk' import type { addPermission } from '../../../../lib/aws-util.js' -import { +import type { PropertyTableFieldDefinition, TableGenerator, ValueTableFieldDefinition, } from '../../../../lib/table-generator.js' import type { fatalError } from '../../../../lib/util.js' -import { stringTranslateToId } from '../../../../lib/command/command-util.js' -import { +import type { APICommand } from '../../../../lib/command/api-command.js' +import type { stringTranslateToId } from '../../../../lib/command/command-util.js' +import type { createChooseFn, - type ChooseFunction, + ChooseFunction, } from '../../../../lib/command/util/util-util.js' import { mockedTableOutput, @@ -181,13 +182,15 @@ test('chooseAppFn uses correct endpoint to list apps', async () => { const apiAppsListMock = jest.fn() .mockResolvedValueOnce(appList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - apps: { - list: apiAppsListMock, + const command = { + client: { + apps: { + list: apiAppsListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(appList) + expect(await listItems(command)).toBe(appList) expect(apiAppsListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/devicepreferences-util.test.ts b/src/__tests__/lib/command/util/devicepreferences-util.test.ts index 8e655584..62150bcb 100644 --- a/src/__tests__/lib/command/util/devicepreferences-util.test.ts +++ b/src/__tests__/lib/command/util/devicepreferences-util.test.ts @@ -1,12 +1,9 @@ import { jest } from '@jest/globals' -import type { - DevicePreference, - DevicePreferencesEndpoint, - SmartThingsClient, -} from '@smartthings/core-sdk' +import type { DevicePreference, DevicePreferencesEndpoint } from '@smartthings/core-sdk' import type { ValueTableFieldDefinition } from '../../../../lib/table-generator.js' +import type { APICommand } from '../../../../lib/command/api-command.js' import type { ChooseFunction, createChooseFn } from '../../../../lib/command/util/util-util.js' @@ -58,13 +55,15 @@ test('chooseDevicePreference', async () => { const apiDevicePreferencesListMock = jest.fn() .mockResolvedValueOnce(devicePreferenceList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - devicePreferences: { - list: apiDevicePreferencesListMock, + const command = { + client: { + devicePreferences: { + list: apiDevicePreferencesListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(devicePreferenceList) + expect(await listItems(command)).toBe(devicePreferenceList) expect(apiDevicePreferencesListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/deviceprofiles-choose.test.ts b/src/__tests__/lib/command/util/deviceprofiles-choose.test.ts index 5c223a52..83a35d35 100644 --- a/src/__tests__/lib/command/util/deviceprofiles-choose.test.ts +++ b/src/__tests__/lib/command/util/deviceprofiles-choose.test.ts @@ -4,11 +4,11 @@ import type { DeviceProfile, DeviceProfilesEndpoint, LocaleReference, - SmartThingsClient, } from '@smartthings/core-sdk' import type { WithLocales } from '../../../../lib/api-helpers.js' -import { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' +import type { APICommand } from '../../../../lib/command/api-command.js' +import { type createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' const createChooseFnMock = jest.fn>() @@ -29,12 +29,14 @@ describe('chooseDeviceProfileFn', () => { const locales = [{ tag: 'es_MX' }, { tag: 'en_CA' }, { tag: 'fr_CA' }] as LocaleReference[] const apiDeviceProfilesListLocalesMock = jest.fn() .mockResolvedValue(locales) - const client = { - deviceProfiles: { - list: apiDeviceProfilesListMock, - listLocales: apiDeviceProfilesListLocalesMock, + const command = { + client: { + deviceProfiles: { + list: apiDeviceProfilesListMock, + listLocales: apiDeviceProfilesListLocalesMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand it('uses correct endpoint to list device profiles', async () => { const chooseDeviceProfile = chooseDeviceProfileFn() @@ -51,7 +53,7 @@ describe('chooseDeviceProfileFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toBe(deviceProfiles) + expect(await listItems(command)).toBe(deviceProfiles) expect(apiDeviceProfilesListMock).toHaveBeenCalledExactlyOnceWith() @@ -73,7 +75,7 @@ describe('chooseDeviceProfileFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toStrictEqual([{ + expect(await listItems(command)).toStrictEqual([{ id: 'device-profile-id', locales: 'en_CA, es_MX, fr_CA', }]) @@ -99,7 +101,7 @@ describe('chooseDeviceProfileFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] apiDeviceProfilesListLocalesMock.mockImplementationOnce(() => { throw { message: 'status code 404' } }) - expect(await listItems(client)).toStrictEqual([{ + expect(await listItems(command)).toStrictEqual([{ id: 'device-profile-id', }]) @@ -123,7 +125,7 @@ describe('chooseDeviceProfileFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] apiDeviceProfilesListLocalesMock.mockImplementationOnce(() => { throw Error('other error') }) - await expect(listItems(client)).rejects.toThrow('other error') + await expect(listItems(command)).rejects.toThrow('other error') expect(apiDeviceProfilesListMock).toHaveBeenCalledExactlyOnceWith() expect(apiDeviceProfilesListLocalesMock).toHaveBeenCalledExactlyOnceWith('device-profile-id') diff --git a/src/__tests__/lib/command/util/devices-choose.test.ts b/src/__tests__/lib/command/util/devices-choose.test.ts index 3ee24f22..99aee607 100644 --- a/src/__tests__/lib/command/util/devices-choose.test.ts +++ b/src/__tests__/lib/command/util/devices-choose.test.ts @@ -5,16 +5,15 @@ import type { Device, DeviceListOptions, DevicesEndpoint, - SmartThingsClient, } from '@smartthings/core-sdk' -import { APICommand } from '../../../../lib/command/api-command.js' +import type { ValueTableFieldDefinition } from '../../../../lib/table-generator.js' +import type { fatalError } from '../../../../lib/util.js' +import type { APICommand } from '../../../../lib/command/api-command.js' import type { stringTranslateToId } from '../../../../lib/command/command-util.js' -import { TableCommonListOutputProducer } from '../../../../lib/command/format.js' -import { BuildOutputFormatterFlags } from '../../../../lib/command/output-builder.js' +import type { TableCommonListOutputProducer } from '../../../../lib/command/format.js' +import type { BuildOutputFormatterFlags } from '../../../../lib/command/output-builder.js' import type { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' -import { ValueTableFieldDefinition } from '../../../../lib/table-generator.js' -import { fatalError } from '../../../../lib/util.js' const stringTranslateToIdMock = jest.fn() @@ -43,11 +42,13 @@ describe('chooseDeviceFn', () => { const deviceList = [{ deviceId: 'listed-device-id' } as Device] const apiDevicesListMock = jest.fn() .mockResolvedValue(deviceList) - const client = { - devices: { - list: apiDevicesListMock, + const command = { + client: { + devices: { + list: apiDevicesListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand it('uses correct endpoint to list devices', async () => { createChooseFnMock.mockReturnValueOnce(chooseDeviceMock) @@ -63,7 +64,7 @@ describe('chooseDeviceFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toBe(deviceList) + expect(await listItems(command)).toBe(deviceList) expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith() }) @@ -83,7 +84,7 @@ describe('chooseDeviceFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toBe(deviceList) + expect(await listItems(command)).toBe(deviceList) expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith(deviceListOptions) }) @@ -111,8 +112,8 @@ describe('chooseComponentFn', () => { const listItems = createChooseFnMockForComponent.mock.calls[0][1] - const client = {} as unknown as SmartThingsClient - expect(await listItems(client)).toBe(components) + const command = { client: {} } as unknown as APICommand + expect(await listItems(command)).toBe(components) }) it('includes " (default)" for main component', async () => { diff --git a/src/__tests__/lib/command/util/edge/channels-choose.test.ts b/src/__tests__/lib/command/util/edge/channels-choose.test.ts index f4ce73ed..f8d0cc66 100644 --- a/src/__tests__/lib/command/util/edge/channels-choose.test.ts +++ b/src/__tests__/lib/command/util/edge/channels-choose.test.ts @@ -121,9 +121,19 @@ describe('chooseChannel', () => { )).toBe('chosen-channel-id') expect(chooseOptionsWithDefaultsMock).toHaveBeenCalledExactlyOnceWith({ listItems: listItemsMock }) - expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith(command, + expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( + command, expect.objectContaining({ primaryKeyName: 'channelId', sortKeyName: 'name' }), - expect.objectContaining({ listItems: listItemsMock })) + expect.objectContaining({ listItems: expect.any(Function) }), + ) + + const listItems = selectFromListMock.mock.calls[0][2].listItems + const channels = [channel] + listItemsMock.mockResolvedValueOnce(channels) + + expect(await listItems()).toBe(channels) + + expect(listItemsMock).toHaveBeenCalledExactlyOnceWith(command) }) it('defaults to listChannels for listing channels', async () => { diff --git a/src/__tests__/lib/command/util/edge/drivers-choose.test.ts b/src/__tests__/lib/command/util/edge/drivers-choose.test.ts index 3e066f4e..5c8de6d2 100644 --- a/src/__tests__/lib/command/util/edge/drivers-choose.test.ts +++ b/src/__tests__/lib/command/util/edge/drivers-choose.test.ts @@ -1,9 +1,11 @@ import { jest } from '@jest/globals' +import type { EdgeDriver } from '@smartthings/core-sdk' + +import type { APICommand } from '../../../../../lib/command/api-command.js' import type { DriverChoice } from '../../../../../lib/command/util/drivers-choose.js' import type { ChooseFunction, createChooseFn } from '../../../../../lib/command/util/util-util.js' import type { listDrivers } from '../../../../../lib/command/util/edge/drivers-util.js' -import { EdgeDriver, SmartThingsClient } from '@smartthings/core-sdk' const createChooseFnMock = jest.fn>() @@ -26,7 +28,7 @@ describe('createDriverFn', () => { const drivers = [] as EdgeDriver[] - const client = { drivers: {} } as SmartThingsClient + const command = { client: { drivers: {} } } as APICommand it('does not include all organizations by default', async () => { expect(chooseDriverFn()).toBe(createDriverMock) @@ -40,9 +42,9 @@ describe('createDriverFn', () => { listDriversMock.mockResolvedValueOnce(drivers) - expect(await listItems(client)).toBe(drivers) + expect(await listItems(command)).toBe(drivers) - expect(listDriversMock).toHaveBeenCalledExactlyOnceWith(client, undefined) + expect(listDriversMock).toHaveBeenCalledExactlyOnceWith(command.client, undefined) }) it('requests all organizations when specified', async () => { @@ -57,8 +59,8 @@ describe('createDriverFn', () => { listDriversMock.mockResolvedValueOnce(drivers) - expect(await listItems(client)).toBe(drivers) + expect(await listItems(command)).toBe(drivers) - expect(listDriversMock).toHaveBeenCalledExactlyOnceWith(client, true) + expect(listDriversMock).toHaveBeenCalledExactlyOnceWith(command.client, true) }) }) diff --git a/src/__tests__/lib/command/util/hubs-choose.test.ts b/src/__tests__/lib/command/util/hubs-choose.test.ts index cce64fb6..1ce20ff6 100644 --- a/src/__tests__/lib/command/util/hubs-choose.test.ts +++ b/src/__tests__/lib/command/util/hubs-choose.test.ts @@ -1,7 +1,8 @@ import { jest } from '@jest/globals' -import { DeviceIntegrationType, type Device, type DevicesEndpoint, type SmartThingsClient } from '@smartthings/core-sdk' +import { DeviceIntegrationType, type Device, type DevicesEndpoint } from '@smartthings/core-sdk' +import { APICommand } from '../../../../lib/command/api-command.js' import { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' @@ -20,11 +21,13 @@ describe('chooseHubFn', () => { const devices = [{ deviceId: 'hub-device-id' } as Device] const apiDevicesListMock = jest.fn() .mockResolvedValue(devices) - const client = { - devices: { - list: apiDevicesListMock, + const command = { + client: { + devices: { + list: apiDevicesListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand it('limits to hub devices', async () => { const chooseHub = chooseHubFn() @@ -40,7 +43,7 @@ describe('chooseHubFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toBe(devices) + expect(await listItems(command)).toBe(devices) expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith( { type: DeviceIntegrationType.HUB, locationId: undefined }, @@ -54,7 +57,7 @@ describe('chooseHubFn', () => { const listItems = createChooseFnMock.mock.calls[0][1] - expect(await listItems(client)).toBe(devices) + expect(await listItems(command)).toBe(devices) expect(apiDevicesListMock).toHaveBeenCalledExactlyOnceWith( { type: DeviceIntegrationType.HUB, locationId: 'location-filter-id' }, diff --git a/src/__tests__/lib/command/util/installedapps-util.test.ts b/src/__tests__/lib/command/util/installedapps-util.test.ts index dfe2d8bf..76127b43 100644 --- a/src/__tests__/lib/command/util/installedapps-util.test.ts +++ b/src/__tests__/lib/command/util/installedapps-util.test.ts @@ -1,8 +1,9 @@ import { jest } from '@jest/globals' -import { InstalledApp, InstalledAppListOptions, InstalledAppsEndpoint, type SmartThingsClient } from '@smartthings/core-sdk' +import { InstalledApp, InstalledAppListOptions, InstalledAppsEndpoint } from '@smartthings/core-sdk' import type { WithLocation, withLocations, WithNamedLocation } from '../../../../lib/api-helpers.js' +import type { APICommand } from '../../../../lib/command/api-command.js' import type { ChooseFunction, createChooseFn } from '../../../../lib/command/util/util-util.js' @@ -26,11 +27,13 @@ describe('chooseInstalledAppFn', () => { const installedApps = [installedApp1, installedApp2] const apiInstalledAppsListMock = jest.fn() .mockResolvedValue(installedApps) - const client = { - installedApps: { - list: apiInstalledAppsListMock, + const command = { + client: { + installedApps: { + list: apiInstalledAppsListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand const chooseInstalledAppMock = jest.fn>() createChooseFnMock.mockReturnValue(chooseInstalledAppMock) @@ -48,7 +51,7 @@ describe('chooseInstalledAppFn', () => { const listFunction = createChooseFnMock.mock.calls[0][1] - expect(await listFunction(client)).toBe(installedApps) + expect(await listFunction(command)).toBe(installedApps) expect(apiInstalledAppsListMock).toHaveBeenCalledExactlyOnceWith(undefined) expect(withLocationsMock).not.toHaveBeenCalled() @@ -74,9 +77,9 @@ describe('chooseInstalledAppFn', () => { ] withLocationsMock.mockResolvedValue(installedAppsWithLocations) - expect(await listFunction(client)).toBe(installedAppsWithLocations) + expect(await listFunction(command)).toBe(installedAppsWithLocations) expect(apiInstalledAppsListMock).toHaveBeenCalledExactlyOnceWith(listOptions) - expect(withLocationsMock).toHaveBeenCalledExactlyOnceWith(client, installedApps) + expect(withLocationsMock).toHaveBeenCalledExactlyOnceWith(command.client, installedApps) }) }) diff --git a/src/__tests__/lib/command/util/locations-util.test.ts b/src/__tests__/lib/command/util/locations-util.test.ts index 18686aa2..e940421a 100644 --- a/src/__tests__/lib/command/util/locations-util.test.ts +++ b/src/__tests__/lib/command/util/locations-util.test.ts @@ -1,11 +1,9 @@ import { jest } from '@jest/globals' -import { LocationItem, LocationsEndpoint, SmartThingsClient } from '@smartthings/core-sdk' -import { stringTranslateToId } from '../../../../lib/command/command-util.js' -import { - createChooseFn, - type ChooseFunction, -} from '../../../../lib/command/util/util-util.js' +import type { LocationItem, LocationsEndpoint } from '@smartthings/core-sdk' +import type { stringTranslateToId } from '../../../../lib/command/command-util.js' +import type { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' +import { APICommand } from '../../../../lib/command/api-command.js' const stringTranslateToIdMock = jest.fn() @@ -38,13 +36,15 @@ test('chooseLocationFn uses correct endpoint to list locations', async () => { const apiLocationsListMock = jest.fn() .mockResolvedValueOnce(locationList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - locations: { - list: apiLocationsListMock, + const command = { + client: { + locations: { + list: apiLocationsListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(locationList) + expect(await listItems(command)).toBe(locationList) expect(apiLocationsListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/organizations-util.test.ts b/src/__tests__/lib/command/util/organizations-util.test.ts index ec6163c2..6d6d4d70 100644 --- a/src/__tests__/lib/command/util/organizations-util.test.ts +++ b/src/__tests__/lib/command/util/organizations-util.test.ts @@ -1,18 +1,10 @@ import { jest } from '@jest/globals' -import type { - OrganizationResponse, - OrganizationsEndpoint, - SmartThingsClient, -} from '@smartthings/core-sdk' +import type { OrganizationResponse, OrganizationsEndpoint } from '@smartthings/core-sdk' -import type { - ValueTableFieldDefinition, -} from '../../../../lib/table-generator.js' -import type { - createChooseFn, - ChooseFunction, -} from '../../../../lib/command/util/util-util.js' +import type { ValueTableFieldDefinition } from '../../../../lib/table-generator.js' +import type { APICommand } from '../../../../lib/command/api-command.js' +import type { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' import type { InputDefinition, selectDef, @@ -97,13 +89,15 @@ test('chooseOrganizationFn uses correct endpoint to list organizations', async ( const apiOrganizationsListMock = jest.fn() .mockResolvedValueOnce(organizationList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - organizations: { - list: apiOrganizationsListMock, + const command = { + client: { + organizations: { + list: apiOrganizationsListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(organizationList) + expect(await listItems(command)).toBe(organizationList) expect(apiOrganizationsListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/rules-choose.test.ts b/src/__tests__/lib/command/util/rules-choose.test.ts index 4413f50e..d1c1280b 100644 --- a/src/__tests__/lib/command/util/rules-choose.test.ts +++ b/src/__tests__/lib/command/util/rules-choose.test.ts @@ -1,9 +1,10 @@ import { jest } from '@jest/globals' -import { Rule, SmartThingsClient } from '@smartthings/core-sdk' +import type { Rule } from '@smartthings/core-sdk' import type { WithLocation } from '../../../../lib/api-helpers.js' -import { getRulesByLocation } from '../../../../lib/command/util/rules-util.js' +import type { APICommand } from '../../../../lib/command/api-command.js' +import type { getRulesByLocation } from '../../../../lib/command/util/rules-util.js' import type { createChooseFn, ChooseFunction } from '../../../../lib/command/util/util-util.js' @@ -39,9 +40,9 @@ test('chooseRuleFn', async () => { const ruleList = [{ id: 'listed-rule-id' } as Rule & WithLocation] getRulesByLocationMock.mockResolvedValueOnce(ruleList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { rules: {} } as SmartThingsClient + const command = { client: { rules: {} } } as APICommand - expect(await listItems(client)).toBe(ruleList) + expect(await listItems(command)).toBe(ruleList) - expect(getRulesByLocationMock).toHaveBeenCalledExactlyOnceWith(client, 'location-id') + expect(getRulesByLocationMock).toHaveBeenCalledExactlyOnceWith(command.client, 'location-id') }) diff --git a/src/__tests__/lib/command/util/scenes-util.test.ts b/src/__tests__/lib/command/util/scenes-util.test.ts index bb970db0..332f0d32 100644 --- a/src/__tests__/lib/command/util/scenes-util.test.ts +++ b/src/__tests__/lib/command/util/scenes-util.test.ts @@ -1,7 +1,8 @@ import { jest } from '@jest/globals' -import type { ScenesEndpoint, SceneSummary, SmartThingsClient } from '@smartthings/core-sdk' +import type { ScenesEndpoint, SceneSummary } from '@smartthings/core-sdk' +import type { APICommand } from '../../../../lib/command/api-command.js' import type { ChooseFunction, createChooseFn } from '../../../../lib/command/util/util-util.js' @@ -31,13 +32,15 @@ test('chooseSceneFn uses correct endpoint to list scenes', async () => { const apiScenesListMock = jest.fn() .mockResolvedValueOnce(sceneList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - scenes: { - list: apiScenesListMock, + const command = { + client: { + scenes: { + list: apiScenesListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(sceneList) + expect(await listItems(command)).toBe(sceneList) expect(apiScenesListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/schema-util.test.ts b/src/__tests__/lib/command/util/schema-util.test.ts index 43a6aa3c..4aef62d7 100644 --- a/src/__tests__/lib/command/util/schema-util.test.ts +++ b/src/__tests__/lib/command/util/schema-util.test.ts @@ -383,13 +383,15 @@ test('chooseSchemaAppFn uses correct endpoint to list Schema Apps', async () => const apiSchemaListMock = jest.fn() .mockResolvedValueOnce(schemaList) const listItems = createChooseFnMock.mock.calls[0][1] - const client = { - schema: { - list: apiSchemaListMock, + const command = { + client: { + schema: { + list: apiSchemaListMock, + }, }, - } as unknown as SmartThingsClient + } as unknown as APICommand - expect(await listItems(client)).toBe(schemaList) + expect(await listItems(command)).toBe(schemaList) expect(apiSchemaListMock).toHaveBeenCalledExactlyOnceWith() }) diff --git a/src/__tests__/lib/command/util/util-util.test.ts b/src/__tests__/lib/command/util/util-util.test.ts index f5163c18..20372d6a 100644 --- a/src/__tests__/lib/command/util/util-util.test.ts +++ b/src/__tests__/lib/command/util/util-util.test.ts @@ -6,10 +6,8 @@ import type { SelectFromListFlags, } from '../../../../lib/command/select.js' import type { APICommand } from '../../../../lib/command/api-command.js' -import type { SmartThingsClient } from '@smartthings/core-sdk' -import type { ListDataFunction } from '../../../../lib/command/io-defs.js' import type { stringTranslateToId } from '../../../../lib/command/command-util.js' -import type { ListItemPredicate, ChooseOptions } from '../../../../lib/command/util/util-util.js' +import type { ListItemPredicate, ChooseOptions, CreateChooseFunctionOptions } from '../../../../lib/command/util/util-util.js' import type { SimpleType } from '../../../test-lib/simple-type.js' @@ -51,8 +49,8 @@ describe('chooseOptionsWithDefaults', () => { }) it('passes on other values unchanged', () => { - expect(chooseOptionsWithDefaults({ someOtherKey: 'some other value' } as Partial>)) - .toEqual(expect.objectContaining({ someOtherKey: 'some other value' })) + const config = { someOtherKey: 'some other value' } as Partial> + expect(chooseOptionsWithDefaults(config)).toEqual(expect.objectContaining(config)) }) }) @@ -82,17 +80,13 @@ describe('createChooseFn', () => { } as unknown as APICommand describe('resulting function', () => { - const itemListMock = jest.fn<(client: SmartThingsClient) => Promise>() + const itemListMock = jest.fn>[1]>() .mockResolvedValue(itemList) const chooseSimpleType = createChooseFn(config, itemListMock) it('sets default for passed options', async () => { - const listItemsMock = jest.fn>() - const opts: Partial> = { listItems: listItemsMock } + expect(await chooseSimpleType(command, undefined, {})).toBe('selected-simple-type-id') - expect(await chooseSimpleType(command, undefined, opts)).toBe('selected-simple-type-id') - - expect(listItemsMock).not.toHaveBeenCalled() expect(stringTranslateToIdMock).not.toHaveBeenCalled() expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( command, @@ -169,10 +163,28 @@ describe('createChooseFn', () => { expect(listFromTranslateCall).toBe(listFromSelectCall) }) - it('uses passed function to list items', async () => { + it('uses listItems from createChooseFn by default', async () => { expect(await chooseSimpleType(command)).toBe('selected-simple-type-id') - expect(itemListMock).toHaveBeenCalledExactlyOnceWith(command.client) + expect(itemListMock).toHaveBeenCalledExactlyOnceWith(command) + + const listItems = selectFromListMock.mock.calls[0][2].listItems + + // The internal function should not result in more API calls, no matter how many times it's called. + expect(await listItems()).toBe(itemList) + expect(await listItems()).toBe(itemList) + + expect(itemListMock).toHaveBeenCalledExactlyOnceWith(command) + }) + + it('overrides listItems from createChooseFn with one from options when specified', async () => { + const overridingListItemsMock = jest.fn>['listItems']>() + .mockResolvedValueOnce(itemList) + expect(await chooseSimpleType(command, undefined, { listItems: overridingListItemsMock })) + .toBe('selected-simple-type-id') + + expect(overridingListItemsMock).toHaveBeenCalledExactlyOnceWith(command) + expect(itemListMock).not.toHaveBeenCalled() const listItems = selectFromListMock.mock.calls[0][2].listItems @@ -180,7 +192,51 @@ describe('createChooseFn', () => { expect(await listItems()).toBe(itemList) expect(await listItems()).toBe(itemList) - expect(itemListMock).toHaveBeenCalledExactlyOnceWith(command.client) + expect(overridingListItemsMock).toHaveBeenCalledExactlyOnceWith(command) + expect(itemListMock).not.toHaveBeenCalled() + }) + + it('throws error when `useConfigDefault` is used when no default is set up', async () => { + await expect(chooseSimpleType(command, undefined, { useConfigDefault: true })).rejects.toThrow() + }) + + const getItemMock = jest.fn>['defaultValue']['getItem']>() + .mockResolvedValue(item1) + const userMessageMock = jest.fn>['defaultValue']['userMessage']>() + const chooseSimpleTypeWithDefaultConfig = createChooseFn( + config, + itemListMock, + { + defaultValue: { + configKey: '', + getItem: getItemMock, + userMessage: userMessageMock, + }, + }, + ) + + it('passes default config options to selectFromList when configured and requested', async () => { + expect(await chooseSimpleTypeWithDefaultConfig(command, undefined, { useConfigDefault: true })) + .toBe('selected-simple-type-id') + + expect(selectFromListMock).toHaveBeenCalledExactlyOnceWith( + command, + config, + expect.objectContaining({ + defaultValue: { + configKey: '', + getItem: expect.any(Function), + userMessage: userMessageMock, + }, + }), + ) + + const getItem = selectFromListMock.mock.calls[0][2].defaultValue?.getItem + + expect(getItem).toBeDefined() + expect(await getItem?.('input-id')).toBe(item1) + + expect(getItemMock).toHaveBeenCalledExactlyOnceWith(command, 'input-id') }) }) }) diff --git a/src/lib/command/util/apps-util.ts b/src/lib/command/util/apps-util.ts index f3f5241e..d31925ba 100644 --- a/src/lib/command/util/apps-util.ts +++ b/src/lib/command/util/apps-util.ts @@ -57,7 +57,7 @@ export const chooseAppFn = (): ChooseFunction => createChooseFn( sortKeyName: 'displayName', listTableFieldDefinitions: ['displayName', 'appType', 'appId'], }, - (client: SmartThingsClient) => client.apps.list(), + command => command.client.apps.list(), ) export const chooseApp = chooseAppFn() diff --git a/src/lib/command/util/devicepreferences-util.ts b/src/lib/command/util/devicepreferences-util.ts index 6bad9713..359867a4 100644 --- a/src/lib/command/util/devicepreferences-util.ts +++ b/src/lib/command/util/devicepreferences-util.ts @@ -1,4 +1,4 @@ -import { type DevicePreference, type SmartThingsClient } from '@smartthings/core-sdk' +import { type DevicePreference } from '@smartthings/core-sdk' import { type TableFieldDefinition } from '../../table-generator.js' import { type ChooseFunction, createChooseFn } from './util-util.js' @@ -31,7 +31,7 @@ export const chooseDevicePreferenceFn = (): ChooseFunction => sortKeyName: 'preferenceId', listTableFieldDefinitions: ['preferenceId', 'title', 'name'], }, - (client: SmartThingsClient) => client.devicePreferences.list(), + command => command.client.devicePreferences.list(), ) export const chooseDevicePreference = chooseDevicePreferenceFn() diff --git a/src/lib/command/util/deviceprofiles-choose.ts b/src/lib/command/util/deviceprofiles-choose.ts index 3f1c05a9..307ecf20 100644 --- a/src/lib/command/util/deviceprofiles-choose.ts +++ b/src/lib/command/util/deviceprofiles-choose.ts @@ -1,20 +1,21 @@ -import { type DeviceProfile, type LocaleReference, type SmartThingsClient } from '@smartthings/core-sdk' +import { type DeviceProfile, type LocaleReference } from '@smartthings/core-sdk' import { type WithLocales } from '../../api-helpers.js' import { type TableFieldDefinition } from '../../table-generator.js' +import { type APICommand } from '../api-command.js' import { type ChooseFunction, createChooseFn } from './util-util.js' export const chooseDeviceProfileFn = ( options?: { verbose?: boolean }, ): ChooseFunction => { - const listItems = async (client: SmartThingsClient): Promise<(DeviceProfile & WithLocales)[]> => { - const deviceProfiles = await client.deviceProfiles.list() + const listItems = async (command: APICommand): Promise<(DeviceProfile & WithLocales)[]> => { + const deviceProfiles = await command.client.deviceProfiles.list() if (options?.verbose) { return await Promise.all(deviceProfiles.map(async deviceProfile => { const withLocales: DeviceProfile & WithLocales = { ...deviceProfile } try { - const locales = await client.deviceProfiles.listLocales(deviceProfile.id) + const locales = await command.client.deviceProfiles.listLocales(deviceProfile.id) withLocales.locales = locales?.map((it: LocaleReference) => it.tag).sort().join(', ') } catch (error) { if (!error.message?.includes('status code 404')) { diff --git a/src/lib/command/util/devices-choose.ts b/src/lib/command/util/devices-choose.ts index 057c92c1..4ecb49ec 100644 --- a/src/lib/command/util/devices-choose.ts +++ b/src/lib/command/util/devices-choose.ts @@ -1,9 +1,4 @@ -import { - type Component, - type Device, - type DeviceListOptions, - type SmartThingsClient, -} from '@smartthings/core-sdk' +import { type Component, type Device, type DeviceListOptions } from '@smartthings/core-sdk' import { fatalError } from '../../util.js' import { type ChooseFunction, createChooseFn } from './util-util.js' @@ -18,7 +13,7 @@ export const chooseDeviceFn = ( sortKeyName: 'label', listTableFieldDefinitions: ['label', 'name', 'type', 'deviceId'], }, - (client: SmartThingsClient) => client.devices.list(deviceListOptions), + command => command.client.devices.list(deviceListOptions), ) export const chooseDevice = chooseDeviceFn() diff --git a/src/lib/command/util/drivers-choose.ts b/src/lib/command/util/drivers-choose.ts index fedf9300..f2d0c572 100644 --- a/src/lib/command/util/drivers-choose.ts +++ b/src/lib/command/util/drivers-choose.ts @@ -1,4 +1,4 @@ -import { type EdgeDriverSummary, type SmartThingsClient } from '@smartthings/core-sdk' +import { type EdgeDriverSummary } from '@smartthings/core-sdk' import { type ChooseFunction, createChooseFn } from './util-util.js' import { listDrivers } from './edge/drivers-util.js' @@ -19,7 +19,7 @@ export const chooseDriverFn = ( primaryKeyName: 'driverId', sortKeyName: 'name', }, - (client: SmartThingsClient) => listDrivers(client, options?.includeAllOrganizations), + command => listDrivers(command.client, options?.includeAllOrganizations), ) export const chooseDriver = chooseDriverFn() diff --git a/src/lib/command/util/edge/channels-choose.ts b/src/lib/command/util/edge/channels-choose.ts index 71a39557..fa0547c0 100644 --- a/src/lib/command/util/edge/channels-choose.ts +++ b/src/lib/command/util/edge/channels-choose.ts @@ -32,8 +32,9 @@ export async function chooseChannel(command: APICommand, promptMessage: string, sortKeyName: 'name', } - const listItems = options?.listItems ?? - ((): Promise => listChannels(command.client, { includeReadOnly: opts.includeReadOnly })) + const listItems = (): Promise => options?.listItems + ? options.listItems(command) + : listChannels(command.client, { includeReadOnly: opts.includeReadOnly }) const preselectedId = channelFromArg ? (opts.allowIndex @@ -49,6 +50,9 @@ export async function chooseChannel(command: APICommand, promptMessage: string, `using previously specified default channel named "${channel.name}" (${channel.channelId})`, } : undefined - return selectFromList(command, config, - { preselectedId, listItems, promptMessage, defaultValue }) + return selectFromList( + command, + config, + { preselectedId, listItems, promptMessage, defaultValue }, + ) } diff --git a/src/lib/command/util/hubs-choose.ts b/src/lib/command/util/hubs-choose.ts index 7ce73b55..a048b35f 100644 --- a/src/lib/command/util/hubs-choose.ts +++ b/src/lib/command/util/hubs-choose.ts @@ -1,4 +1,4 @@ -import { type Device, DeviceIntegrationType, type SmartThingsClient } from '@smartthings/core-sdk' +import { type Device, DeviceIntegrationType } from '@smartthings/core-sdk' import { type ChooseFunction, createChooseFn } from './util-util.js' @@ -11,8 +11,7 @@ export const chooseHubFn = (options?: { locationId: string | string[] }): Choose sortKeyName: 'name', listTableFieldDefinitions: ['label', 'name', 'deviceId'], }, - (client: SmartThingsClient) => - client.devices.list({ type: DeviceIntegrationType.HUB, locationId: options?.locationId }), + command => command.client.devices.list({ type: DeviceIntegrationType.HUB, locationId: options?.locationId }), ) } diff --git a/src/lib/command/util/installedapps-util.ts b/src/lib/command/util/installedapps-util.ts index 27ddb22c..d2c5f0c2 100644 --- a/src/lib/command/util/installedapps-util.ts +++ b/src/lib/command/util/installedapps-util.ts @@ -1,7 +1,8 @@ -import { type SmartThingsClient, type InstalledApp, InstalledAppListOptions } from '@smartthings/core-sdk' +import { type InstalledApp, InstalledAppListOptions } from '@smartthings/core-sdk' import { withLocations, type WithNamedLocation } from '../../api-helpers.js' import { type TableFieldDefinition } from '../../table-generator.js' +import { type APICommand } from '../api-command.js' import { listTableFieldDefinitions } from './installedapps-table.js' import { type ChooseFunction, createChooseFn } from './util-util.js' @@ -20,10 +21,10 @@ const buildListTableFieldDefinitions = ( const listInstalledAppsFn = ( options?: ChooseInstalledAppOptions, -) => async (client: SmartThingsClient): Promise<(InstalledApp & WithNamedLocation)[]> => { - const installedApps = await client.installedApps.list(options?.listOptions) +) => async (command: APICommand): Promise<(InstalledApp & WithNamedLocation)[]> => { + const installedApps = await command.client.installedApps.list(options?.listOptions) if (options?.verbose) { - return await withLocations(client, installedApps) + return await withLocations(command.client, installedApps) } return installedApps } diff --git a/src/lib/command/util/locations-util.ts b/src/lib/command/util/locations-util.ts index 2d94f45d..923466f8 100644 --- a/src/lib/command/util/locations-util.ts +++ b/src/lib/command/util/locations-util.ts @@ -1,7 +1,7 @@ -import { Location, LocationItem, SmartThingsClient } from '@smartthings/core-sdk' +import { type Location, type LocationItem } from '@smartthings/core-sdk' -import { TableFieldDefinition } from '../../table-generator.js' -import { ChooseFunction, createChooseFn } from './util-util.js' +import { type TableFieldDefinition } from '../../table-generator.js' +import { type ChooseFunction, createChooseFn } from './util-util.js' export const tableFieldDefinitions: TableFieldDefinition[] = [ @@ -15,7 +15,7 @@ export const chooseLocationFn = (): ChooseFunction => createChoose primaryKeyName: 'locationId', sortKeyName: 'name', }, - (client: SmartThingsClient) => client.locations.list(), + command => command.client.locations.list(), ) export const chooseLocation = chooseLocationFn() diff --git a/src/lib/command/util/organizations-util.ts b/src/lib/command/util/organizations-util.ts index 3da5e1b7..cb2bdc83 100644 --- a/src/lib/command/util/organizations-util.ts +++ b/src/lib/command/util/organizations-util.ts @@ -1,6 +1,6 @@ -import { type OrganizationResponse, type SmartThingsClient } from '@smartthings/core-sdk' +import { type OrganizationResponse } from '@smartthings/core-sdk' -import { TableFieldDefinition } from '../../table-generator.js' +import { type TableFieldDefinition } from '../../table-generator.js' import { type InputDefinition, selectDef, @@ -38,7 +38,7 @@ export const chooseOrganizationFn = (): ChooseFunction => primaryKeyName: 'organizationId', sortKeyName: 'name', }, - (client: SmartThingsClient) => client.organizations.list(), + command => command.client.organizations.list(), ) export const chooseOrganization = chooseOrganizationFn() diff --git a/src/lib/command/util/rules-choose.ts b/src/lib/command/util/rules-choose.ts index e8e0851d..d1ec6120 100644 --- a/src/lib/command/util/rules-choose.ts +++ b/src/lib/command/util/rules-choose.ts @@ -1,4 +1,4 @@ -import { type Rule, type SmartThingsClient } from '@smartthings/core-sdk' +import { type Rule } from '@smartthings/core-sdk' import { type WithNamedLocation } from '../../api-helpers.js' import { getRulesByLocation } from './rules-util.js' @@ -12,7 +12,7 @@ export const chooseRuleFn = (locationId?: string): ChooseFunction getRulesByLocation(client, locationId), + command => getRulesByLocation(command.client, locationId), ) export const chooseRule = chooseRuleFn() diff --git a/src/lib/command/util/scenes-util.ts b/src/lib/command/util/scenes-util.ts index 2852c61c..bf113e99 100644 --- a/src/lib/command/util/scenes-util.ts +++ b/src/lib/command/util/scenes-util.ts @@ -14,7 +14,7 @@ export const chooseSceneFn = (): ChooseFunction => createChooseFn( primaryKeyName: 'sceneId', sortKeyName: 'sceneName', }, - client => client.scenes.list(), + command => command.client.scenes.list(), ) export const chooseScene = chooseSceneFn() diff --git a/src/lib/command/util/schema-util.ts b/src/lib/command/util/schema-util.ts index d2b47a18..45becf22 100644 --- a/src/lib/command/util/schema-util.ts +++ b/src/lib/command/util/schema-util.ts @@ -1,7 +1,6 @@ import { type SchemaApp, type SchemaAppRequest, - type SmartThingsClient, type SmartThingsURLProvider, type ViperAppLinks, } from '@smartthings/core-sdk' @@ -142,7 +141,7 @@ export const chooseSchemaAppFn = (): ChooseFunction => createChooseFn sortKeyName: 'appName', listTableFieldDefinitions: ['appName', 'endpointAppId', 'hostingType'], }, - (client: SmartThingsClient): Promise => client.schema.list(), + command => command.client.schema.list(), ) export const chooseSchemaApp = chooseSchemaAppFn() diff --git a/src/lib/command/util/util-util.ts b/src/lib/command/util/util-util.ts index 28961392..d582c491 100644 --- a/src/lib/command/util/util-util.ts +++ b/src/lib/command/util/util-util.ts @@ -1,22 +1,22 @@ -import { type SmartThingsClient } from '@smartthings/core-sdk' - import { type APICommand } from '../api-command.js' -import { type ListDataFunction } from '../io-defs.js' import { stringTranslateToId } from '../command-util.js' -import { type SelectFromListConfig, type SelectFromListFlags, selectFromList } from '../select.js' +import { type SelectFromListConfig, type SelectFromListFlags, SelectOptions, selectFromList } from '../select.js' export type ListItemPredicate = (value: T, index: number, array: T[]) => boolean /** - * Note that not all functions that use this interface support all options. Check the - * `chooseThing` (e.g. `chooseDevice`) method itself. + * Note that a few functions using this interface don't support all options. Check the + * `chooseThing` (e.g. `chooseDevice`) method itself. (If the `chooseThing` is implemented using + * `createChooseFn`, it will support all of them, with the exception of `useConfigDefault` + * which will work only if the call to `createChooseFn` includes configuration for it via the + * `defaultValue` option.) */ export type ChooseOptions = { allowIndex: boolean verbose: boolean useConfigDefault: boolean - listItems?: ListDataFunction + listItems?: (command: APICommand) => Promise autoChoose?: boolean listFilter?: ListItemPredicate promptMessage?: string @@ -29,11 +29,19 @@ export const chooseOptionsDefaults = (): ChooseOptions => ( autoChoose: false, }) -export const chooseOptionsWithDefaults = (options: Partial> | undefined): ChooseOptions => ({ +export const chooseOptionsWithDefaults = ( + options: Partial> | undefined, +): ChooseOptions => ({ ...chooseOptionsDefaults(), ...options, }) +export type CreateChooseFunctionOptions = { + defaultValue: Omit>['defaultValue'], 'getItem'> & { + getItem: (command: APICommand, id: string) => Promise + } +} + export type ChooseFunction = ( command: APICommand, itemIdOrNameFromArg?: string, @@ -41,17 +49,19 @@ export type ChooseFunction = ( export const createChooseFn = ( config: SelectFromListConfig, - listItems: (client: SmartThingsClient) => Promise, + listItems: (command: APICommand) => Promise, + createOptions?: CreateChooseFunctionOptions, ): ChooseFunction => async ( command: APICommand, itemIdOrNameFromArg?: string, - options?: Partial>): Promise => { + options?: Partial>, + ): Promise => { const opts = chooseOptionsWithDefaults(options) // Listing items usually makes an API call which we only want to happen once so we do it // now and just use stub functions that return these items later as needed. - const items = await listItems(command.client) + const items = await (opts.listItems ?? listItems)(command) const filteredItems = opts.listFilter ? items.filter(opts.listFilter) : items const listItemsWrapper = async (): Promise => filteredItems @@ -59,10 +69,27 @@ export const createChooseFn = ( ? await stringTranslateToId(config, itemIdOrNameFromArg, listItemsWrapper) : itemIdOrNameFromArg - return selectFromList(command, config, { + const selectOptions: SelectOptions = { preselectedId, autoChoose: opts.autoChoose, listItems: listItemsWrapper, promptMessage: opts.promptMessage, - }) + } + + if (opts.useConfigDefault) { + if (!createOptions?.defaultValue) { + throw Error('invalid state, the choose function was called with "useConfigDefault"' + + ' but no default configured') + } + selectOptions.defaultValue = { + ...createOptions.defaultValue, + getItem: (id: string): Promise => createOptions.defaultValue.getItem(command, id), + } + } + + return selectFromList( + command, + config, + selectOptions, + ) }