Skip to content

Commit

Permalink
indexer-common,cli: Add actions update ... command to CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
hopeyen authored and fordN committed Dec 12, 2022
1 parent aa5ea42 commit d820a46
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 39 deletions.
2 changes: 1 addition & 1 deletion packages/indexer-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"scripts": {
"format": "prettier --write 'src/**/*.ts'",
"lint": "eslint . --ext .ts,.tsx --fix",
"compile": "tsc",
"compile": "tsc --build",
"prepare": "yarn format && yarn lint && yarn compile",
"disputes": "yarn prepare && ./dist/cli.js indexer disputes get",
"clean": "rm -rf ./node_modules ./dist ./tsconfig.tsbuildinfo",
Expand Down
4 changes: 2 additions & 2 deletions packages/indexer-cli/src/__tests__/indexer/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('Indexer actions tests', () => {
afterAll(teardown)
describe('Actions help', () => {
cliTest('Indexer actions', ['indexer', 'actions'], 'references/indexer-actions', {
expectedExitCode: 1,
expectedExitCode: 255,
cwd: baseDir,
timeout: 10000,
})
Expand All @@ -18,7 +18,7 @@ describe('Indexer actions tests', () => {
['indexer', 'actions', '--help'],
'references/indexer-actions',
{
expectedExitCode: 1,
expectedExitCode: 255,
cwd: baseDir,
timeout: 10000,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Manage indexer actions

indexer actions update Update one or more actions
indexer actions queue Queue an action item
indexer actions get List one or more actions
indexer actions execute Execute approved items in the action queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Manage indexer configuration
indexer allocations create Create an allocation
indexer allocations close Close an allocation
indexer allocations Manage indexer allocations
indexer actions update Update one or more actions
indexer actions queue Queue an action item
indexer actions get List one or more actions
indexer actions execute Execute approved items in the action queue
Expand Down
129 changes: 128 additions & 1 deletion packages/indexer-cli/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import {
ActionResult,
ActionStatus,
ActionType,
ActionUpdateInput,
IndexerManagementClient,
nullPassThrough,
OrderDirection,
parseBoolean,
} from '@graphprotocol/indexer-common'
import { validateRequiredParams } from './command-helpers'
import { validatePOI, validateRequiredParams } from './command-helpers'
import gql from 'graphql-tag'
import { utils } from 'ethers'
import { parseGRT } from '@graphprotocol/common-ts'

export interface GenericActionInputParams {
targetDeployment: string
Expand Down Expand Up @@ -101,6 +105,63 @@ export async function validateActionInput(
)
}

export function validateActionType(input: string): ActionType {
const validVariants = Object.keys(ActionType).map(variant =>
variant.toLocaleLowerCase(),
)
if (!validVariants.includes(input.toLocaleLowerCase())) {
throw Error(
`Invalid 'ActionType' "${input}", must be one of ['${validVariants.join(`', '`)}']`,
)
}
return ActionType[input.toUpperCase() as keyof typeof ActionType]
}

export function validateActionStatus(input: string): ActionStatus {
const validVariants = Object.keys(ActionStatus).map(variant =>
variant.toLocaleLowerCase(),
)
if (!validVariants.includes(input.toLocaleLowerCase())) {
throw Error(
`Invalid 'ActionStatus' "${input}", must be one of ['${validVariants.join(
`', '`,
)}']`,
)
}
return ActionStatus[input.toUpperCase() as keyof typeof ActionStatus]
}

export function buildActionFilter(
id: string | undefined,
type: string | undefined,
status: string | undefined,
source: string | undefined,
reason: string | undefined,
): ActionFilter {
const filter: ActionFilter = {}
if (id) {
filter.id = +id
}
if (type) {
filter.type = validateActionType(type)
}
if (status) {
filter.status = validateActionStatus(status)
}
if (source) {
filter.source = source
}
if (reason) {
filter.reason = reason
}
if (Object.keys(filter).length === 0) {
throw Error(
`No action filter provided, please specify at least one filter using ['--id', '--type', '--status', '--source', '--reason']`,
)
}
return filter
}

export async function queueActions(
client: IndexerManagementClient,
actions: ActionInput[],
Expand Down Expand Up @@ -135,6 +196,35 @@ export async function queueActions(
return result.data.queueActions
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ACTION_PARAMS_PARSERS: Record<keyof ActionUpdateInput, (x: never) => any> = {
deploymentID: x => nullPassThrough(x),
allocationID: x => x,
amount: nullPassThrough(parseGRT),
poi: nullPassThrough((x: string) => validatePOI(x)),
force: x => parseBoolean(x),
type: x => validateActionType(x),
status: x => validateActionStatus(x),
reason: nullPassThrough,
}

/**
* Parses a user-provided action update input into a normalized form.
*/
export const parseActionUpdateInput = (input: object): ActionUpdateInput => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const obj = {} as any
for (const [key, value] of Object.entries(input)) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj[key] = (ACTION_PARAMS_PARSERS as any)[key](value)
} catch {
throw new Error(key)
}
}
return obj as ActionUpdateInput
}

export async function executeApprovedActions(
client: IndexerManagementClient,
): Promise<ActionResult[]> {
Expand Down Expand Up @@ -357,3 +447,40 @@ export async function deleteActions(

return result.data.deleteActions
}

export async function updateActions(
client: IndexerManagementClient,
filter: ActionFilter,
action: ActionUpdateInput,
): Promise<ActionResult[]> {
const result = await client
.mutation(
gql`
mutation updateActions($filter: ActionFilter!, $action: ActionUpdateInput!) {
updateActions(filter: $filter, action: $action) {
id
type
allocationID
deploymentID
amount
poi
force
source
reason
priority
transaction
status
failureReason
}
}
`,
{ filter, action },
)
.toPromise()

if (result.error) {
throw result.error
}

return result.data.updateActions
}
6 changes: 4 additions & 2 deletions packages/indexer-cli/src/command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,17 @@ export async function validateRequiredParams(
}
}

export async function validatePOI(poi: string | undefined): Promise<string | undefined> {
export function validatePOI(poi: string | undefined): string | undefined {
if (poi !== undefined) {
if (typeof poi == 'number' && poi == 0) {
poi = utils.hexlify(Array(32).fill(0))
}
// Ensure user provided POI is formatted properly - '0x...' (32 bytes)
const isHex = utils.isHexString(poi, 32)
if (!isHex) {
throw new Error('Must be a 32 byte length hex string')
throw new Error(
`Invalid POI provided ('${poi}'): Must be a 32 byte length hex string`,
)
}
}
return poi
Expand Down
2 changes: 1 addition & 1 deletion packages/indexer-cli/src/commands/indexer/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ module.exports = {
const { print } = toolbox
print.info(toolbox.command?.description)
print.printCommands(toolbox, ['indexer', 'actions'])
process.exitCode = 1
process.exitCode = -1
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ module.exports = {
})
numericActionIDs = queuedActions.map(action => action.id)
if (numericActionIDs.length === 0) {
throw Error(`No 'queued' actions found.`)
throw Error(`No 'queued' actions found`)
}
} else {
numericActionIDs = actionIDs.map(action => +action)
Expand Down
2 changes: 1 addition & 1 deletion packages/indexer-cli/src/commands/indexer/actions/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ module.exports = {
}

if (!['undefined', 'number'].includes(typeof first)) {
throw Error(`Invalid value for '--first' option, must have a numeric value.`)
throw Error(`Invalid value for '--first' option, must have a numeric value`)
}

if (!['undefined', 'string'].includes(typeof fields)) {
Expand Down
12 changes: 3 additions & 9 deletions packages/indexer-cli/src/commands/indexer/actions/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import chalk from 'chalk'
import { loadValidatedConfig } from '../../../config'
import { createIndexerManagementClient } from '../../../client'
import { printObjectOrArray } from '../../../command-helpers'
import { buildActionInput, queueActions } from '../../../actions'
import { ActionInput, ActionStatus, ActionType } from '@graphprotocol/indexer-common'
import { buildActionInput, queueActions, validateActionType } from '../../../actions'
import { ActionInput, ActionStatus } from '@graphprotocol/indexer-common'

const HELP = `
${chalk.bold(
Expand Down Expand Up @@ -65,14 +65,8 @@ module.exports = {
)
}

if (!['allocate', 'unallocate', 'reallocate'].includes(type)) {
throw Error(
`Invalid 'ActionType' "${type}", must be one of ['allocate', 'unallocate', 'reallocate']`,
)
}

actionInputParams = await buildActionInput(
ActionType[type.toUpperCase() as keyof typeof ActionType],
validateActionType(type),
{ targetDeployment, param1, param2, param3, param4 },
decisionSource,
decisionReason,
Expand Down
121 changes: 121 additions & 0 deletions packages/indexer-cli/src/commands/indexer/actions/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { GluegunToolbox } from 'gluegun'
import chalk from 'chalk'

import { Action, ActionFilter, ActionUpdateInput } from '@graphprotocol/indexer-common'
import { loadValidatedConfig } from '../../../config'
import { createIndexerManagementClient } from '../../../client'
import { fixParameters, printObjectOrArray } from '../../../command-helpers'
import {
buildActionFilter,
parseActionUpdateInput,
updateActions,
} from '../../../actions'
import { partition } from '@thi.ng/iterators'

const HELP = `
${chalk.bold('graph indexer actions update')} [options] [<key1> <value1> ...]
${chalk.dim('Options:')}
-h, --help Show usage information
--id <actionID> Filter by actionID
--type allocate|unallocate|reallocate Filter by type
--status queued|approved|pending|success|failed|canceled Filter by status
--source <source> Filter by source
--reason <reason> Filter by reason string
-o, --output table|json|yaml Choose the output format: table (default), JSON, or YAML
`

module.exports = {
name: 'update',
alias: [],
description: 'Update one or more actions',
run: async (toolbox: GluegunToolbox) => {
const { print, parameters } = toolbox

const inputSpinner = toolbox.print.spin('Processing inputs')

const { id, type, status, source, reason, h, help, o, output } = parameters.options

const [...setValues] = fixParameters(parameters, { h, help }) || []
let updateActionInput: ActionUpdateInput = {}
let actionFilter: ActionFilter = {}

const outputFormat = o || output || 'table'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (help || h) {
inputSpinner.stopAndPersist({ symbol: '💁', text: HELP })
return
}
try {
if (!['json', 'yaml', 'table'].includes(outputFormat)) {
throw Error(
`Invalid output format "${outputFormat}" must be one of ['json', 'yaml' or 'table']`,
)
}

// 1. Convert all `null` strings to real nulls, and other values
// to regular JS strings (which for some reason they are not...)
const kvs = setValues.map(param => (param === 'null' ? null : param.toString()))

// 2. Check that all key/value pairs are complete and
// there's no value missing at the end
if (kvs.length % 2 !== 0) {
throw Error(`An uneven number of key/value pairs was passed in: ${kvs.join(' ')}`)
}

updateActionInput = parseActionUpdateInput({
...Object.fromEntries([...partition(2, 2, kvs)]),
})

actionFilter = buildActionFilter(id, type, status, source, reason)

inputSpinner.succeed('Processed input parameters')
} catch (error) {
inputSpinner.fail(error.toString())
print.info(HELP)
process.exitCode = 1
return
}

const actionSpinner = toolbox.print.spin('Updating actions')

try {
const config = loadValidatedConfig()
const client = await createIndexerManagementClient({ url: config.api })

const actionsUpdated = await updateActions(client, actionFilter, updateActionInput)

if (!actionsUpdated || actionsUpdated.length === 0) {
print.info('No actions found')
process.exitCode = 1
return
}

actionSpinner.succeed(`'${actionsUpdated.length}' actions updated`)

const displayProperties: (keyof Action)[] = [
'id',
'type',
'deploymentID',
'allocationID',
'amount',
'poi',
'force',
'priority',
'status',
'source',
'failureReason',
'transaction',
'reason',
]

printObjectOrArray(print, outputFormat, actionsUpdated, displayProperties)
} catch (error) {
actionSpinner.fail(error.toString())
process.exitCode = 1
return
}
},
}
Loading

0 comments on commit d820a46

Please sign in to comment.