Skip to content

Commit

Permalink
fix: use InstalledApp fields in installedapps:rename and refactor to …
Browse files Browse the repository at this point in the history
…use new functional paradigm
  • Loading branch information
rossiam committed Dec 14, 2020
1 parent 804eaaa commit 8170818
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 157 deletions.
142 changes: 71 additions & 71 deletions packages/cli/README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/cli/src/commands/capabilities/namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export default class CapabilitiesListNamespaces extends APICommand {
}

listTableFieldDefinitions = ['name', 'ownerType', 'ownerId']
sortKeyName = 'name'
primaryKeyName = 'name'

async run(): Promise<void> {
const { args, argv, flags } = this.parse(CapabilitiesListNamespaces)
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/src/commands/deviceprofiles/view/update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { SelectingInputOutputAPICommand } from '@smartthings/cli-lib'
import {buildTableOutput, DeviceDefinition, DeviceDefinitionRequest, prunePresentationValues, augmentPresentationValues} from '../view'
import {generateDefaultConfig} from '../create'
import {cleanupRequest} from '../update'

import { generateDefaultConfig } from '../create'
import { cleanupRequest } from '../update'
import { buildTableOutput, DeviceDefinition, DeviceDefinitionRequest, prunePresentationValues, augmentPresentationValues } from '../view'


export default class CapabilitiesUpdate extends SelectingInputOutputAPICommand<DeviceDefinitionRequest, DeviceDefinition, DeviceDefinition> {
Expand Down Expand Up @@ -64,8 +65,6 @@ export default class CapabilitiesUpdate extends SelectingInputOutputAPICommand<D
if (presentationData) {
presentationData = augmentPresentationValues(presentationData)
} else {
// eslint-disable-next-line no-console
//console.log('Generating presentation')
presentationData = await generateDefaultConfig(this.client, id, profileData)
}

Expand All @@ -77,7 +76,7 @@ export default class CapabilitiesUpdate extends SelectingInputOutputAPICommand<D
profileData.metadata.mnmn = presentation.manufacturerName
const profile = await this.client.deviceProfiles.update(id, cleanupRequest(profileData))

return {...profile, presentation: prunePresentationValues(presentation)}
return { ...profile, presentation: prunePresentationValues(presentation) }
})
}
}
38 changes: 19 additions & 19 deletions packages/cli/src/commands/installedapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@ import { flags } from '@oclif/command'

import { InstalledApp } from '@smartthings/core-sdk'

import { ListingOutputAPICommand, TableFieldDefinition, withLocations } from '@smartthings/cli-lib'
import { APICommand, outputListing, TableFieldDefinition, withLocations } from '@smartthings/cli-lib'


export type InstalledAppWithLocation = InstalledApp & { location?: string }

