Skip to content

Commit

Permalink
fix: minor history fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
rossiam committed Nov 30, 2023
1 parent f4c0418 commit fdf4250
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-cars-grin.md
@@ -0,0 +1,5 @@
---
"@smartthings/cli": patch
---

handle epoch timestamps for history, honor `after` flag strictly in JSON and YAML output
57 changes: 49 additions & 8 deletions packages/cli/src/__tests__/lib/commands/history-util.test.ts
Expand Up @@ -19,7 +19,7 @@ import {
jest.mock('inquirer')

describe('history-util', () => {
describe('epochTime', () => {
describe('toEpochTime', () => {
it ('handles ISO input', () => {
expect(toEpochTime('2022-08-01T22:41:42.559Z')).toBe(1659393702559)
})
Expand All @@ -29,7 +29,11 @@ describe('history-util', () => {
expect(toEpochTime('8/1/2022, 6:41:42 PM')).toBe(expected)
})

it ('handles undefined input', () => {
it ('handles input in millis since epoch', () => {
expect(toEpochTime('1700596752000')).toBe(1700596752000)
})

it('handles undefined input', () => {
expect(toEpochTime(undefined)).toBeUndefined()
})
})
Expand Down Expand Up @@ -189,8 +193,8 @@ describe('history-util', () => {
hasNext.mockReturnValueOnce(true)
hasNext.mockReturnValueOnce(true)
hasNext.mockReturnValueOnce(false)
promptSpy.mockResolvedValueOnce({ more: '' })
promptSpy.mockResolvedValueOnce({ more: 'y' })
promptSpy.mockResolvedValueOnce({ more: true })
promptSpy.mockResolvedValueOnce({ more: true })

await writeDeviceEventsTable(commandMock, dataMock)

Expand All @@ -201,8 +205,8 @@ describe('history-util', () => {

it('returns next page until canceled', async () => {
hasNext.mockReturnValue(true)
promptSpy.mockResolvedValueOnce({ more: '' })
promptSpy.mockResolvedValueOnce({ more: 'n' })
promptSpy.mockResolvedValueOnce({ more: true })
promptSpy.mockResolvedValueOnce({ more: false })

await writeDeviceEventsTable(commandMock, dataMock)

Expand Down Expand Up @@ -231,8 +235,12 @@ describe('history-util', () => {

const params = { locationId: 'location-id', deviceId: 'device-id' }
const items: DeviceActivity[] = []
for (let index = 0; index < maxItemsPerRequest * maxRequestsBeforeWarning + 10; index++) {
items.push({ deviceId: 'history-device-id', text: `item${index}` } as DeviceActivity)
const totalNumItems = maxItemsPerRequest * maxRequestsBeforeWarning + 10
for (let index = 0; index < totalNumItems; index++) {
items.push({
deviceId: 'history-device-id',
text: `item${index}`,
} as DeviceActivity)
}

const promptMock = jest.mocked(inquirer.prompt)
Expand Down Expand Up @@ -353,5 +361,38 @@ describe('history-util', () => {
expect(hasNextMock).toHaveBeenCalledTimes(6)
expect(nextMock).toHaveBeenCalledTimes(6)
})

it('stops paging when results are before specified after', async () => {
const epochTimestamp = 1701097200 // 2023/11/27 9:00 a.m. CST
const makeActivity = (index: number, epoch: number): DeviceActivity => ({
deviceId: 'history-device-id',
text: `item${index}`,
epoch,
} as DeviceActivity)
const firstPage = [
makeActivity(0, epochTimestamp + 600),
makeActivity(1, epochTimestamp + 500),
makeActivity(2, epochTimestamp + 400),
]
const secondPage = [
makeActivity(3, epochTimestamp + 300),
makeActivity(4, epochTimestamp + 200),
makeActivity(5, epochTimestamp + 100),
]
const historyResponse = makeHistoryResponse(firstPage)
historyDevicesMock.mockResolvedValueOnce(historyResponse)
hasNextMock.mockReturnValue(true)
nextMock.mockImplementationOnce(async () => historyResponse.items = secondPage)

const paramsWithAfter = { ...params, after: epochTimestamp + 350 }

const result = await getHistory(client, 300, 3, paramsWithAfter)
expect(result).toStrictEqual(firstPage)

expect(historyDevicesMock).toHaveBeenCalledTimes(1)
expect(historyDevicesMock).toHaveBeenCalledWith(paramsWithAfter)
expect(hasNextMock).toHaveBeenCalledTimes(1)
expect(nextMock).toHaveBeenCalledTimes(1)
})
})
})
58 changes: 31 additions & 27 deletions packages/cli/src/lib/commands/history-util.ts
Expand Up @@ -36,17 +36,15 @@ export const historyFlags = {
}),
}

export function toEpochTime(date?: string): number | undefined {
if (date) {
return new Date(date).getTime()
}
}
export const toEpochTime = (date?: string): number | undefined =>
date ? (date.match(/^\d+$/) ? Number(date) : new Date(date).getTime()) : undefined

export function sortEvents(list: DeviceActivity[]): DeviceActivity[] {
return list.sort((a, b) => a.epoch === b.epoch ? 0 : (a.epoch < b.epoch ? 1 : -1))
}
export const sortEvents = (list: DeviceActivity[]): DeviceActivity[] => list.sort((a, b) => b.epoch - a.epoch)

export function getNextDeviceEvents(table: Table, items: DeviceActivity[], options: Partial<DeviceActivityOptions>): void {
export const getNextDeviceEvents = (
table: Table,
items: DeviceActivity[],
options: Partial<DeviceActivityOptions>): void => {
for (const item of items) {
const date = new Date(item.time)
const value = JSON.stringify(item.value)
Expand All @@ -64,36 +62,37 @@ export function getNextDeviceEvents(table: Table, items: DeviceActivity[], optio
}
}

export async function writeDeviceEventsTable(
export const writeDeviceEventsTable = async (
command: SmartThingsCommandInterface,
data: PaginatedList<DeviceActivity>,
options?: Partial<DeviceActivityOptions>): Promise<void> {

options?: Partial<DeviceActivityOptions>): Promise<void> => {
const opts = { includeName: false, utcTimeFormat: false, ...options }
const head = options && options.includeName ?
['Time', 'Device Name', 'Component', 'Capability', 'Attribute', 'Value'] :
['Time', 'Component', 'Capability', 'Attribute', 'Value']

if (data.items) {
let table = command.tableGenerator.newOutputTable({ isList: true, head })
const table = command.tableGenerator.newOutputTable({ isList: true, head })
getNextDeviceEvents(table, sortEvents(data.items), opts)
process.stdout.write(table.toString())

let more = 'y'
while (more.toLowerCase() !== 'n' && data.hasNext()) {
more = (await inquirer.prompt({
type: 'input',
while (data.hasNext()) {
const more = (await inquirer.prompt({
type: 'confirm',
name: 'more',
message: 'Fetch more history records ([y]/n)?',
})).more

if (more.toLowerCase() !== 'n') {
table = command.tableGenerator.newOutputTable({ isList: true, head })
await data.next()
if (data.items.length) {
getNextDeviceEvents(table, sortEvents(data.items), opts)
process.stdout.write(table.toString())
}
message: 'Fetch more history records?',
default: true,
})).more as boolean

if (!more) {
break
}

const table = command.tableGenerator.newOutputTable({ isList: true, head })
await data.next()
if (data.items.length) {
getNextDeviceEvents(table, sortEvents(data.items), opts)
process.stdout.write(table.toString())
}
}
}
Expand Down Expand Up @@ -132,6 +131,11 @@ export const getHistory = async (client: SmartThingsClient, limit: number, perRe
const items: DeviceActivity[] = [...history.items]
while (items.length < limit && history.hasNext()) {
await history.next()
// The API allows the user to continue to view history from before the specified "after"
// with paging so we stop processing if we get items that come before the specified "after".
if ((params.after && history.items[0].epoch <= params.after)) {
break
}
items.push(...history.items.slice(0, limit - items.length))
}
return items
Expand Down

0 comments on commit fdf4250

Please sign in to comment.