From 67c668a1ab776c899fdf2bb9e8775b1923d203f9 Mon Sep 17 00:00:00 2001 From: Davi Aquino Date: Thu, 12 Oct 2023 10:17:01 -0300 Subject: [PATCH] feat: add reactor management commands (#17) - use new reactor `code` field - deprecate formulas --- package.json | 2 +- src/commands/reactorFormulas/index.ts | 9 ++- src/commands/reactorFormulas/update.ts | 5 +- src/commands/reactors/create.ts | 52 +++++++++++++ src/commands/reactors/delete.ts | 40 ++++++++++ src/commands/reactors/index.ts | 10 ++- src/commands/reactors/update.ts | 100 +++++++++++++++++++++++++ src/reactors/management.ts | 36 ++++++++- src/reactors/utils.ts | 70 +++++++++++++++++ yarn.lock | 8 +- 10 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 src/commands/reactors/create.ts create mode 100644 src/commands/reactors/delete.ts create mode 100644 src/commands/reactors/update.ts create mode 100644 src/reactors/utils.ts diff --git a/package.json b/package.json index fb72240..b10d4ac 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "/oclif.manifest.json" ], "dependencies": { - "@basis-theory/basis-theory-js": "^1.79.2", + "@basis-theory/basis-theory-js": "^2.3.1", "@inquirer/confirm": "^2.0.3", "@inquirer/input": "^1.2.1", "@inquirer/prompts": "^2.1.1", diff --git a/src/commands/reactorFormulas/index.ts b/src/commands/reactorFormulas/index.ts index 391fdc0..9c671d6 100644 --- a/src/commands/reactorFormulas/index.ts +++ b/src/commands/reactorFormulas/index.ts @@ -4,7 +4,7 @@ import { selectReactorFormula } from '../../reactorFormulas/management'; export default class ReactorFormulas extends BaseCommand { public static description = - 'List Reactor Formulas. Requires `reactor:read` Management Application permission'; + '[Deprecated] List Reactor Formulas. Requires `reactor:read` Management Application permission'; public static examples = ['<%= config.bin %> <%= command.id %>']; @@ -19,14 +19,17 @@ export default class ReactorFormulas extends BaseCommand { public static args = {}; public async run(): Promise { + this.log( + 'WARNING: Reactor Formulas are being deprecated and will be removed in a future release.' + ); const { bt, flags: { page }, } = await this.parse(ReactorFormulas); - const reactor = await selectReactorFormula(bt, page); + const formula = await selectReactorFormula(bt, page); - if (!reactor) { + if (!formula) { return undefined; } diff --git a/src/commands/reactorFormulas/update.ts b/src/commands/reactorFormulas/update.ts index 7efe6d8..891a4e4 100644 --- a/src/commands/reactorFormulas/update.ts +++ b/src/commands/reactorFormulas/update.ts @@ -9,7 +9,7 @@ import { export default class Update extends BaseCommand { public static description = - 'Updates an existing Reactor Formula. Requires `reactor:update` Management Application permission'; + '[Deprecated] Updates an existing Reactor Formula. Requires `reactor:update` Management Application permission'; public static examples = [ '<%= config.bin %> <%= command.id %> 03858bf5-32d3-4a2e-b74b-daeea0883bca', @@ -35,6 +35,9 @@ export default class Update extends BaseCommand { }; public async run(): Promise { + this.log( + 'WARNING: Reactor Formulas are being deprecated and will be removed in a future release.' + ); const { bt, args: { id }, diff --git a/src/commands/reactors/create.ts b/src/commands/reactors/create.ts new file mode 100644 index 0000000..913cb61 --- /dev/null +++ b/src/commands/reactors/create.ts @@ -0,0 +1,52 @@ +import { BaseCommand } from '../../base'; +import { createReactor } from '../../reactors/management'; +import { createModelFromFlags, REACTOR_FLAGS } from '../../reactors/utils'; +import { promptStringIfUndefined } from '../../utils'; + +export default class Create extends BaseCommand { + public static description = + 'Creates a new Reactor. Requires `reactor:create` Management Application permission'; + + public static examples = ['<%= config.bin %> <%= command.id %> ']; + + public static flags = { + ...REACTOR_FLAGS, + }; + + public async run(): Promise { + const { flags, bt } = await this.parse(Create); + + const name = await promptStringIfUndefined(flags.name, { + message: 'What is the Reactor name?', + validate: (value) => Boolean(value), + }); + + const code = await promptStringIfUndefined(flags['code'], { + message: 'Enter the Reactor code file path:', + validate: (value) => Boolean(value), + }); + + const applicationId = await promptStringIfUndefined( + flags['application-id'], + { + message: '(Optional) Enter the Application ID to use in the Reactor:', + } + ); + + const configuration = await promptStringIfUndefined(flags.configuration, { + message: '(Optional) Enter the configuration file path (.env format):', + }); + + const model = createModelFromFlags({ + name, + code, + applicationId, + configuration, + }); + + const { id } = await createReactor(bt, model); + + this.log('Reactor created successfully!'); + this.log(`id: ${id}`); + } +} diff --git a/src/commands/reactors/delete.ts b/src/commands/reactors/delete.ts new file mode 100644 index 0000000..64067dd --- /dev/null +++ b/src/commands/reactors/delete.ts @@ -0,0 +1,40 @@ +import { Args, Flags } from '@oclif/core'; +import { BaseCommand } from '../../base'; +import { deleteReactor } from '../../reactors/management'; + +export default class Delete extends BaseCommand { + public static description = + 'Deletes a Reactor. Requires `reactor:delete` and `reactor:read` Management Application permissions'; + + public static examples = [ + '<%= config.bin %> <%= command.id %> 03858bf5-32d3-4a2e-b74b-daeea0883bca', + ]; + + public static args = { + id: Args.string({ + description: 'Reactor id to delete', + required: true, + }), + }; + + public static flags = { + yes: Flags.boolean({ + char: 'y', + description: 'auto confirm the operation', + default: false, + allowNo: false, + }), + }; + + public async run(): Promise { + const { + bt, + flags: { yes }, + args: { id }, + } = await this.parse(Delete); + + if (await deleteReactor(bt, id, yes)) { + this.log('Reactor deleted successfully!'); + } + } +} diff --git a/src/commands/reactors/index.ts b/src/commands/reactors/index.ts index 9311d22..40c4896 100644 --- a/src/commands/reactors/index.ts +++ b/src/commands/reactors/index.ts @@ -2,7 +2,7 @@ import { select } from '@inquirer/prompts'; import { Flags } from '@oclif/core'; import { BaseCommand } from '../../base'; import { showReactorLogs } from '../../logs'; -import { selectReactor } from '../../reactors/management'; +import { deleteReactor, selectReactor } from '../../reactors/management'; export default class Reactors extends BaseCommand { public static description = @@ -44,6 +44,10 @@ export default class Reactors extends BaseCommand { value: 'logs', description: 'See Reactor real-time logs', }, + { + name: 'Delete', + value: 'delete', + }, ], }); @@ -57,6 +61,10 @@ export default class Reactors extends BaseCommand { return showReactorLogs(bt, reactor.id); } + if (action === 'delete' && (await deleteReactor(bt, reactor.id))) { + return this.log('Reactor deleted successfully!'); + } + return undefined; } } diff --git a/src/commands/reactors/update.ts b/src/commands/reactors/update.ts new file mode 100644 index 0000000..271e048 --- /dev/null +++ b/src/commands/reactors/update.ts @@ -0,0 +1,100 @@ +import { Args, Flags, ux } from '@oclif/core'; +import { BaseCommand } from '../../base'; +import { watchForChanges } from '../../files'; +import { showReactorLogs } from '../../logs'; +import { patchReactor } from '../../reactors/management'; +import { createModelFromFlags, REACTOR_FLAGS } from '../../reactors/utils'; + +export default class Update extends BaseCommand { + public static description = + 'Updates an existing Reactor. Requires `reactor:update` Management Application permission'; + + public static examples = [ + '<%= config.bin %> <%= command.id %> 03858bf5-32d3-4a2e-b74b-daeea0883bca', + '<%= config.bin %> <%= command.id %> 03858bf5-32d3-4a2e-b74b-daeea0883bca --code ./reactor.js', + '<%= config.bin %> <%= command.id %> 03858bf5-32d3-4a2e-b74b-daeea0883bca --configuration ./.env.reactor', + ]; + + public static flags = { + ...REACTOR_FLAGS, + watch: Flags.boolean({ + char: 'w', + description: 'Watch for changes in informed files', + default: false, + required: false, + }), + logs: Flags.boolean({ + char: 'l', + description: 'Start logs server after update', + default: false, + required: false, + }), + }; + + public static args = { + id: Args.string({ + description: 'Reactor id to update', + required: true, + }), + }; + + public async run(): Promise { + const { + bt, + args: { id }, + flags: { + name, + code, + 'application-id': applicationId, + configuration, + watch, + logs, + }, + } = await this.parse(Update); + + const model = createModelFromFlags({ + name, + code, + applicationId, + configuration, + }); + + await patchReactor(bt, id, model); + + this.log('Reactor updated successfully!'); + + if (logs) { + await showReactorLogs(bt, id); + } + + if (watch) { + const entries = Object.entries({ + code, + configuration, + }).filter(([, value]) => Boolean(value)) as [string, string][]; + + const files = entries.reduce( + (arr, [, file]) => [...arr, file], + [] as string[] + ); + + if (files.length) { + this.log(`Watching files for changes: ${files.join(', ')} `); + } + + entries.forEach(([prop, file]) => { + watchForChanges(file, async () => { + ux.action.start(`Detected change in ${file}. Pushing changes`); + await patchReactor( + bt, + id, + createModelFromFlags({ + [prop]: file, + }) + ); + ux.action.stop('✅\t'); + }); + }); + } + } +} diff --git a/src/reactors/management.ts b/src/reactors/management.ts index aa2013a..194989b 100644 --- a/src/reactors/management.ts +++ b/src/reactors/management.ts @@ -1,11 +1,13 @@ import type { Reactor, PatchReactor, + CreateReactor, } from '@basis-theory/basis-theory-js/types/models'; import type { BasisTheory as IBasisTheory, PaginatedList, } from '@basis-theory/basis-theory-js/types/sdk'; +import confirm from '@inquirer/confirm'; import { ux } from '@oclif/core'; import type { TableRow } from '../types'; import { selectOrNavigate } from '../utils'; @@ -73,6 +75,38 @@ const selectReactor = async ( return selection; }; +const createReactor = ( + bt: IBasisTheory, + model: CreateReactor +): Promise => { + debug(`Creating Reactor`, JSON.stringify(model, undefined, 2)); + + return bt.reactors.create(model); +}; + +const deleteReactor = async ( + bt: IBasisTheory, + id: string, + force = false +): Promise => { + if (!force) { + const proceed = await confirm({ + message: `Are you sure you want to delete this Reactor (${id})?`, + default: false, + }); + + if (!proceed) { + return false; + } + } + + debug(`Deleting Reactor`, id); + + await bt.reactors.delete(id); + + return true; +}; + const patchReactor = ( bt: IBasisTheory, id: string, @@ -83,4 +117,4 @@ const patchReactor = ( return bt.reactors.patch(id, model); }; -export { selectReactor, patchReactor }; +export { selectReactor, createReactor, patchReactor, deleteReactor }; diff --git a/src/reactors/utils.ts b/src/reactors/utils.ts new file mode 100644 index 0000000..937907f --- /dev/null +++ b/src/reactors/utils.ts @@ -0,0 +1,70 @@ +import type { + CreateReactor as CreateReactorModel, + PatchReactor as PatchReactorModel, +} from '@basis-theory/basis-theory-js/types/models/reactors'; +import { Flags } from '@oclif/core'; +import { parse } from 'dotenv'; +import { readFileContents } from '../files'; + +const REACTOR_FLAGS = { + name: Flags.string({ + char: 'n', + description: 'name of the Reactor', + }), + configuration: Flags.file({ + char: 'c', + description: + 'path to configuration file (.env format) to use in the Reactor', + }), + 'application-id': Flags.string({ + char: 'i', + description: 'application ID to use in the Reactor', + }), + code: Flags.file({ + char: 'r', + description: 'path to JavaScript file containing the Reactor code', + }), +}; + +interface ReactorFlagProps { + /** + * Reactor's application id + */ + applicationId?: string; + /** + * Path to code file + */ + code?: string; + /** + * Path to .env file + */ + configuration?: string; +} + +type CreateReactor = ReactorFlagProps & + Omit; +type PatchReactor = ReactorFlagProps & + Omit; + +function createModelFromFlags(payload: CreateReactor): CreateReactorModel; + +function createModelFromFlags(payload: PatchReactor): PatchReactorModel; + +// eslint-disable-next-line get-off-my-lawn/prefer-arrow-functions +function createModelFromFlags({ + name, + applicationId, + code, + configuration, +}: CreateReactor | PatchReactor): CreateReactorModel | PatchReactorModel { + return { + name, + code: code ? readFileContents(code) : undefined, + application: applicationId ? { id: applicationId } : undefined, + configuration: configuration + ? parse(readFileContents(configuration)) + : undefined, + }; +} + +export { REACTOR_FLAGS, createModelFromFlags }; diff --git a/yarn.lock b/yarn.lock index 60c0ba7..ddd5482 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,10 +381,10 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" -"@basis-theory/basis-theory-js@^1.79.2": - version "1.79.2" - resolved "https://registry.yarnpkg.com/@basis-theory/basis-theory-js/-/basis-theory-js-1.79.2.tgz#7cf72b092492369cb193d97d8736eecf27e2dfe4" - integrity sha512-3Uy6kjSNF/wTk1Ux+DVIdxeNycayB4HmDLhfUK0nuNtBiFOnNda6SnXDrGY3l1u/DKJ5SpjggyJk4Mdfr4nzBw== +"@basis-theory/basis-theory-js@^2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@basis-theory/basis-theory-js/-/basis-theory-js-2.3.1.tgz#5b39f1a213f410aa3a3d3b6028d889a3412d671d" + integrity sha512-Fzu4BxongcYy3gcYdnKCIM34gwLkj4UuPLt3pFBlnYxzHDNBnsCdUkWoDygljQNzQCbkqUhj0wRa9r+33ONpPQ== dependencies: axios "^1.4.0" camelcase-keys "^6.2.2"