export default class InstalledAppsCommand extends ListingOutputAPICommand<InstalledApp, InstalledAppWithLocation> {
export const listTableFieldDefinitions = ['displayName', 'installedAppType', 'installedAppStatus', 'installedAppId']
export const tableFieldDefinitions: TableFieldDefinition<InstalledApp>[] = [
'displayName', 'installedAppId', 'installedAppType', 'installedAppStatus',
'singleInstance', 'appId', 'locationId', 'singleInstance',
{
label: 'Classifications',
value: installedApp => installedApp.classifications?.join('\n') ?? '',
include: installedApp => !!installedApp.classifications,
},
]

export default class InstalledAppsCommand extends APICommand {
static description = 'get a specific app or a list of apps'

static flags = {
...ListingOutputAPICommand.flags,
...APICommand.flags,
...outputListing.flags,
verbose: flags.boolean({
description: 'include location name in output',
char: 'v',
Expand All @@ -25,18 +37,8 @@ export default class InstalledAppsCommand extends ListingOutputAPICommand<Instal

primaryKeyName = 'installedAppId'
sortKeyName = 'displayName'
protected listTableFieldDefinitions = ['displayName', 'installedAppType',
'installedAppStatus', 'installedAppId']

protected tableFieldDefinitions: TableFieldDefinition<InstalledApp>[] = [
'displayName', 'installedAppId', 'installedAppType', 'installedAppStatus',
'singleInstance', 'appId', 'locationId', 'singleInstance',
{
label: 'Classifications',
value: installedApp => installedApp.classifications?.join('\n') ?? '',
include: installedApp => !!installedApp.classifications,
},
]
listTableFieldDefinitions = listTableFieldDefinitions
tableFieldDefinitions = tableFieldDefinitions

async run(): Promise<void> {
const { args, argv, flags } = this.parse(InstalledAppsCommand)
Expand All @@ -46,7 +48,7 @@ export default class InstalledAppsCommand extends ListingOutputAPICommand<Instal
this.listTableFieldDefinitions.splice(3, 0, 'location')
}

await this.processNormally(
await outputListing<InstalledApp, InstalledAppWithLocation>(this,
args.id,
async () => {
const apps = await this.client.installedApps.list()
Expand All @@ -55,9 +57,7 @@ export default class InstalledAppsCommand extends ListingOutputAPICommand<Instal
}
return apps
},
(id: string) => {
return this.client.installedApps.get(id)
},
id => this.client.installedApps.get(id),
)
}
}
10 changes: 5 additions & 5 deletions packages/cli/src/commands/installedapps/delete.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { InstalledApp } from '@smartthings/core-sdk'

import { SelectingAPICommand } from '@smartthings/cli-lib'
import { selectAndActOn, APICommand } from '@smartthings/cli-lib'


export default class InstalledAppDeleteCommand extends SelectingAPICommand<InstalledApp> {
export default class InstalledAppDeleteCommand extends APICommand {
static description = 'delete the installed app instance'

static flags = SelectingAPICommand.flags
static flags = APICommand.flags

static args = [{
name: 'id',
Expand All @@ -22,9 +22,9 @@ export default class InstalledAppDeleteCommand extends SelectingAPICommand<Insta
const { args, argv, flags } = this.parse(InstalledAppDeleteCommand)
await super.setup(args, argv, flags)

await this.processNormally(args.id,
await selectAndActOn<InstalledApp>(this, args.id,
async () => await this.client.installedApps.list(),
async (id) => { await this.client.installedApps.delete(id) },
async id => { await this.client.installedApps.delete(id) },
'installed app {{id}} deleted')
}
}
32 changes: 17 additions & 15 deletions packages/cli/src/commands/installedapps/rename.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import inquirer from 'inquirer'

import { InstalledApp } from '@smartthings/core-sdk'
import { SelectingOutputAPICommand } from '@smartthings/cli-lib'
import { buildTableOutput } from '../devices'
import { selectActOnAndOutput, APICommand } from '@smartthings/cli-lib'
import { listTableFieldDefinitions, tableFieldDefinitions } from '../installedapps'


export default class DeviceComponentStatusCommand extends SelectingOutputAPICommand<InstalledApp, InstalledApp> {
export default class DeviceComponentStatusCommand extends APICommand {
static description = 'renamed an installed app instance'

static flags = SelectingOutputAPICommand.flags
static flags = {
...APICommand.flags,
...selectActOnAndOutput.flags,
}

static args = [
{
Expand All @@ -21,30 +24,29 @@ export default class DeviceComponentStatusCommand extends SelectingOutputAPIComm
},
]

protected buildTableOutput = buildTableOutput
tableFieldDefinitions = tableFieldDefinitions

primaryKeyName = 'deviceId'
sortKeyName = 'label'
listTableFieldDefinitions = ['label', 'name', 'type', 'deviceId']
itemName = 'installed app'
primaryKeyName = 'installedAppId'
sortKeyName = 'displayName'
listTableFieldDefinitions = listTableFieldDefinitions
acceptIndexId = true

async run(): Promise<void> {
const { args, argv, flags } = this.parse(DeviceComponentStatusCommand)
await super.setup(args, argv, flags)

await this.processNormally(
await selectActOnAndOutput<InstalledApp, InstalledApp>(this,
args.id,
() => this.client.installedApps.list(),
async (id) => {
let displayName = args.name
if (!displayName) {
displayName = (await inquirer.prompt({
async id => {
const displayName = args.name ??
(await inquirer.prompt({
type: 'input',
name: 'label',
message: 'Enter new installed app name:',
})).label
}
return this.client.installedApps.update(id, {displayName})
return this.client.installedApps.update(id, { displayName })
},
)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/lib/src/__tests__/input-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export function buildMockCommand(flags: { [name: string]: any } = {}, profileCon
flags,
profileConfig,
tableGenerator: new DefaultTableGenerator(true),
exit(code?: number): never {
throw Error(`not implemented; code was ${code}`)
},
}
}

Expand Down
172 changes: 172 additions & 0 deletions packages/lib/src/acting-io.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import inquirer from 'inquirer'

import { isIndexArgument } from './api-command'
import { formatAndWriteItem, outputList, CommonListOutputProducer, CommonOutputProducer, ActionFunction, ListDataFunction,
Sorting, IdRetrievalFunction } from './basic-io'
import { SmartThingsCommandInterface } from './smartthings-command'


// Functions in this file support commands that act on an item. The biggest difference
// between these and the methods from listing-io.ts is that they list items and immediately
// query the user for an item to act on (if one wasn't specified on the command line). This
// makes them safe to use for actions that make changes to the item or delete it.

// TODO: implement equivalent of acceptIndexId from old code

// TODO: drop "new" from name when old one is gone
export async function newStringGetIdFromUser<L>(primaryKeyName: string, list: L[]): Promise<string> {
const convertToId = (itemIdOrIndex: string): string | false => {
if (itemIdOrIndex.length === 0) {
return false
}
const matchingItem = list.find(item => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return (primaryKeyName in item) && itemIdOrIndex === item[primaryKeyName]
})
if (matchingItem) {
return itemIdOrIndex
}

if (!isIndexArgument(itemIdOrIndex)) {
return false
}

const index = Number.parseInt(itemIdOrIndex)

if (!Number.isNaN(index) && index > 0 && index <= list.length) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const pk = list[index - 1][primaryKeyName]
if (typeof pk === 'string') {
return pk
} else {
throw Error(`invalid type ${typeof pk} for primary key` +
` ${primaryKeyName} in ${JSON.stringify(list[index - 1])}`)
}
} else {
return false
}
}

const itemIdOrIndex: string = (await inquirer.prompt({
type: 'input',
name: 'itemIdOrIndex',
message: 'Enter id or index',
validate: (input) => {
return convertToId(input)
? true
: `Invalid id or index ${itemIdOrIndex}. Please enter an index or valid id.`
},
})).itemIdOrIndex
const inputId = convertToId(itemIdOrIndex)
if (inputId === false) {
throw Error(`unable to convert ${itemIdOrIndex} to id`)
}
return inputId
}


export type SelectingCommand<L> = SmartThingsCommandInterface & Sorting & CommonListOutputProducer<L>

/**
* Process a command that selects and item (e.g. a device), performs some action on that device.
* This method is mainly here to combine shared code for the methods below but could be used
* elsewhere in circumstances where unusual output might be necessary.
*
* @param command The command itself which conforms to the `SelectingCommand` type.
* @param id The id of the item to act on, or undefined if the user should be asked to select one.
* @param listFunction A function that returns a list of all items which will be used to build the
* list of the user to choose from when `id` is undefined.
* @param actionFunction The function that performs the action.
* @param getIdFromUser A function that queries the user into a for an item given a list of them.
*
* @returns a tuple containing the id of the item acted on and the result of the action function.
*/
export async function selectAndActOnGeneric<ID, O, L>(command: SelectingCommand<L>,
id: ID | undefined, listFunction: ListDataFunction<L>, actionFunction: ActionFunction<ID, O>,
getIdFromUser: IdRetrievalFunction<ID, L>): Promise<[ID, O]> {
let computedId: ID
if (id) {
computedId = id
} else {
const list = await outputList(command, listFunction, true)
if (list.length === 0) {
// Nothing was found; user was already notified.
command.exit(0)
}
computedId = await getIdFromUser(command.primaryKeyName, list)
}
const updatedItem = await actionFunction(computedId)
return [computedId, updatedItem]
}
selectActOnAndOutputGeneric.flags = outputList.flags

/**
* Process a command that selects and item (e.g. a device), performs some action on that device
* and then simply states that the action has completed. This is useful for actions that don't
* return data, like deleting an item.
*
* @param command The command itself which conforms to the `SelectingCommand` type.
* @param id The id of the item to act on, or undefined if the user should be asked to select one.
* @param listFunction A function that returns a list of all items which will be used to build the
* list of the user to choose from when `id` is undefined.
* @param actionFunction The function that performs the action.
* @param successMessage The message that should be displayed upon successful completion of the
* action. (Successful completion is defined as `actionFunction` not throwing any exceptions.)
*
* @returns the id selected and acted on by `actionFunction`
*/
export async function selectAndActOn<L>(command: SelectingCommand<L>,
id: string | undefined, listFunction: ListDataFunction<L>,
actionFunction: ActionFunction<string, void>, successMessage: string): Promise<string> {
const [computedId] = await selectAndActOnGeneric<string, void, L>(command, id, listFunction,
actionFunction, newStringGetIdFromUser)
process.stdout.write(`${successMessage.replace('{{id}}', JSON.stringify(computedId))}\n`)
return computedId
}


export type SelectingOutputCommand<O, L> = SelectingCommand<L> & CommonOutputProducer<O>

/**
* Process a command that selects an item (e.g. a device), performs some action on that device
* and then outputs the results. Unless your ids are not simple strings, you probably want to
* use `selectActOnAndOutput` below instead.
*
* @param command The command itself which conforms to the `SelectingOutputCommand` type.
* @param id The id of the item to act on, or undefined if the user should be asked to select one.
* @param listFunction A function that returns a list of all items which will be used to build the
* list of the user to choose from when `id` is undefined.
* @param actionFunction The function that performs the action.
* @param getIdFromUser A function that queries the user into a for an item given a list of them.
*
* @returns a tuple containing the id of the item acted on and the result of the action function.
*/
export async function selectActOnAndOutputGeneric<ID, O, L>(command: SelectingOutputCommand<O, L>,
id: ID | undefined, listFunction: ListDataFunction<L>, actionFunction: ActionFunction<ID, O>,
getIdFromUser: IdRetrievalFunction<ID, L>): Promise<[ID, O]> {
const [computedId, updatedItem] = await selectAndActOnGeneric(command, id, listFunction, actionFunction, getIdFromUser)
await formatAndWriteItem(command, updatedItem)
return [computedId, updatedItem]
}
selectActOnAndOutputGeneric.flags = outputList.flags

/**
* Process a command that selects an item (e.g. a device), performs some action on that device
* and then outputs the results.
*
* @param command The command itself which conforms to the `SelectingOutputCommand` type.
* @param id The id of the item to act on, or undefined if the user should be asked to select one.
* @param listFunction A function that returns a list of all items which will be used to build the
* list of the user to choose from when `id` is undefined.
* @param actionFunction The function that performs the action.
*
* @returns a tuple containing the id of the item acted on and the result of the action function.
*/
export async function selectActOnAndOutput<O, L>(command: SelectingOutputCommand<O, L>,
id: string | undefined, listFunction: ListDataFunction<L>,
actionFunction: ActionFunction<string, O>): Promise<[string, O]> {
return selectActOnAndOutputGeneric<string, O, L>(command, id, listFunction, actionFunction, newStringGetIdFromUser)
}
selectActOnAndOutput.flags = selectActOnAndOutputGeneric.flags

0 comments on commit 8170818

Please sign in to comment.