From 1cc5c768f77ffd8d7ff6b7246fd27cad7c8f823b Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 28 May 2026 15:39:32 +0200 Subject: [PATCH] Migrate flow commands to Zod Migrates all flow commands to use Zod schema validation, replacing the old interface-based Options pattern with exported Zod schemas. Commands migrated: - flow disable, enable, get, export, list, remove - flow owner list, ensure, remove - flow run cancel, get, list, resubmit Closes pnp/cli-microsoft365#7304 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/m365/flow/commands/flow-disable.spec.ts | 2 + src/m365/flow/commands/flow-disable.ts | 47 ++---- src/m365/flow/commands/flow-enable.spec.ts | 2 + src/m365/flow/commands/flow-enable.ts | 47 ++---- src/m365/flow/commands/flow-export.spec.ts | 40 ++--- src/m365/flow/commands/flow-export.ts | 141 ++++++------------ src/m365/flow/commands/flow-get.spec.ts | 2 + src/m365/flow/commands/flow-get.ts | 47 ++---- src/m365/flow/commands/flow-list.spec.ts | 20 +-- src/m365/flow/commands/flow-list.ts | 84 +++-------- src/m365/flow/commands/flow-remove.spec.ts | 30 ++-- src/m365/flow/commands/flow-remove.ts | 67 ++------- .../flow/commands/owner/owner-ensure.spec.ts | 52 ++++--- src/m365/flow/commands/owner/owner-ensure.ts | 118 ++++----------- .../flow/commands/owner/owner-list.spec.ts | 20 ++- src/m365/flow/commands/owner/owner-list.ts | 61 ++------ .../flow/commands/owner/owner-remove.spec.ts | 46 ++++-- src/m365/flow/commands/owner/owner-remove.ts | 110 ++++---------- src/m365/flow/commands/run/run-cancel.spec.ts | 70 +++------ src/m365/flow/commands/run/run-cancel.ts | 66 ++------ src/m365/flow/commands/run/run-get.spec.ts | 24 +-- src/m365/flow/commands/run/run-get.ts | 76 ++-------- src/m365/flow/commands/run/run-list.spec.ts | 42 +++--- src/m365/flow/commands/run/run-list.ts | 116 ++++---------- .../flow/commands/run/run-resubmit.spec.ts | 70 +++------ src/m365/flow/commands/run/run-resubmit.ts | 66 ++------ 26 files changed, 462 insertions(+), 1004 deletions(-) diff --git a/src/m365/flow/commands/flow-disable.spec.ts b/src/m365/flow/commands/flow-disable.spec.ts index d513802fa42..b7c01e93d18 100644 --- a/src/m365/flow/commands/flow-disable.spec.ts +++ b/src/m365/flow/commands/flow-disable.spec.ts @@ -7,6 +7,7 @@ import request from '../../../request.js'; import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; +import { accessToken } from '../../../utils/accessToken.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; import command from './flow-disable.js'; @@ -20,6 +21,7 @@ describe(commands.DISABLE, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; }); diff --git a/src/m365/flow/commands/flow-disable.ts b/src/m365/flow/commands/flow-disable.ts index c43865d2270..1bb02d219f7 100644 --- a/src/m365/flow/commands/flow-disable.ts +++ b/src/m365/flow/commands/flow-disable.ts @@ -1,20 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + environmentName: z.string().alias('e'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - name: string; - environmentName: string; - asAdmin: boolean; -} - class FlowDisableCommand extends PowerAutomateCommand { public get name(): string { return commands.DISABLE; @@ -24,33 +28,8 @@ class FlowDisableCommand extends PowerAutomateCommand { return 'Disables specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--asAdmin' - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/flow-enable.spec.ts b/src/m365/flow/commands/flow-enable.spec.ts index fcfdb628a20..4ad47022f2e 100644 --- a/src/m365/flow/commands/flow-enable.spec.ts +++ b/src/m365/flow/commands/flow-enable.spec.ts @@ -7,6 +7,7 @@ import request from '../../../request.js'; import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; +import { accessToken } from '../../../utils/accessToken.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; import command from './flow-enable.js'; @@ -20,6 +21,7 @@ describe(commands.ENABLE, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; }); diff --git a/src/m365/flow/commands/flow-enable.ts b/src/m365/flow/commands/flow-enable.ts index 7bbea86c545..2c6b59377ab 100644 --- a/src/m365/flow/commands/flow-enable.ts +++ b/src/m365/flow/commands/flow-enable.ts @@ -1,20 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + environmentName: z.string().alias('e'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - name: string; - environmentName: string; - asAdmin: boolean; -} - class FlowEnableCommand extends PowerAutomateCommand { public get name(): string { return commands.ENABLE; @@ -24,33 +28,8 @@ class FlowEnableCommand extends PowerAutomateCommand { return 'Enables specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--asAdmin' - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/flow-export.spec.ts b/src/m365/flow/commands/flow-export.spec.ts index 125b5a3cfd1..ffa21e688db 100644 --- a/src/m365/flow/commands/flow-export.spec.ts +++ b/src/m365/flow/commands/flow-export.spec.ts @@ -12,7 +12,7 @@ import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; -import command from './flow-export.js'; +import command, { options } from './flow-export.js'; import { formatting } from '../../../utils/formatting.js'; import { accessToken } from '../../../utils/accessToken.js'; @@ -21,6 +21,7 @@ describe(commands.EXPORT, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let loggerLogToStderrSpy: sinon.SinonSpy; const actualFilename = `20180916t000000zba9d7134cc81499e9884bf70642afac7_20180916042428.zip`; @@ -120,6 +121,7 @@ describe(commands.EXPORT, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(accessToken, 'assertAccessTokenType').returns(); }); @@ -292,49 +294,49 @@ describe(commands.EXPORT, () => { }); it('fails validation if the id is not a GUID', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: 'abc' }); + assert.strictEqual(actual.success, false); }); it('fails validation if format is specified as neither JSON nor ZIP', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'text' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'text' }); + assert.strictEqual(actual.success, false); }); it('fails validation if format is specified as JSON and packageCreatedBy parameter is specified', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageCreatedBy: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageCreatedBy: 'abc' }); + assert.strictEqual(actual.success, false); }); it('fails validation if format is specified as JSON and packageDescription parameter is specified', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageDescription: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageDescription: 'abc' }); + assert.strictEqual(actual.success, false); }); it('fails validation if format is specified as JSON and packageDisplayName parameter is specified', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageDisplayName: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageDisplayName: 'abc' }); + assert.strictEqual(actual.success, false); }); it('fails validation if format is specified as JSON and packageSourceEnvironment parameter is specified', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageSourceEnvironment: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json', packageSourceEnvironment: 'abc' }); + assert.strictEqual(actual.success, false); }); it('fails validation if specified path doesn\'t exist', async () => { sinon.stub(fs, 'existsSync').callsFake(() => false); - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, path: '/path/not/found.zip' } }, commandInfo); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, path: '/path/not/found.zip' }); sinonUtil.restore(fs.existsSync); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); it('passes validation when the id and environment specified', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}` } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}` }); + assert.strictEqual(actual.success, true); }); it('passes validation when the id and environment specified and format set to JSON', async () => { - const actual = await command.validate({ options: { environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json' } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: foundEnvironmentId, name: `${foundFlowName}`, format: 'json' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/flow/commands/flow-export.ts b/src/m365/flow/commands/flow-export.ts index 21c2594abee..2b5003b87bc 100644 --- a/src/m365/flow/commands/flow-export.ts +++ b/src/m365/flow/commands/flow-export.ts @@ -1,29 +1,34 @@ import fs from 'fs'; import path from 'path'; +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; -import { validation } from '../../../utils/validation.js'; import PowerPlatformCommand from '../../base/PowerPlatformCommand.js'; import commands from '../commands.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + name: z.uuid().alias('n'), + packageDisplayName: z.string().optional().alias('d'), + packageDescription: z.string().optional(), + packageCreatedBy: z.string().optional().alias('c'), + packageSourceEnvironment: z.string().optional().alias('s'), + format: z.string().transform(v => v.toLowerCase()).pipe(z.enum(['json', 'zip'], { + error: 'Option format must be json or zip. Default is zip' + })).optional().alias('f'), + path: z.string().optional().alias('p') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - name: string; - packageDisplayName?: string; - packageDescription?: string; - packageCreatedBy?: string; - packageSourceEnvironment?: string; - format?: string; - path?: string; -} - class FlowExportCommand extends PowerPlatformCommand { public get name(): string { return commands.EXPORT; @@ -33,94 +38,38 @@ class FlowExportCommand extends PowerPlatformCommand { return 'Exports the specified Microsoft Flow as a file'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - packageDisplayName: typeof args.options.packageDisplayName !== 'undefined', - packageDescription: typeof args.options.packageDescription !== 'undefined', - packageCreatedBy: typeof args.options.packageCreatedBy !== 'undefined', - packageSourceEnvironment: typeof args.options.packageSourceEnvironment !== 'undefined', - format: args.options.format, - path: typeof args.options.path !== 'undefined' - }); - }); + public get schema(): z.ZodType { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '-d, --packageDisplayName [packageDisplayName]' - }, - { - option: '--packageDescription [packageDescription]' - }, - { - option: '-c, --packageCreatedBy [packageCreatedBy]' - }, - { - option: '-s, --packageSourceEnvironment [packageSourceEnvironment]' - }, - { - option: '-f, --format [format]' - }, - { - option: '-p, --path [path]' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - const lowerCaseFormat = args.options.format ? args.options.format.toLowerCase() : ''; - - if (!validation.isValidGuid(args.options.name)) { - return `${args.options.name} is not a valid GUID`; - } - - if (args.options.format && (lowerCaseFormat !== 'json' && lowerCaseFormat !== 'zip')) { - return 'Option format must be json or zip. Default is zip'; - } - - if (lowerCaseFormat === 'json') { - if (args.options.packageCreatedBy) { - return 'packageCreatedBy cannot be specified with output of json'; - } - - if (args.options.packageDescription) { - return 'packageDescription cannot be specified with output of json'; - } - - if (args.options.packageDisplayName) { - return 'packageDisplayName cannot be specified with output of json'; - } - - if (args.options.packageSourceEnvironment) { - return 'packageSourceEnvironment cannot be specified with output of json'; - } - } - - if (args.options.path && !fs.existsSync(path.dirname(args.options.path))) { - return 'Specified path where to save the file does not exist'; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => options.format !== 'json' || !options.packageCreatedBy, { + error: 'packageCreatedBy cannot be specified with output of json', + path: ['packageCreatedBy'] + }) + .refine(options => options.format !== 'json' || !options.packageDescription, { + error: 'packageDescription cannot be specified with output of json', + path: ['packageDescription'] + }) + .refine(options => options.format !== 'json' || !options.packageDisplayName, { + error: 'packageDisplayName cannot be specified with output of json', + path: ['packageDisplayName'] + }) + .refine(options => options.format !== 'json' || !options.packageSourceEnvironment, { + error: 'packageSourceEnvironment cannot be specified with output of json', + path: ['packageSourceEnvironment'] + }) + .refine(options => { + if (options.path) { + return fs.existsSync(path.dirname(options.path)); } return true; - } - ); + }, { + error: 'Specified path where to save the file does not exist', + path: ['path'] + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/flow-get.spec.ts b/src/m365/flow/commands/flow-get.spec.ts index f9f2481e820..93279afa304 100644 --- a/src/m365/flow/commands/flow-get.spec.ts +++ b/src/m365/flow/commands/flow-get.spec.ts @@ -7,6 +7,7 @@ import request from '../../../request.js'; import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; +import { accessToken } from '../../../utils/accessToken.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; import command from './flow-get.js'; @@ -24,6 +25,7 @@ describe(commands.GET, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; }); diff --git a/src/m365/flow/commands/flow-get.ts b/src/m365/flow/commands/flow-get.ts index 60dd0dada81..af96490b782 100644 --- a/src/m365/flow/commands/flow-get.ts +++ b/src/m365/flow/commands/flow-get.ts @@ -1,20 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + name: z.string().alias('n'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - name: string; - asAdmin: boolean; -} - interface Trigger { type: string; kind?: string; @@ -49,33 +53,8 @@ class FlowGetCommand extends PowerAutomateCommand { return 'Gets information about the specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--asAdmin' - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/flow-list.spec.ts b/src/m365/flow/commands/flow-list.spec.ts index d816bdc01c7..c15d21d4aa7 100644 --- a/src/m365/flow/commands/flow-list.spec.ts +++ b/src/m365/flow/commands/flow-list.spec.ts @@ -11,7 +11,7 @@ import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; -import command from './flow-list.js'; +import command, { options } from './flow-list.js'; import { accessToken } from '../../../utils/accessToken.js'; describe(commands.LIST, () => { @@ -219,6 +219,7 @@ describe(commands.LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -228,6 +229,7 @@ describe(commands.LIST, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -270,23 +272,23 @@ describe(commands.LIST, () => { }); it('fails validation if asAdmin is specified in combination with a sharingStatus', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, asAdmin: true, sharingStatus: 'all' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, asAdmin: true, sharingStatus: 'all' }); + assert.strictEqual(actual.success, false); }); it('fails validation if sharingStatus is not a valid sharingstatus', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, sharingStatus: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, sharingStatus: 'invalid' }); + assert.strictEqual(actual.success, false); }); it('passes validation if sharingStatus is valid', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, sharingStatus: 'all' } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, sharingStatus: 'all' }); + assert.strictEqual(actual.success, true); }); it('passes validation if asAdmin is passed', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, asAdmin: true } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, asAdmin: true }); + assert.strictEqual(actual.success, true); }); it('retrieves available flows', async () => { diff --git a/src/m365/flow/commands/flow-list.ts b/src/m365/flow/commands/flow-list.ts index 9870f6f6c65..0552f5b5b97 100644 --- a/src/m365/flow/commands/flow-list.ts +++ b/src/m365/flow/commands/flow-list.ts @@ -1,21 +1,25 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import { formatting } from '../../../utils/formatting.js'; import { odata } from '../../../utils/odata.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + sharingStatus: z.enum(['all', 'personal', 'ownedByMe', 'sharedWithMe']).optional(), + withSolutions: z.boolean().optional(), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - sharingStatus?: string; - withSolutions?: boolean; - asAdmin?: boolean; -} - interface PowerAutomateFlow { name: string; id: string; @@ -26,8 +30,6 @@ interface PowerAutomateFlow { } class FlowListCommand extends PowerAutomateCommand { - private allowedSharingStatuses = ['all', 'personal', 'ownedByMe', 'sharedWithMe']; - public get name(): string { return commands.LIST; } @@ -36,66 +38,18 @@ class FlowListCommand extends PowerAutomateCommand { return 'Lists Power Automate flows in the given environment'; } - public defaultProperties(): string[] | undefined { - return ['name', 'displayName']; - } - - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initTypes(); + public get schema(): z.ZodType { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - sharingStatus: typeof args.options.sharingStatus !== 'undefined', - withSolutions: !!args.options.withSolutions, - asAdmin: !!args.options.asAdmin - }); + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema.refine(options => !(options.asAdmin && options.sharingStatus), { + error: 'The options asAdmin and sharingStatus cannot be specified together.' }); } - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '--sharingStatus [sharingStatus]', - autocomplete: this.allowedSharingStatuses - }, - { - option: '--withSolutions' - }, - { - option: '--asAdmin' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.asAdmin && args.options.sharingStatus) { - return `The options asAdmin and sharingStatus cannot be specified together.`; - } - - if (args.options.sharingStatus && !this.allowedSharingStatuses.some(status => status === args.options.sharingStatus)) { - return `${args.options.sharingStatus} is not a valid sharing status. Allowed values are: ${this.allowedSharingStatuses.join(', ')}`; - } - - return true; - } - ); - } - - #initTypes(): void { - this.types.string.push('environmentName', 'sharingStatus'); - this.types.boolean.push('withSolutions', 'asAdmin'); + public defaultProperties(): string[] | undefined { + return ['name', 'displayName']; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/flow-remove.spec.ts b/src/m365/flow/commands/flow-remove.spec.ts index 12543149d0a..6b2cabf2c68 100644 --- a/src/m365/flow/commands/flow-remove.spec.ts +++ b/src/m365/flow/commands/flow-remove.spec.ts @@ -9,14 +9,16 @@ import request from '../../../request.js'; import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; +import { accessToken } from '../../../utils/accessToken.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import commands from '../commands.js'; -import command from './flow-remove.js'; +import command, { options } from './flow-remove.js'; describe(commands.REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let loggerLogToStderrSpy: sinon.SinonSpy; let promptIssued: boolean = false; @@ -25,8 +27,10 @@ describe(commands.REMOVE, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -72,23 +76,19 @@ describe(commands.REMOVE, () => { }); it('fails validation if the name is not valid GUID', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - name: 'invalid' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + name: 'invalid' + }); + assert.strictEqual(actual.success, false); }); it('passes validation when the name and environment specified', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - name: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72' - } - }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + name: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72' + }); + assert.strictEqual(actual.success, true); }); it('prompts before removing the specified Microsoft Flow owned by the currently signed-in user when force option not passed', async () => { diff --git a/src/m365/flow/commands/flow-remove.ts b/src/m365/flow/commands/flow-remove.ts index 115016f4a11..881c438a04b 100644 --- a/src/m365/flow/commands/flow-remove.ts +++ b/src/m365/flow/commands/flow-remove.ts @@ -1,23 +1,26 @@ -import GlobalOptions from '../../../GlobalOptions.js'; +import { z } from 'zod'; import { cli } from '../../../cli/cli.js'; import { Logger } from '../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; -import { validation } from '../../../utils/validation.js'; import commands from '../commands.js'; import PowerAutomateCommand from '../../base/PowerAutomateCommand.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.uuid().alias('n'), + environmentName: z.string().alias('e'), + asAdmin: z.boolean().optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - name: string; - asAdmin?: boolean; - force?: boolean; -} - class FlowRemoveCommand extends PowerAutomateCommand { public get name(): string { return commands.REMOVE; @@ -27,50 +30,8 @@ class FlowRemoveCommand extends PowerAutomateCommand { return 'Removes the specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin, - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--asAdmin' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.name)) { - return `${args.options.name} is not a valid GUID`; - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/owner/owner-ensure.spec.ts b/src/m365/flow/commands/owner/owner-ensure.spec.ts index 46a77ef0f31..bf993316efb 100644 --- a/src/m365/flow/commands/owner/owner-ensure.spec.ts +++ b/src/m365/flow/commands/owner/owner-ensure.spec.ts @@ -12,9 +12,10 @@ import { entraUser } from '../../../../utils/entraUser.js'; import { formatting } from '../../../../utils/formatting.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './owner-ensure.js'; +import command, { options } from './owner-ensure.js'; describe(commands.OWNER_ENSURE, () => { const validEnvironmentName = 'Default-6a2903af-9c03-4c02-a50b-e7419599925b'; @@ -28,14 +29,17 @@ describe(commands.OWNER_ENSURE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -77,34 +81,44 @@ describe(commands.OWNER_ENSURE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if flowName is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: 'invalid', userId: validUserId, roleName: validRoleName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if flowName is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: 'invalid', userId: validUserId, roleName: validRoleName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: validFlowName, userId: 'invalid', roleName: validRoleName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, userId: 'invalid', roleName: validRoleName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if groupId is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: validFlowName, groupId: 'invalid', roleName: validRoleName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if groupId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, groupId: 'invalid', roleName: validRoleName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if username is not a valid user principal name', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: validFlowName, userName: 'invalid', roleName: validRoleName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if username is not a valid user principal name', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, userName: 'invalid', roleName: validRoleName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if roleName is not a valid role name', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: validFlowName, userName: validUserName, roleName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if roleName is not a valid role name', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, userName: validUserName, roleName: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when required parameters are provided', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironmentName, flowName: validFlowName, userName: validUserName, roleName: validRoleName } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation when no owner identifier is provided', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, roleName: validRoleName }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation when multiple owner identifiers are provided', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, userId: validUserId, groupId: validGroupId, roleName: validRoleName }); + assert.strictEqual(actual.success, false); + }); + + it('passes validation when required parameters are provided', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironmentName, flowName: validFlowName, userName: validUserName, roleName: validRoleName }); + assert.strictEqual(actual.success, true); }); it('adds owner to a flow with userId', async () => { diff --git a/src/m365/flow/commands/owner/owner-ensure.ts b/src/m365/flow/commands/owner/owner-ensure.ts index 3e1a4b7fb4d..67f9e495599 100644 --- a/src/m365/flow/commands/owner/owner-ensure.ts +++ b/src/m365/flow/commands/owner/owner-ensure.ts @@ -1,5 +1,6 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; @@ -8,24 +9,27 @@ import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + flowName: z.uuid(), + environmentName: z.string().alias('e'), + roleName: z.enum(['CanView', 'CanEdit']), + userId: z.uuid().optional(), + userName: z.string().refine(name => validation.isValidUserPrincipalName(name), { + error: e => `'${e.input}' is not a valid userName.` + }).optional(), + groupId: z.uuid().optional(), + groupName: z.string().optional(), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - flowName: string; - environmentName: string; - roleName: string; - userId?: string; - userName?: string; - groupId?: string; - groupName?: string; - asAdmin?: boolean; -} - class FlowOwnerEnsureCommand extends PowerAutomateCommand { - private static readonly allowedRoleNames: string[] = ['CanView', 'CanEdit']; - public get name(): string { return commands.OWNER_ENSURE; } @@ -34,85 +38,19 @@ class FlowOwnerEnsureCommand extends PowerAutomateCommand { return 'Assigns/updates permissions to a Power Automate flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin, - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined', - groupId: typeof args.options.groupId !== 'undefined', - groupName: typeof args.options.groupName !== 'undefined' - }); - }); + public get schema(): z.ZodType { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema.refine( + options => [options.userId, options.userName, options.groupId, options.groupName].filter(x => x !== undefined).length === 1, { - option: '--flowName ' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - }, - { - option: '--groupId [groupId]' - }, - { - option: '--groupName [groupName]' - }, - { - option: '--roleName ', - autocomplete: FlowOwnerEnsureCommand.allowedRoleNames - }, - { - option: '--asAdmin' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['userId', 'userName', 'groupId', 'groupName'] }); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID.`; - } - - if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `${args.options.userId} is not a valid GUID.`; + error: 'Specify either userId, userName, groupId, or groupName, but not multiple.', + params: { + customCode: 'optionSet', + options: ['userId', 'userName', 'groupId', 'groupName'] } - - if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { - return `${args.options.userName} is not a valid userName.`; - } - - if (args.options.groupId && !validation.isValidGuid(args.options.groupId)) { - return `${args.options.groupId} is not a valid GUID.`; - } - - if (FlowOwnerEnsureCommand.allowedRoleNames.indexOf(args.options.roleName) === -1) { - return `${args.options.roleName} is not a valid roleName. Valid values are: ${FlowOwnerEnsureCommand.allowedRoleNames.join(', ')}`; - } - - return true; } ); } @@ -174,4 +112,4 @@ class FlowOwnerEnsureCommand extends PowerAutomateCommand { } } -export default new FlowOwnerEnsureCommand(); \ No newline at end of file +export default new FlowOwnerEnsureCommand(); \ No newline at end of file diff --git a/src/m365/flow/commands/owner/owner-list.spec.ts b/src/m365/flow/commands/owner/owner-list.spec.ts index 997ccdd661d..5ba40fad64e 100644 --- a/src/m365/flow/commands/owner/owner-list.spec.ts +++ b/src/m365/flow/commands/owner/owner-list.spec.ts @@ -10,9 +10,10 @@ import { telemetry } from '../../../../telemetry.js'; import { formatting } from '../../../../utils/formatting.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './owner-list.js'; +import command, { options } from './owner-list.js'; describe(commands.OWNER_LIST, () => { const environmentName = 'Default-d87a7535-dd31-4437-bfe1-95340acd55c6'; @@ -27,13 +28,17 @@ describe(commands.OWNER_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -50,7 +55,6 @@ describe(commands.OWNER_LIST, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); - commandInfo = cli.getCommandInfo(command); }); afterEach(() => { @@ -115,13 +119,13 @@ describe(commands.OWNER_LIST, () => { new CommandError(error.error.message)); }); - it('fails validation if flowName is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if flowName is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if flowName a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if flowName a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName }); + assert.strictEqual(actual.success, true); }); }); \ No newline at end of file diff --git a/src/m365/flow/commands/owner/owner-list.ts b/src/m365/flow/commands/owner/owner-list.ts index 0c09d2bd7c1..441e0c664d8 100644 --- a/src/m365/flow/commands/owner/owner-list.ts +++ b/src/m365/flow/commands/owner/owner-list.ts @@ -1,9 +1,9 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; -import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; @@ -25,16 +25,19 @@ interface FlowPermissionPrincipal { type: string; } +export const options = z.strictObject({ + ...globalOptionsZod.shape, + flowName: z.uuid(), + environmentName: z.string().alias('e'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - flowName: string; - environmentName: string; - asAdmin?: boolean; -} - class FlowOwnerListCommand extends PowerAutomateCommand { public get name(): string { return commands.OWNER_LIST; @@ -48,46 +51,8 @@ class FlowOwnerListCommand extends PowerAutomateCommand { return ['roleName', 'id', 'type']; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '--flowName ' - }, - { - option: '--asAdmin' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID.`; - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/owner/owner-remove.spec.ts b/src/m365/flow/commands/owner/owner-remove.spec.ts index 51c3c47ebec..0e77ab27bea 100644 --- a/src/m365/flow/commands/owner/owner-remove.spec.ts +++ b/src/m365/flow/commands/owner/owner-remove.spec.ts @@ -13,9 +13,10 @@ import { formatting } from '../../../../utils/formatting.js'; import { settingsNames } from '../../../../settingsNames.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './owner-remove.js'; +import command, { options } from './owner-remove.js'; describe(commands.OWNER_REMOVE, () => { const environmentName = 'Default-d87a7535-dd31-4437-bfe1-95340acd55c6'; @@ -32,6 +33,7 @@ describe(commands.OWNER_REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean = false; before(() => { @@ -39,8 +41,10 @@ describe(commands.OWNER_REMOVE, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -232,28 +236,38 @@ describe(commands.OWNER_REMOVE, () => { assert(postSpy.notCalled); }); - it('fails validation if flowName is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: 'invalid', userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if flowName is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: 'invalid', userId: userId }); + assert.strictEqual(actual.success, false); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, userId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, userId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if groupId is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, groupId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if groupId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, groupId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if username is not a valid user principal name', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, userName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if username is not a valid user principal name', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, userName: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if groupName passed', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, groupName: groupName } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation when no owner identifier is provided', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation when multiple owner identifiers are provided', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, userId: userId, groupId: groupId }); + assert.strictEqual(actual.success, false); + }); + + it('passes validation if groupName passed', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, groupName: groupName }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/flow/commands/owner/owner-remove.ts b/src/m365/flow/commands/owner/owner-remove.ts index 80217c48617..e8b11bfeb5a 100644 --- a/src/m365/flow/commands/owner/owner-remove.ts +++ b/src/m365/flow/commands/owner/owner-remove.ts @@ -1,6 +1,7 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; @@ -9,21 +10,26 @@ import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + flowName: z.uuid(), + environmentName: z.string().alias('e'), + userId: z.uuid().optional(), + userName: z.string().refine(name => validation.isValidUserPrincipalName(name), { + error: e => `'${e.input}' is not a valid userName.` + }).optional(), + groupId: z.uuid().optional(), + groupName: z.string().optional(), + asAdmin: z.boolean().optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - flowName: string; - environmentName: string; - userId?: string; - userName?: string; - groupId?: string; - groupName?: string; - asAdmin?: boolean; - force?: boolean; -} - class FlowOwnerRemoveCommand extends PowerAutomateCommand { public get name(): string { return commands.OWNER_REMOVE; @@ -33,85 +39,23 @@ class FlowOwnerRemoveCommand extends PowerAutomateCommand { return 'Removes owner permissions to a Power Automate flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); + public get schema(): z.ZodType { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined', - groupId: typeof args.options.groupId !== 'undefined', - groupName: typeof args.options.groupName !== 'undefined', - asAdmin: !!args.options.asAdmin, - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '--flowName ' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - }, - { - option: '--groupId [groupId]' - }, - { - option: '--groupName [groupName]' - }, - { - option: '--asAdmin' - }, + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema.refine( + options => [options.userId, options.userName, options.groupId, options.groupName].filter(x => x !== undefined).length === 1, { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID.`; + error: 'Specify either userId, userName, groupId, or groupName, but not multiple.', + params: { + customCode: 'optionSet', + options: ['userId', 'userName', 'groupId', 'groupName'] } - - if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `${args.options.userId} is not a valid GUID.`; - } - - if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { - return `${args.options.userName} is not a valid userName.`; - } - - if (args.options.groupId && !validation.isValidGuid(args.options.groupId)) { - return `${args.options.groupId} is not a valid GUID.`; - } - - return true; } ); } - #initOptionSets(): void { - this.optionSets.push({ options: ['userId', 'userName', 'groupId', 'groupName'] }); - } - public async commandAction(logger: Logger, args: CommandArgs): Promise { try { if (this.verbose) { diff --git a/src/m365/flow/commands/run/run-cancel.spec.ts b/src/m365/flow/commands/run/run-cancel.spec.ts index bacf89e7428..33d81fd19bf 100644 --- a/src/m365/flow/commands/run/run-cancel.spec.ts +++ b/src/m365/flow/commands/run/run-cancel.spec.ts @@ -9,15 +9,17 @@ import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './run-cancel.js'; +import command, { options } from './run-cancel.js'; describe(commands.RUN_CANCEL, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean = false; before(() => { @@ -25,8 +27,10 @@ describe(commands.RUN_CANCEL, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -71,26 +75,22 @@ describe(commands.RUN_CANCEL, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the flowName is not valid GUID', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - flowName: 'invalid', - name: '08585981115186985105550762687CU161' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the flowName is not valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + flowName: 'invalid', + name: '08585981115186985105550762687CU161' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the name, environmentName and flowName specified', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - flowName: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72', - name: '08585981115186985105550762687CU161' - } - }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when the name, environmentName and flowName specified', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + flowName: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72', + name: '08585981115186985105550762687CU161' + }); + assert.strictEqual(actual.success, true); }); it('prompts before cancelling the specified Microsoft FlowName when force option not passed', async () => { @@ -284,36 +284,4 @@ describe(commands.RUN_CANCEL, () => { } as any), new CommandError(`Request to Azure Resource Manager failed with error: '{"error":{"code":"WorkflowRunNotFound","message":"The workflow '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72' run '08585981115186985105550762688CP233' could not be found."}}`)); }); - it('supports specifying name', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--name') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying environment', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--environment') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying flow', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--flow') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); }); diff --git a/src/m365/flow/commands/run/run-cancel.ts b/src/m365/flow/commands/run/run-cancel.ts index 121e1e06d8e..a045802b094 100644 --- a/src/m365/flow/commands/run/run-cancel.ts +++ b/src/m365/flow/commands/run/run-cancel.ts @@ -1,23 +1,26 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; -import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + flowName: z.uuid(), + environmentName: z.string().alias('e'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - flowName: string; - name: string; - force?: boolean; -} - class FlowRunCancelCommand extends PowerAutomateCommand { public get name(): string { return commands.RUN_CANCEL; @@ -27,49 +30,8 @@ class FlowRunCancelCommand extends PowerAutomateCommand { return 'Cancels a specific run of the specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '--flowName ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID`; - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/run/run-get.spec.ts b/src/m365/flow/commands/run/run-get.spec.ts index 9f82c9ee7de..ddbbfccd244 100644 --- a/src/m365/flow/commands/run/run-get.spec.ts +++ b/src/m365/flow/commands/run/run-get.spec.ts @@ -7,11 +7,12 @@ import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { cli } from '../../../../cli/cli.js'; -import command from './run-get.js'; +import command, { options } from './run-get.js'; describe(commands.RUN_GET, () => { const flowName = '396d5ec9-ae2d-4a84-967d-cd7f56cd8f30'; @@ -216,14 +217,17 @@ describe(commands.RUN_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -400,18 +404,18 @@ describe(commands.RUN_GET, () => { new CommandError(`Could not find flow '${flowName}'.`)); }); - it('fails validation if the flowName is not valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: 'invalid', name: runName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the flowName is not valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: 'invalid', name: runName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the withActions parameter is not valid boolean or string', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, name: runName, withActions: -1 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the withActions parameter is not valid boolean or string', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, name: runName, withActions: -1 }); + assert.strictEqual(actual.success, false); }); - it('passes validation if the flowName is not valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, name: runName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if the flowName is not valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, name: runName }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/flow/commands/run/run-get.ts b/src/m365/flow/commands/run/run-get.ts index 740e3f441a8..5c0960fd699 100644 --- a/src/m365/flow/commands/run/run-get.ts +++ b/src/m365/flow/commands/run/run-get.ts @@ -1,23 +1,26 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; -import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + flowName: z.uuid(), + environmentName: z.string().alias('e'), + withTrigger: z.boolean().optional(), + withActions: z.union([z.string(), z.boolean()]).optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - flowName: string; - name: string; - withTrigger?: boolean; - withActions?: string | boolean; -} - interface FlowLink { uri: string; } @@ -79,57 +82,8 @@ class FlowRunGetCommand extends PowerAutomateCommand { return 'Gets information about a specific run of the specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '--flowName ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--withTrigger' - }, - { - option: '--withActions [withActions]' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID`; - } - - if (args.options.withActions && (typeof args.options.withActions !== 'string' && typeof args.options.withActions !== 'boolean')) { - return 'the withActions parameter must be a string or boolean'; - } - - return true; - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - withTrigger: !!args.options.withTrigger, - withActions: typeof args.options.withActions !== 'undefined' - }); - }); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/run/run-list.spec.ts b/src/m365/flow/commands/run/run-list.spec.ts index 91b1073624a..0c9ff8f7450 100644 --- a/src/m365/flow/commands/run/run-list.spec.ts +++ b/src/m365/flow/commands/run/run-list.spec.ts @@ -9,9 +9,10 @@ import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './run-list.js'; +import command, { options } from './run-list.js'; describe(commands.RUN_LIST, () => { const environmentName = 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c'; @@ -113,14 +114,17 @@ describe(commands.RUN_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -294,33 +298,33 @@ describe(commands.RUN_LIST, () => { new CommandError('An error has occurred')); }); - it('fails validation if the flowName is not a valid GUID', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the flowName is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the status is not a valid status', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, status: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the status is not a valid status', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, status: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the triggerStartTime is not a valid ISO datetime', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, triggerStartTime: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the triggerStartTime is not a valid ISO datetime', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, triggerStartTime: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the triggerEndTime is not a valid ISO datetime', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, triggerEndTime: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the triggerEndTime is not a valid ISO datetime', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, triggerEndTime: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the output is not json and withTrigger is specified', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, output: 'text', withTrigger: true } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the output is not json and withTrigger is specified', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, output: 'text', withTrigger: true }); + assert.strictEqual(actual.success, false); }); - it('passes validation if all options are passed properly', async () => { - const actual = await command.validate({ options: { environmentName: environmentName, flowName: flowName, status: status, triggerStartTime: triggerStartTime, triggerEndTime: triggerEndTime } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if all options are passed properly', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: environmentName, flowName: flowName, status: status, triggerStartTime: triggerStartTime, triggerEndTime: triggerEndTime }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/flow/commands/run/run-list.ts b/src/m365/flow/commands/run/run-list.ts index c13e5ccfb26..647507d04ae 100644 --- a/src/m365/flow/commands/run/run-list.ts +++ b/src/m365/flow/commands/run/run-list.ts @@ -1,5 +1,6 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; @@ -7,20 +8,27 @@ import { validation } from '../../../../utils/validation.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + flowName: z.uuid(), + environmentName: z.string().alias('e'), + status: z.enum(['Succeeded', 'Running', 'Failed', 'Cancelled']).optional(), + triggerStartTime: z.string().refine(date => validation.isValidISODateTime(date), { + error: e => `'${e.input}' is not a valid datetime.` + }).optional(), + triggerEndTime: z.string().refine(date => validation.isValidISODateTime(date), { + error: e => `'${e.input}' is not a valid datetime.` + }).optional(), + withTrigger: z.boolean().optional(), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - flowName: string; - status?: string; - triggerStartTime?: string; - triggerEndTime?: string; - withTrigger?: boolean - asAdmin?: boolean; -} - interface PowerAutomateFlowRun { name: string; startTime: string; @@ -38,8 +46,6 @@ interface PowerAutomateFlowRun { } class FlowRunListCommand extends PowerAutomateCommand { - public readonly allowedStatusses: string[] = ['Succeeded', 'Running', 'Failed', 'Cancelled']; - public get name(): string { return commands.RUN_LIST; } @@ -48,83 +54,23 @@ class FlowRunListCommand extends PowerAutomateCommand { return 'Lists runs of the specified Microsoft Flow'; } - public defaultProperties(): string[] | undefined { - return ['name', 'startTime', 'status']; + public get schema(): z.ZodType { + return options; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - status: typeof args.options.status !== 'undefined', - triggerStartTime: typeof args.options.triggerStartTime !== 'undefined', - triggerEndTime: typeof args.options.triggerEndTime !== 'undefined', - withTrigger: !!args.options.withTrigger, - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '--flowName ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '--status [status]', - autocomplete: this.allowedStatusses - }, - { - option: '--triggerStartTime [triggerStartTime]' - }, - { - option: '--triggerEndTime [triggerEndTime]' - }, - { - option: '--withTrigger' - }, - { - option: '--asAdmin' + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema.refine(options => { + if (options.output !== 'json' && options.withTrigger) { + return false; } - ); + return true; + }, { + error: 'The --withTrigger option is only available when output is set to json' + }); } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID`; - } - - if (args.options.status && this.allowedStatusses.indexOf(args.options.status) === -1) { - return `'${args.options.status}' is not a valid status. Allowed values are: ${this.allowedStatusses.join(',')}`; - } - - if (args.options.triggerStartTime && !validation.isValidISODateTime(args.options.triggerStartTime)) { - return `'${args.options.triggerStartTime}' is not a valid datetime.`; - } - - if (args.options.triggerEndTime && !validation.isValidISODateTime(args.options.triggerEndTime)) { - return `'${args.options.triggerEndTime}' is not a valid datetime.`; - } - - if (args.options.output !== 'json' && args.options.withTrigger) { - return 'The --withTrigger option is only available when output is set to json'; - } - - return true; - } - ); + public defaultProperties(): string[] | undefined { + return ['name', 'startTime', 'status']; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/flow/commands/run/run-resubmit.spec.ts b/src/m365/flow/commands/run/run-resubmit.spec.ts index 5f560bc4e03..4f44941db9b 100644 --- a/src/m365/flow/commands/run/run-resubmit.spec.ts +++ b/src/m365/flow/commands/run/run-resubmit.spec.ts @@ -9,15 +9,17 @@ import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; +import { accessToken } from '../../../../utils/accessToken.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './run-resubmit.js'; +import command, { options } from './run-resubmit.js'; describe(commands.RUN_RESUBMIT, () => { let log: string[]; let logger: Logger; let loggerLogToStderrSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean = false; before(() => { @@ -25,8 +27,10 @@ describe(commands.RUN_RESUBMIT, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -72,26 +76,22 @@ describe(commands.RUN_RESUBMIT, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the flowName is not valid GUID', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - flowName: 'invalid', - name: '08585981115186985105550762687CU161' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the flowName is not valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + flowName: 'invalid', + name: '08585981115186985105550762687CU161' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the name, environmentName and flowName specified', async () => { - const actual = await command.validate({ - options: { - environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', - flowName: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72', - name: '08585981115186985105550762687CU161' - } - }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when the name, environmentName and flowName specified', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: 'Default-eff8592e-e14a-4ae8-8771-d96d5c549e1c', + flowName: '0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac72', + name: '08585981115186985105550762687CU161' + }); + assert.strictEqual(actual.success, true); }); it('prompts before resubmitting the specified Microsoft Flow when force option not passed', async () => { @@ -309,36 +309,4 @@ describe(commands.RUN_RESUBMIT, () => { assert.strictEqual(postStub.lastCall.args[0].url, 'https://api.flow.microsoft.com/providers/Microsoft.ProcessSimple/environments/Default-d87a7535-dd31-4437-bfe1-95340acd55c6/flows/0f64d9dd-01bb-4c1b-95b3-cb4a1a08ac88/triggers/manual/histories/08585981115186985105550762687CU161/resubmit?api-version=2016-11-01'); }); - it('supports specifying name', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--name') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying environment', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--environment') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying flow', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--flow') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); }); diff --git a/src/m365/flow/commands/run/run-resubmit.ts b/src/m365/flow/commands/run/run-resubmit.ts index 38e57e93228..f5d59448451 100644 --- a/src/m365/flow/commands/run/run-resubmit.ts +++ b/src/m365/flow/commands/run/run-resubmit.ts @@ -1,24 +1,27 @@ +import { z } from 'zod'; import chalk from 'chalk'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; -import { validation } from '../../../../utils/validation.js'; import commands from '../../commands.js'; import PowerAutomateCommand from '../../../base/PowerAutomateCommand.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + flowName: z.uuid(), + environmentName: z.string().alias('e'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - force: boolean; - environmentName: string; - flowName: string; - name: string; -} - class FlowRunResubmitCommand extends PowerAutomateCommand { public get name(): string { return commands.RUN_RESUBMIT; @@ -28,49 +31,8 @@ class FlowRunResubmitCommand extends PowerAutomateCommand { return 'Resubmits a specific flow run for the specified Microsoft Flow'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '--flowName ' - }, - { - option: '-e, --environmentName ' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.flowName)) { - return `${args.options.flowName} is not a valid GUID`; - } - - return true; - } - ); + public get schema(): z.ZodType { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise {