From 4136c7ffc4d95bbf05d32eab02ec0a0e0c037435 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 25 Jun 2026 09:15:50 +0200 Subject: [PATCH 1/3] Migrate pp gateway, managementapp, solution, and tenant commands to Zod Migrates the following commands from legacy #initOptions()/#initValidators()/ #initTelemetry() pattern to Zod schema-based validation: - pp gateway get - pp managementapp add - pp managementapp list - pp solution get - pp solution list - pp solution publish - pp solution publisher add - pp solution publisher get - pp solution publisher list - pp solution publisher remove - pp solution remove - pp tenant settings set Closes #7314 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../pp/commands/gateway/gateway-get.spec.ts | 45 +-- src/m365/pp/commands/gateway/gateway-get.ts | 37 +- .../managementapp/managementapp-add.spec.ts | 122 +++---- .../managementapp/managementapp-add.ts | 74 ++-- .../managementapp/managementapp-list.spec.ts | 15 +- .../managementapp/managementapp-list.ts | 8 + .../pp/commands/solution/solution-get.spec.ts | 49 +-- src/m365/pp/commands/solution/solution-get.ts | 81 ++--- .../commands/solution/solution-list.spec.ts | 25 +- .../pp/commands/solution/solution-list.ts | 42 +-- .../solution/solution-publish.spec.ts | 53 +-- .../pp/commands/solution/solution-publish.ts | 87 ++--- .../solution/solution-publisher-add.spec.ts | 121 +++---- .../solution/solution-publisher-add.ts | 101 ++---- .../solution/solution-publisher-get.spec.ts | 45 ++- .../solution/solution-publisher-get.ts | 81 ++--- .../solution/solution-publisher-list.spec.ts | 19 +- .../solution/solution-publisher-list.ts | 48 +-- .../solution-publisher-remove.spec.ts | 51 +-- .../solution/solution-publisher-remove.ts | 92 ++--- .../commands/solution/solution-remove.spec.ts | 55 +-- .../pp/commands/solution/solution-remove.ts | 87 ++--- .../tenant/tenant-settings-set.spec.ts | 75 ++-- .../pp/commands/tenant/tenant-settings-set.ts | 342 +++++------------- 24 files changed, 664 insertions(+), 1091 deletions(-) diff --git a/src/m365/pp/commands/gateway/gateway-get.spec.ts b/src/m365/pp/commands/gateway/gateway-get.spec.ts index e7990e333b2..ff8c5294f8d 100644 --- a/src/m365/pp/commands/gateway/gateway-get.spec.ts +++ b/src/m365/pp/commands/gateway/gateway-get.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 './gateway-get.js'; +import command, { options } from './gateway-get.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.GATEWAY_GET, () => { @@ -20,6 +20,7 @@ describe(commands.GATEWAY_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const gateway: any = { id: "1f69e798-5852-4fdd-ab01-33bb14b6e934", @@ -39,6 +40,7 @@ describe(commands.GATEWAY_GET, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -74,30 +76,19 @@ describe(commands.GATEWAY_GET, () => { assert.notStrictEqual(command.description, null); }); - it("fails validation if the id is not valid", async () => { - const actual = await command.validate( - { - options: { - id: "3eb1a01b" - } - }, - commandInfo - ); - - assert.notStrictEqual(actual, true); + it("fails validation if the id is not valid", () => { + const actual = commandOptionsSchema.safeParse({ id: "3eb1a01b" }); + assert.strictEqual(actual.success, false); }); - it("passes validation if the id is valid", async () => { - const actual = await command.validate( - { - options: { - id: "1f69e798-5852-4fdd-ab01-33bb14b6e934" - } - }, - commandInfo - ); + it("passes validation if the id is valid", () => { + const actual = commandOptionsSchema.safeParse({ id: "1f69e798-5852-4fdd-ab01-33bb14b6e934" }); + assert.strictEqual(actual.success, true); + }); - assert.strictEqual(actual, true); + it("fails validation with unknown options", () => { + const actual = commandOptionsSchema.safeParse({ id: "1f69e798-5852-4fdd-ab01-33bb14b6e934", unknownOption: "value" }); + assert.strictEqual(actual.success, false); }); it("correctly handles error", async () => { @@ -107,9 +98,9 @@ describe(commands.GATEWAY_GET, () => { await assert.rejects( command.action(logger, { - options: { - id: 'testid' - } + options: commandOptionsSchema.parse({ + id: '1f69e798-5852-4fdd-ab01-33bb14b6e934' + }) }), new CommandError("An error has occurred") ); @@ -128,9 +119,9 @@ describe(commands.GATEWAY_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: "1f69e798-5852-4fdd-ab01-33bb14b6e934" - } + }) }); const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; diff --git a/src/m365/pp/commands/gateway/gateway-get.ts b/src/m365/pp/commands/gateway/gateway-get.ts index 54a55f58943..6c03070f376 100644 --- a/src/m365/pp/commands/gateway/gateway-get.ts +++ b/src/m365/pp/commands/gateway/gateway-get.ts @@ -1,11 +1,25 @@ +import { z } from 'zod'; import { Logger } from "../../../../cli/Logger.js"; -import { CommandArgs } from "../../../../Command.js"; +import { globalOptionsZod } from "../../../../Command.js"; import request from "../../../../request.js"; import { formatting } from "../../../../utils/formatting.js"; import { validation } from "../../../../utils/validation.js"; import PowerBICommand from "../../../base/PowerBICommand.js"; import commands from "../../commands.js"; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i') +}); + +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + class PpGatewayGetCommand extends PowerBICommand { public get name(): string { return commands.GATEWAY_GET; @@ -15,25 +29,8 @@ class PpGatewayGetCommand extends PowerBICommand { return "Gets information about the specified gateway."; } - constructor() { - super(); - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift({ - option: "-i, --id " - }); - } - - #initValidators(): void { - this.validators.push(async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id as string)) { - return `${args.options.id} is not a valid GUID`; - } - return true; - }); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/managementapp/managementapp-add.spec.ts b/src/m365/pp/commands/managementapp/managementapp-add.spec.ts index 043a8cd289f..fff14ebcb8e 100644 --- a/src/m365/pp/commands/managementapp/managementapp-add.spec.ts +++ b/src/m365/pp/commands/managementapp/managementapp-add.spec.ts @@ -11,8 +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 './managementapp-add.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import command, { options } from './managementapp-add.js'; import { accessToken } from '../../../../utils/accessToken.js'; import { entraApp } from '../../../../utils/entraApp.js'; @@ -21,6 +20,7 @@ describe(commands.MANAGEMENTAPP_ADD, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -30,6 +30,7 @@ describe(commands.MANAGEMENTAPP_ADD, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -77,9 +78,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { sinon.stub(entraApp, 'getAppRegistrationByAppName').rejects(new Error(error)); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ name: 'My app' - } + }) }), new CommandError(error)); }); @@ -88,9 +89,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { sinon.stub(entraApp, 'getAppRegistrationByAppName').rejects(new Error(error)); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ name: 'My app' - } + }) }), new CommandError(error)); }); @@ -98,9 +99,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { sinon.stub(request, 'get').rejects(new Error('An error has occurred')); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' - } + }) }), new CommandError(`An error has occurred`)); }); @@ -108,87 +109,60 @@ describe(commands.MANAGEMENTAPP_ADD, () => { sinon.stub(request, 'get').rejects(new Error('An error has occurred')); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ name: 'My app' - } + }) }), new CommandError(`An error has occurred`)); }); - it('fails validation if appId and objectId specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', objectId: 'c75be2e1-0204-4f95-857d-51a37cf40be8' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if appId and objectId specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', objectId: 'c75be2e1-0204-4f95-857d-51a37cf40be8' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if appId and name specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', name: 'My app' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if appId and name specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', name: 'My app' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if objectId and name specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', name: 'My app' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if objectId and name specified', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', name: 'My app' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if neither appId, objectId, nor name specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); + it('fails validation if neither appId, objectId, nor name specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); + }); - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the objectId is not a valid guid', () => { + const actual = commandOptionsSchema.safeParse({ objectId: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the objectId is not a valid guid', async () => { - const actual = await command.validate({ options: { objectId: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appId is not a valid guid', () => { + const actual = commandOptionsSchema.safeParse({ appId: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the appId is not a valid guid', async () => { - const actual = await command.validate({ options: { appId: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('passes validation if required options specified (appId)', () => { + const actual = commandOptionsSchema.safeParse({ appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (appId)', async () => { - const actual = await command.validate({ options: { appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (objectId)', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (objectId)', async () => { - const actual = await command.validate({ options: { objectId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ name: 'My app' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { name: 'My app' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f', unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('successfully registers app as managementapp when passing appId', async () => { @@ -203,9 +177,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ appId: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' - } + }) }); const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; assert.strictEqual(call.args[0].applicationId, '9b1b1e42-794b-4c71-93ac-5ed92488b67f'); @@ -231,9 +205,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ name: 'My Test App', debug: true - } + }) }); const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; assert.strictEqual(call.args[0].applicationId, '9b1b1e42-794b-4c71-93ac-5ed92488b67f'); @@ -259,9 +233,9 @@ describe(commands.MANAGEMENTAPP_ADD, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ objectId: '340a4aa3-1af6-43ac-87d8-189819003952', debug: true - } + }) }); const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; assert.strictEqual(call.args[0].applicationId, '9b1b1e42-794b-4c71-93ac-5ed92488b67f'); diff --git a/src/m365/pp/commands/managementapp/managementapp-add.ts b/src/m365/pp/commands/managementapp/managementapp-add.ts index c3c02b0d2eb..2b40a081be2 100644 --- a/src/m365/pp/commands/managementapp/managementapp-add.ts +++ b/src/m365/pp/commands/managementapp/managementapp-add.ts @@ -1,21 +1,29 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; import { entraApp } from '../../../../utils/entraApp.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + appId: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional(), + objectId: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional(), + name: z.string().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - appId?: string; - objectId?: string; - name?: string; -} - class PpManagementAppAddCommand extends PowerPlatformCommand { public get name(): string { return commands.MANAGEMENTAPP_ADD; @@ -25,51 +33,19 @@ class PpManagementAppAddCommand extends PowerPlatformCommand { return 'Registers management application for Power Platform'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - appId: typeof args.options.appId !== 'undefined', - objectId: typeof args.options.objectId !== 'undefined', - name: typeof args.options.name !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { option: '--appId [appId]' }, - { option: '--objectId [objectId]' }, - { option: '--name [name]' } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.appId && !validation.isValidGuid(args.options.appId as string)) { - return `${args.options.appId} is not a valid GUID`; - } - - if (args.options.objectId && !validation.isValidGuid(args.options.objectId as string)) { - return `${args.options.objectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.appId, opts.objectId, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'appId', 'objectId', or 'name', but not multiple.`, + params: { + customCode: 'optionSet', + options: ['appId', 'objectId', 'name'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['appId', 'objectId', 'name'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/managementapp/managementapp-list.spec.ts b/src/m365/pp/commands/managementapp/managementapp-list.spec.ts index 1c29995c1fd..3b13c39e2f9 100644 --- a/src/m365/pp/commands/managementapp/managementapp-list.spec.ts +++ b/src/m365/pp/commands/managementapp/managementapp-list.spec.ts @@ -1,6 +1,8 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -9,12 +11,14 @@ 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 './managementapp-list.js'; +import command, { options } from './managementapp-list.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.MANAGEMENTAPP_LIST, () => { let log: string[]; let logger: Logger; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -23,6 +27,8 @@ describe(commands.MANAGEMENTAPP_LIST, () => { 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(() => { @@ -127,6 +133,11 @@ describe(commands.MANAGEMENTAPP_LIST, () => { throw 'An error has occurred'; }); - await assert.rejects(command.action(logger, { options: {} } as any), new CommandError('An error has occurred')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({}) }), new CommandError('An error has occurred')); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); }); diff --git a/src/m365/pp/commands/managementapp/managementapp-list.ts b/src/m365/pp/commands/managementapp/managementapp-list.ts index 781137da967..a2ebcdb831f 100644 --- a/src/m365/pp/commands/managementapp/managementapp-list.ts +++ b/src/m365/pp/commands/managementapp/managementapp-list.ts @@ -1,8 +1,12 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import { odata } from '../../../../utils/odata.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; +export const options = globalOptionsZod.strict(); + interface ManagementApp { applicationId: string } @@ -16,6 +20,10 @@ class PpManagementAppListCommand extends PowerPlatformCommand { return 'Lists management applications for Power Platform'; } + public get schema(): z.ZodTypeAny | undefined { + return options; + } + public async commandAction(logger: Logger): Promise { const endpoint = `${this.resource}/providers/Microsoft.BusinessAppPlatform/adminApplications?api-version=2020-06-01`; diff --git a/src/m365/pp/commands/solution/solution-get.spec.ts b/src/m365/pp/commands/solution/solution-get.spec.ts index 811663b96e3..ff848e844ae 100644 --- a/src/m365/pp/commands/solution/solution-get.spec.ts +++ b/src/m365/pp/commands/solution/solution-get.spec.ts @@ -12,11 +12,12 @@ import { session } from '../../../../utils/session.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-get.js'; +import command, { options } from './solution-get.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_GET, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const validId = 'ee62fd63-e49e-4c09-80de-8fae1b9a427e'; const validName = 'Default'; @@ -56,6 +57,7 @@ describe(commands.SOLUTION_GET, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -95,43 +97,46 @@ describe(commands.SOLUTION_GET, () => { assert.notStrictEqual(command.description, null); }); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + it('fails validation when no solution found', async () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); sinon.stub(powerPlatform, 'getSolutionByName').rejects(new Error(`The specified solution '${validName}' does not exist.`)); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ environmentName: validEnvironment, name: validName - } + }) }), new CommandError(`The specified solution '${validName}' does not exist.`)); }); - it('fails validation if the id is not a valid guid', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - id: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id is not a valid guid', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + id: 'Invalid GUID' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName }); + assert.strictEqual(actual.success, true); }); it('retrieves a specific solution from power platform environment with the name parameter', async () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); sinon.stub(powerPlatform, 'getSolutionByName').callsFake(async () => solutionResponse.value[0]); - await command.action(logger, { options: { verbose: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default' }) }); assert(loggerLogSpy.calledWith(solutionResponse.value[0])); }); @@ -139,7 +144,7 @@ describe(commands.SOLUTION_GET, () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); sinon.stub(powerPlatform, 'getSolutionByName').callsFake(async () => solutionResponse.value[0]); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default', output: 'text' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default', output: 'text' }) }); assert(loggerLogSpy.calledWith(solutionResponseText)); }); @@ -156,7 +161,7 @@ describe(commands.SOLUTION_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', id: 'ee62fd63-e49e-4c09-80de-8fae1b9a427e' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', id: 'ee62fd63-e49e-4c09-80de-8fae1b9a427e' }) }); assert(loggerLogSpy.calledWith(solutionResponse.value[0])); }); @@ -173,7 +178,7 @@ describe(commands.SOLUTION_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', id: 'ee62fd63-e49e-4c09-80de-8fae1b9a427e', output: 'text' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', id: 'ee62fd63-e49e-4c09-80de-8fae1b9a427e', output: 'text' }) }); assert(loggerLogSpy.calledWith(solutionResponseText)); }); @@ -190,7 +195,7 @@ describe(commands.SOLUTION_GET, () => { } }); - await assert.rejects(command.action(logger, { options: { environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', name: 'Default' }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); }); diff --git a/src/m365/pp/commands/solution/solution-get.ts b/src/m365/pp/commands/solution/solution-get.ts index bcec662b0bb..405b63e0ef7 100644 --- a/src/m365/pp/commands/solution/solution-get.ts +++ b/src/m365/pp/commands/solution/solution-get.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 { powerPlatform } from '../../../../utils/powerPlatform.js'; import { validation } from '../../../../utils/validation.js'; @@ -8,17 +9,22 @@ import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; import { Publisher, Solution } from './Solution.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional().alias('i'), + name: z.string().optional().alias('n'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -export interface Options extends GlobalOptions { - environmentName: string; - id?: string; - name?: string; - asAdmin?: boolean; -} - class PpSolutionGetCommand extends PowerPlatformCommand { public get name(): string { return commands.SOLUTION_GET; @@ -28,58 +34,19 @@ class PpSolutionGetCommand extends PowerPlatformCommand { return 'Gets a specific solution in a given environment.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - asAdmin: !!args.options.asAdmin - }); - }); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '-i, --id [id]' - }, - { - option: '-n, --name [name]' - }, - { - option: '--asAdmin' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'name'] } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'id' or 'name', but not both.`, + params: { + customCode: 'optionSet', + options: ['id', 'name'] } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/solution/solution-list.spec.ts b/src/m365/pp/commands/solution/solution-list.spec.ts index 80cea3f896a..bf16a59f746 100644 --- a/src/m365/pp/commands/solution/solution-list.spec.ts +++ b/src/m365/pp/commands/solution/solution-list.spec.ts @@ -1,6 +1,8 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -9,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 './solution-list.js'; +import command, { options } from './solution-list.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_LIST, () => { @@ -74,6 +76,8 @@ describe(commands.SOLUTION_LIST, () => { "publisher": "Dynamics 365" } ]; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -85,6 +89,8 @@ describe(commands.SOLUTION_LIST, () => { 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(() => { @@ -126,6 +132,11 @@ describe(commands.SOLUTION_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['uniquename', 'version', 'publisher']); }); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + it('retrieves solutions from power platform environment', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url === `https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments/4be50206-9576-4237-8b17-38d8aadfaa36?api-version=2020-10-01&$select=properties.linkedEnvironmentMetadata.instanceApiUrl`)) { @@ -147,7 +158,7 @@ describe(commands.SOLUTION_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36' }) }); assert(loggerLogSpy.calledWith(solutionResponse.value)); }); @@ -172,7 +183,7 @@ describe(commands.SOLUTION_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', output: 'json' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', output: 'json' }) }); assert(loggerLogSpy.calledWith(solutionResponse.value)); }); @@ -197,7 +208,7 @@ describe(commands.SOLUTION_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', asAdmin: true, output: 'json' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', asAdmin: true, output: 'json' }) }); assert(loggerLogSpy.calledWith(solutionResponse.value)); }); @@ -223,7 +234,7 @@ describe(commands.SOLUTION_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', output: 'text' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36', output: 'text' }) }); assert(loggerLogSpy.calledWith(solutionResponseText)); }); @@ -240,7 +251,7 @@ describe(commands.SOLUTION_LIST, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: {} }), + await assert.rejects(command.action(logger, { options: {} as any }), new CommandError(`The environment 'undefined' could not be retrieved. See the inner exception for more details: undefined`)); }); @@ -272,7 +283,7 @@ describe(commands.SOLUTION_LIST, () => { }); - await assert.rejects(command.action(logger, { options: { environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: '4be50206-9576-4237-8b17-38d8aadfaa36' }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); }); diff --git a/src/m365/pp/commands/solution/solution-list.ts b/src/m365/pp/commands/solution/solution-list.ts index 3f692e8d6c1..d46379e2967 100644 --- a/src/m365/pp/commands/solution/solution-list.ts +++ b/src/m365/pp/commands/solution/solution-list.ts @@ -1,21 +1,25 @@ +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 { odata } from '../../../../utils/odata.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; import { Publisher, Solution } from './Solution.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - asAdmin: boolean; -} - class PpSolutionListCommand extends PowerPlatformCommand { public get name(): string { return commands.SOLUTION_LIST; @@ -29,30 +33,8 @@ class PpSolutionListCommand extends PowerPlatformCommand { return ['uniquename', 'version', 'publisher']; } - 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: '-e, --environmentName ' - }, - { - option: '--asAdmin' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/solution/solution-publish.spec.ts b/src/m365/pp/commands/solution/solution-publish.spec.ts index bfddccb9c53..847def939d6 100644 --- a/src/m365/pp/commands/solution/solution-publish.spec.ts +++ b/src/m365/pp/commands/solution/solution-publish.spec.ts @@ -12,11 +12,12 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-publish.js'; +import command, { options } from './solution-publish.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_PUBLISH, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; //#region Mocked Responses const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const validId = '00000001-0000-0000-0001-00000000009b'; @@ -64,6 +65,7 @@ describe(commands.SOLUTION_PUBLISH, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -106,29 +108,32 @@ describe(commands.SOLUTION_PUBLISH, () => { assert.notStrictEqual(command.description, null); }); - it('defines correct option sets', () => { - const optionSets = command.optionSets; - assert.deepStrictEqual(optionSets, [{ options: ['id', 'name'] }]); + it('fails validation when both id and name are specified', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, name: validName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if id is not a valid guid.', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - id: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if id is not a valid guid.', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + id: 'Invalid GUID' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified (id)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (id)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName }); + assert.strictEqual(actual.success, true); }); it('publishes the components of a specified solution owned by the currently signed-in user', async () => { @@ -155,11 +160,11 @@ describe(commands.SOLUTION_PUBLISH, () => { }); await assert.doesNotReject(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, name: validName - } + }) })); }); @@ -187,12 +192,12 @@ describe(commands.SOLUTION_PUBLISH, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, name: validName, wait: true - } + }) }); assert(loggerLogToStderrSpy.called); }); @@ -205,11 +210,11 @@ describe(commands.SOLUTION_PUBLISH, () => { sinon.stub(request, 'get').callsFake(async () => { throw { error: { error: { message: errorMessage } } }; }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId - } + }) }), new CommandError(errorMessage)); }); }); \ No newline at end of file diff --git a/src/m365/pp/commands/solution/solution-publish.ts b/src/m365/pp/commands/solution/solution-publish.ts index 9afa38f1343..2ee379e6bc9 100644 --- a/src/m365/pp/commands/solution/solution-publish.ts +++ b/src/m365/pp/commands/solution/solution-publish.ts @@ -1,6 +1,7 @@ +import { z } from 'zod'; import { AxiosRequestConfig } from 'axios'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; @@ -8,18 +9,23 @@ import { validation } from '../../../../utils/validation.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional().alias('i'), + name: z.string().optional().alias('n'), + asAdmin: z.boolean().optional(), + wait: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - id?: string; - name?: string; - asAdmin?: boolean; - wait?: boolean; -} - interface SolutionComponent { msdyn_componentlogicalname: string; msdyn_name: string; @@ -34,62 +40,19 @@ class PpSolutionPublishCommand extends PowerPlatformCommand { return 'Publishes the components of a solution in a given Power Platform environment'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - asAdmin: !!args.options.asAdmin, - wait: !!args.options.wait - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '-i, --id [id]' - }, - { - option: '-n, --name [name]' - }, - { - option: '--asAdmin' - }, - { - option: '--wait' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'name'] } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'id' or 'name', but not both.`, + params: { + customCode: 'optionSet', + options: ['id', 'name'] } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: any): Promise { diff --git a/src/m365/pp/commands/solution/solution-publisher-add.spec.ts b/src/m365/pp/commands/solution/solution-publisher-add.spec.ts index e32e28fd2c3..248ddb6ffe7 100644 --- a/src/m365/pp/commands/solution/solution-publisher-add.spec.ts +++ b/src/m365/pp/commands/solution/solution-publisher-add.spec.ts @@ -12,18 +12,19 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-publisher-add.js'; +import command, { options } from './solution-publisher-add.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_PUBLISHER_ADD, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; //#region Mocked Responses const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const envUrl = "https://contoso-dev.api.crm4.dynamics.com"; const validName = "PublisherName"; const validDisplayName = "Publisher Name"; const validPrefix = "c6rx"; - const validChoiceValuePrefix = 10000; + const validChoiceValuePrefix = '10000'; //#endregion let log: string[]; @@ -37,6 +38,7 @@ describe(commands.SOLUTION_PUBLISHER_ADD, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -74,74 +76,69 @@ describe(commands.SOLUTION_PUBLISHER_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if choiceValuePrefix is not a number', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - name: validName, - displayName: validDisplayName, - prefix: validPrefix, - choiceValuePrefix: 'Not A Number' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if choiceValuePrefix is more than the upper bound', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - name: validName, - displayName: validDisplayName, - prefix: validPrefix, - choiceValuePrefix: 100000 - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if choiceValuePrefix is not a number', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + name: validName, + displayName: validDisplayName, + prefix: validPrefix, + choiceValuePrefix: 'Not A Number' + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if choiceValuePrefix is less than the lower bound', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - name: validName, - displayName: validDisplayName, - prefix: validPrefix, - choiceValuePrefix: 9999 - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if choiceValuePrefix is more than the upper bound', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + name: validName, + displayName: validDisplayName, + prefix: validPrefix, + choiceValuePrefix: '100000' + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if name is not a valid value', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - name: '9_PublisherName', - displayName: validDisplayName, - prefix: validPrefix, - choiceValuePrefix: validChoiceValuePrefix - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if choiceValuePrefix is less than the lower bound', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + name: validName, + displayName: validDisplayName, + prefix: validPrefix, + choiceValuePrefix: '9999' + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if prefix is not a valid value', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - name: validName, - displayName: validDisplayName, - prefix: 'mscrmfoo', - choiceValuePrefix: validChoiceValuePrefix - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if name is not a valid value', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + name: '9_PublisherName', + displayName: validDisplayName, + prefix: validPrefix, + choiceValuePrefix: validChoiceValuePrefix + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if prefix is not a valid value', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + name: validName, + displayName: validDisplayName, + prefix: 'mscrmfoo', + choiceValuePrefix: validChoiceValuePrefix + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix }); + assert.strictEqual(actual.success, true); }); it('correctly adds a specific publisher with the required parameters', async () => { @@ -157,7 +154,7 @@ describe(commands.SOLUTION_PUBLISHER_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix }) }); }); it('correctly handles API OData error', async () => { @@ -180,7 +177,7 @@ describe(commands.SOLUTION_PUBLISHER_ADD, () => { } }); - await assert.rejects(command.action(logger, { options: { environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: validEnvironment, name: validName, displayName: validDisplayName, prefix: validPrefix, choiceValuePrefix: validChoiceValuePrefix }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); }); diff --git a/src/m365/pp/commands/solution/solution-publisher-add.ts b/src/m365/pp/commands/solution/solution-publisher-add.ts index 143f493db9d..09998cca785 100644 --- a/src/m365/pp/commands/solution/solution-publisher-add.ts +++ b/src/m365/pp/commands/solution/solution-publisher-add.ts @@ -1,23 +1,39 @@ +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 { powerPlatform } from '../../../../utils/powerPlatform.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + name: z.string() + .refine(val => /^[a-zA-Z_][A-Za-z0-9_]+$/.test(val), { + message: 'Option name may only consist of alphanumeric characters and underscores. The first character cannot be a number.' + }).alias('n'), + displayName: z.string(), + prefix: z.string() + .refine(val => /^(?!mscrm.*$)[a-zA-Z][A-Za-z0-9]{1,7}$/.test(val), { + message: `Option prefix may only consist of alphanumeric characters. The first character cannot be a number and may not start with 'mscrm'. It must be between 2 and 8 characters long.` + }), + choiceValuePrefix: z.string() + .refine(val => { + const num = Number(val); + return Number.isInteger(num) && num >= 10000 && num <= 99999; + }, { + message: 'Option choiceValuePrefix should be an integer between 10000 and 99999.' + }), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - name: string; - displayName: string; - prefix: string; - choiceValuePrefix: number; - asAdmin?: boolean; -} - class PpSolutionPublisherAddCommand extends PowerPlatformCommand { public get name(): string { return commands.SOLUTION_PUBLISHER_ADD; @@ -27,68 +43,11 @@ class PpSolutionPublisherAddCommand extends PowerPlatformCommand { return 'Adds a specified publisher in a given environment'; } - 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: '-n, --name ' - }, - { - option: '--displayName ' - }, - { - option: '--prefix ' - }, - { - option: '--choiceValuePrefix ' - }, - { - option: '--asAdmin' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (isNaN(args.options.choiceValuePrefix) || args.options.choiceValuePrefix < 10000 || args.options.choiceValuePrefix > 99999 || !Number.isInteger(args.options.choiceValuePrefix)) { - return 'Option choiceValuePrefix should be an integer between 10000 and 99999.'; - } - - const nameRegEx = new RegExp(/^[a-zA-Z_][A-Za-z0-9_]+$/); - if (!nameRegEx.test(args.options.name)) { - return 'Option name may only consist of alphanumeric characters and underscores. The first character cannot be a number.'; - } - - const prefixRegEx = new RegExp(/^(?!mscrm.*$)[a-zA-Z][A-Za-z0-9]{1,7}$/); - if (!prefixRegEx.test(args.options.prefix)) { - return `Option prefix may only consist of alphanumeric characters. The first character cannot be a number and may not start with 'mscrm'. It must be between 2 and 8 characters long.`; - } - - return true; - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - public async commandAction(logger: Logger, args: any): Promise { + public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { await logger.logToStderr(`Adding new publisher '${args.options.name}'...`); } @@ -105,7 +64,7 @@ class PpSolutionPublisherAddCommand extends PowerPlatformCommand { uniquename: args.options.name, friendlyname: args.options.displayName, customizationprefix: args.options.prefix, - customizationoptionvalueprefix: args.options.choiceValuePrefix + customizationoptionvalueprefix: Number(args.options.choiceValuePrefix) } }; diff --git a/src/m365/pp/commands/solution/solution-publisher-get.spec.ts b/src/m365/pp/commands/solution/solution-publisher-get.spec.ts index 483027c558b..2487e7300cd 100644 --- a/src/m365/pp/commands/solution/solution-publisher-get.spec.ts +++ b/src/m365/pp/commands/solution/solution-publisher-get.spec.ts @@ -12,11 +12,12 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-publisher-get.js'; +import command, { options } from './solution-publisher-get.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_PUBLISHER_GET, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const validId = 'd21aab70-79e7-11dd-8874-00188b01e34f'; const validName = 'MicrosoftCorporation'; @@ -47,6 +48,7 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -85,6 +87,11 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { assert.notStrictEqual(command.description, null); }); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + it('fails validation when no publisher found', async () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); @@ -99,31 +106,29 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ environmentName: validEnvironment, name: validName - } + }) }), new CommandError(`The specified publisher '${validName}' does not exist.`)); }); - it('fails validation if the id is not a valid guid', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - id: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id is not a valid guid', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + id: 'Invalid GUID' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName }); + assert.strictEqual(actual.success, true); }); it('retrieves a specific publisher from power platform environment with the name parameter', async () => { @@ -139,7 +144,7 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, environmentName: validEnvironment, name: validName } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment, name: validName }) }); assert(loggerLogSpy.calledWith(publisherResponse.value[0])); }); @@ -156,7 +161,7 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: validEnvironment, id: validId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId }) }); assert(loggerLogSpy.calledWith(publisherResponse.value[0])); }); @@ -181,7 +186,7 @@ describe(commands.SOLUTION_PUBLISHER_GET, () => { }); - await assert.rejects(command.action(logger, { options: { environmentName: validEnvironment, name: validName } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: validEnvironment, name: validName }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); }); diff --git a/src/m365/pp/commands/solution/solution-publisher-get.ts b/src/m365/pp/commands/solution/solution-publisher-get.ts index c82dd1201b8..a1c5365595c 100644 --- a/src/m365/pp/commands/solution/solution-publisher-get.ts +++ b/src/m365/pp/commands/solution/solution-publisher-get.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 { powerPlatform } from '../../../../utils/powerPlatform.js'; import { validation } from '../../../../utils/validation.js'; @@ -7,17 +8,22 @@ import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; import { Publisher } from './Solution.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional().alias('i'), + name: z.string().optional().alias('n'), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -export interface Options extends GlobalOptions { - environmentName: string; - id?: string; - name?: string; - asAdmin?: boolean; -} - class PpSolutionPublisherGetCommand extends PowerPlatformCommand { public get name(): string { return commands.SOLUTION_PUBLISHER_GET; @@ -27,58 +33,19 @@ class PpSolutionPublisherGetCommand extends PowerPlatformCommand { return 'Gets information about the specified publisher in a given environment.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - asAdmin: !!args.options.asAdmin - }); - }); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '-i, --id [id]' - }, - { - option: '-n, --name [name]' - }, - { - option: '--asAdmin' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'name'] } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'id' or 'name', but not both.`, + params: { + customCode: 'optionSet', + options: ['id', 'name'] } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/solution/solution-publisher-list.spec.ts b/src/m365/pp/commands/solution/solution-publisher-list.spec.ts index 3a7572c1183..4a7f1343c7a 100644 --- a/src/m365/pp/commands/solution/solution-publisher-list.spec.ts +++ b/src/m365/pp/commands/solution/solution-publisher-list.spec.ts @@ -1,6 +1,8 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -10,7 +12,7 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-publisher-list.js'; +import command, { options } from './solution-publisher-list.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_PUBLISHER_LIST, () => { @@ -40,6 +42,8 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { ] }; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -51,6 +55,8 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { 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(() => { @@ -93,6 +99,11 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['publisherid', 'uniquename', 'friendlyname']); }); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + it('retrieves publishers from power platform environment', async () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); @@ -106,7 +117,7 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: validEnvironment } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment }) }); assert(loggerLogSpy.calledWith(publisherResponse.value)); }); @@ -123,7 +134,7 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, environmentName: validEnvironment, withMicrosoftPublishers: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, withMicrosoftPublishers: true }) }); assert(loggerLogSpy.calledWith(publisherResponse.value)); }); @@ -148,7 +159,7 @@ describe(commands.SOLUTION_PUBLISHER_LIST, () => { }); - await assert.rejects(command.action(logger, { options: { environmentName: validEnvironment } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: validEnvironment }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); }); diff --git a/src/m365/pp/commands/solution/solution-publisher-list.ts b/src/m365/pp/commands/solution/solution-publisher-list.ts index 30bb09c7224..e2842205c8b 100644 --- a/src/m365/pp/commands/solution/solution-publisher-list.ts +++ b/src/m365/pp/commands/solution/solution-publisher-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 request, { CliRequestOptions } from '../../../../request.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; import { Publisher } from './Solution.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + withMicrosoftPublishers: z.boolean().optional(), + asAdmin: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - environmentName: string; - withMicrosoftPublishers?: boolean; - asAdmin: boolean; -} - class PpSolutionPublisherListCommand extends PowerPlatformCommand { public get name(): string { return commands.SOLUTION_PUBLISHER_LIST; @@ -29,34 +33,8 @@ class PpSolutionPublisherListCommand extends PowerPlatformCommand { return ['publisherid', 'uniquename', 'friendlyname']; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - withMicrosoftPublishers: typeof args.options.withMicrosoftPublishers !== 'undefined', - asAdmin: !!args.options.asAdmin - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '--withMicrosoftPublishers' - }, - { - option: '--asAdmin' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/solution/solution-publisher-remove.spec.ts b/src/m365/pp/commands/solution/solution-publisher-remove.spec.ts index 7bcc5f63bc7..f4de9f4f8a6 100644 --- a/src/m365/pp/commands/solution/solution-publisher-remove.spec.ts +++ b/src/m365/pp/commands/solution/solution-publisher-remove.spec.ts @@ -13,11 +13,12 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import ppSolutionPublisherGetCommand from './solution-publisher-get.js'; -import command from './solution-publisher-remove.js'; +import command, { options } from './solution-publisher-remove.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; //#region Mocked Responses const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const validId = '00000001-0000-0000-0001-00000000009b'; @@ -38,6 +39,7 @@ describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -85,34 +87,37 @@ describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid guid.', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - id: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if id is not a valid guid.', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + id: 'Invalid GUID' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified (id)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (id)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName }); + assert.strictEqual(actual.success, true); }); it('prompts before removing the specified publisher owned by the currently signed-in user when force option not passed', async () => { sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ environmentName: validEnvironment, id: validId - } + }) }); assert(promptIssued); @@ -152,11 +157,11 @@ describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, name: validName - } + }) }); assert(loggerLogToStderrSpy.called); }); @@ -173,12 +178,12 @@ describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId, force: true - } + }) }); assert(loggerLogToStderrSpy.called); }); @@ -191,12 +196,12 @@ describe(commands.SOLUTION_PUBLISHER_REMOVE, () => { sinon.stub(request, 'delete').callsFake(async () => { throw errorMessage; }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId, force: true - } + }) }), new CommandError(errorMessage)); }); }); diff --git a/src/m365/pp/commands/solution/solution-publisher-remove.ts b/src/m365/pp/commands/solution/solution-publisher-remove.ts index dbee6b751f2..e69e1e60f1d 100644 --- a/src/m365/pp/commands/solution/solution-publisher-remove.ts +++ b/src/m365/pp/commands/solution/solution-publisher-remove.ts @@ -1,26 +1,31 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import Command from '../../../../Command.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import Command, { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { validation } from '../../../../utils/validation.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; -import ppSolutionPublisherGetCommand, { Options as PpSolutionPublisherGetCommandOptions } from './solution-publisher-get.js'; +import ppSolutionPublisherGetCommand, { options as ppSolutionPublisherGetOptions } from './solution-publisher-get.js'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional().alias('i'), + name: z.string().optional().alias('n'), + 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; - id?: string; - name?: string; - asAdmin?: boolean; - force?: boolean; -} - class PpSolutionPublisherRemoveCommand extends PowerPlatformCommand { public get name(): string { @@ -31,62 +36,19 @@ class PpSolutionPublisherRemoveCommand extends PowerPlatformCommand { return 'Removes a specific publisher in the specified Power Platform environment.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - asAdmin: !!args.options.asAdmin, - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '-i, --id [id]' - }, - { - option: '-n, --name [name]' - }, - { - option: '--asAdmin' - }, - { - option: '-f, --force' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'name'] } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'id' or 'name', but not both.`, + params: { + customCode: 'optionSet', + options: ['id', 'name'] } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -111,7 +73,7 @@ class PpSolutionPublisherRemoveCommand extends PowerPlatformCommand { return args.options.id; } - const options: PpSolutionPublisherGetCommandOptions = { + const options: z.infer = { environmentName: args.options.environmentName, name: args.options.name, output: 'json', diff --git a/src/m365/pp/commands/solution/solution-remove.spec.ts b/src/m365/pp/commands/solution/solution-remove.spec.ts index cbe7032113c..88cb89d890a 100644 --- a/src/m365/pp/commands/solution/solution-remove.spec.ts +++ b/src/m365/pp/commands/solution/solution-remove.spec.ts @@ -12,11 +12,12 @@ import { session } from '../../../../utils/session.js'; import { powerPlatform } from '../../../../utils/powerPlatform.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './solution-remove.js'; +import command, { options } from './solution-remove.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.SOLUTION_REMOVE, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; //#region Mocked Responses const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36'; const validId = '00000001-0000-0000-0001-00000000009b'; @@ -50,6 +51,7 @@ describe(commands.SOLUTION_REMOVE, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -97,32 +99,35 @@ describe(commands.SOLUTION_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid guid.', async () => { - const actual = await command.validate({ - options: { - environmentName: validEnvironment, - id: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if id is not a valid guid.', () => { + const actual = commandOptionsSchema.safeParse({ + environmentName: validEnvironment, + id: 'Invalid GUID' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation if required options specified (id)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (id)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if required options specified (name)', async () => { - const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if required options specified (name)', () => { + const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName }); + assert.strictEqual(actual.success, true); }); it('prompts before removing the specified solution owned by the currently signed-in user when force option not passed', async () => { await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ environmentName: validEnvironment, id: validId - } + }) }); assert(promptIssued); @@ -132,10 +137,10 @@ describe(commands.SOLUTION_REMOVE, () => { const postSpy = sinon.spy(request, 'delete'); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ environmentName: validEnvironment, id: validId - } + }) }); assert(postSpy.notCalled); }); @@ -155,11 +160,11 @@ describe(commands.SOLUTION_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, name: validName - } + }) }); assert(loggerLogToStderrSpy.called); }); @@ -176,12 +181,12 @@ describe(commands.SOLUTION_REMOVE, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId, force: true - } + }) }); assert(loggerLogToStderrSpy.called); }); @@ -194,12 +199,12 @@ describe(commands.SOLUTION_REMOVE, () => { sinon.stub(request, 'delete').callsFake(async () => { throw { error: { error: { message: errorMessage } } }; }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ debug: true, environmentName: validEnvironment, id: validId, force: true - } + }) }), new CommandError(errorMessage)); }); }); diff --git a/src/m365/pp/commands/solution/solution-remove.ts b/src/m365/pp/commands/solution/solution-remove.ts index 0bfe64eece8..246ee5f45e8 100644 --- a/src/m365/pp/commands/solution/solution-remove.ts +++ b/src/m365/pp/commands/solution/solution-remove.ts @@ -1,24 +1,30 @@ +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 { powerPlatform } from '../../../../utils/powerPlatform.js'; import { validation } from '../../../../utils/validation.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + environmentName: z.string().alias('e'), + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).optional().alias('i'), + name: z.string().optional().alias('n'), + 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; - id?: string; - name?: string; - asAdmin?: boolean; - force?: boolean; -} - class PpSolutionRemoveCommand extends PowerPlatformCommand { public get name(): string { @@ -29,62 +35,19 @@ class PpSolutionRemoveCommand extends PowerPlatformCommand { return 'Removes a specific solution in the specified Power Platform environment.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - asAdmin: !!args.options.asAdmin, - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-e, --environmentName ' - }, - { - option: '-i, --id [id]' - }, - { - option: '-n, --name [name]' - }, - { - option: '--asAdmin' - }, - { - option: '-f, --force' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'name'] } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { + message: `Specify either 'id' or 'name', but not both.`, + params: { + customCode: 'optionSet', + options: ['id', 'name'] } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/pp/commands/tenant/tenant-settings-set.spec.ts b/src/m365/pp/commands/tenant/tenant-settings-set.spec.ts index 1e5f1aac801..7dd70088c30 100644 --- a/src/m365/pp/commands/tenant/tenant-settings-set.spec.ts +++ b/src/m365/pp/commands/tenant/tenant-settings-set.spec.ts @@ -11,11 +11,12 @@ 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 './tenant-settings-set.js'; +import command, { options } from './tenant-settings-set.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.TENANT_SETTINGS_SET, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const successResponse = { walkMeOptOut: false, @@ -80,6 +81,7 @@ describe(commands.TENANT_SETTINGS_SET, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -117,49 +119,54 @@ describe(commands.TENANT_SETTINGS_SET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if not one property is specified', async () => { - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ walkMeOptOut: false, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the shareWithColleaguesUserLimit is not a valid number', async () => { - const actual = await command.validate({ options: { shareWithColleaguesUserLimit: 'foo' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if not one property is specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('fails validation if the shareWithColleaguesUserLimit is a negative number', async () => { - const actual = await command.validate({ options: { shareWithColleaguesUserLimit: -1 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the shareWithColleaguesUserLimit is not a valid number', () => { + const actual = commandOptionsSchema.safeParse({ shareWithColleaguesUserLimit: 'foo' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the shareWithColleaguesUserLimit is a float number', async () => { - const actual = await command.validate({ options: { shareWithColleaguesUserLimit: 3.14 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the shareWithColleaguesUserLimit is a negative number', () => { + const actual = commandOptionsSchema.safeParse({ shareWithColleaguesUserLimit: '-1' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the shareWithColleaguesUserLimit is a valid number', async () => { - const actual = await command.validate({ options: { shareWithColleaguesUserLimit: 9 } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if the shareWithColleaguesUserLimit is a float number', () => { + const actual = commandOptionsSchema.safeParse({ shareWithColleaguesUserLimit: '3.14' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the storageCapacityConsumptionWarningThreshold is not a valid number', async () => { - const actual = await command.validate({ options: { storageCapacityConsumptionWarningThreshold: 'foo' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('passes validation when the shareWithColleaguesUserLimit is a valid number', () => { + const actual = commandOptionsSchema.safeParse({ shareWithColleaguesUserLimit: '9' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if the storageCapacityConsumptionWarningThreshold is a negative number', async () => { - const actual = await command.validate({ options: { storageCapacityConsumptionWarningThreshold: -1 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the storageCapacityConsumptionWarningThreshold is not a valid number', () => { + const actual = commandOptionsSchema.safeParse({ storageCapacityConsumptionWarningThreshold: 'foo' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the storageCapacityConsumptionWarningThreshold is a float number', async () => { - const actual = await command.validate({ options: { storageCapacityConsumptionWarningThreshold: 3.14 } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the storageCapacityConsumptionWarningThreshold is a negative number', () => { + const actual = commandOptionsSchema.safeParse({ storageCapacityConsumptionWarningThreshold: '-1' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the storageCapacityConsumptionWarningThreshold is a valid number', async () => { - const actual = await command.validate({ options: { storageCapacityConsumptionWarningThreshold: 9 } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if the storageCapacityConsumptionWarningThreshold is a float number', () => { + const actual = commandOptionsSchema.safeParse({ storageCapacityConsumptionWarningThreshold: '3.14' }); + assert.strictEqual(actual.success, false); + }); + + it('passes validation when the storageCapacityConsumptionWarningThreshold is a valid number', () => { + const actual = commandOptionsSchema.safeParse({ storageCapacityConsumptionWarningThreshold: '9' }); + assert.strictEqual(actual.success, true); }); it('successfully updates tenant settings', async () => { @@ -171,7 +178,7 @@ describe(commands.TENANT_SETTINGS_SET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ walkMeOptOut: false, disableNPSCommentsReachout: false, disableNewsletterSendout: false, @@ -184,7 +191,7 @@ describe(commands.TENANT_SETTINGS_SET, () => { disableDocsSearch: false, disableCommunitySearch: false, disableBingVideoSearch: false, - shareWithColleaguesUserLimit: 10000, + shareWithColleaguesUserLimit: '10000', disableShareWithEveryone: false, enableGuestsToMake: false, disableMembersIndicator: false, @@ -193,14 +200,14 @@ describe(commands.TENANT_SETTINGS_SET, () => { disableAdminDigest: false, disableDeveloperEnvironmentCreationByNonAdminUsers: false, disableBillingPolicyCreationByNonAdminUsers: false, - storageCapacityConsumptionWarningThreshold: 85, + storageCapacityConsumptionWarningThreshold: '85', disableChampionsInvitationReachout: false, disableSkillsMatchInvitationReachout: false, disableCopilot: false, enableOpenAiBotPublishing: false, enableModelDataSharing: false - } - } as any); + }) + }); assert(loggerLogSpy.calledWith(successResponse)); }); @@ -215,6 +222,6 @@ describe(commands.TENANT_SETTINGS_SET, () => { throw error; }); - await assert.rejects(command.action(logger, { options: {} } as any), new CommandError(error.error.message)); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ walkMeOptOut: false }) }), new CommandError(error.error.message)); }); }); diff --git a/src/m365/pp/commands/tenant/tenant-settings-set.ts b/src/m365/pp/commands/tenant/tenant-settings-set.ts index a48650dad53..d31b124a172 100644 --- a/src/m365/pp/commands/tenant/tenant-settings-set.ts +++ b/src/m365/pp/commands/tenant/tenant-settings-set.ts @@ -1,43 +1,57 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + walkMeOptOut: z.boolean().optional(), + disableNPSCommentsReachout: z.boolean().optional(), + disableNewsletterSendout: z.boolean().optional(), + disableEnvironmentCreationByNonAdminUsers: z.boolean().optional(), + disablePortalsCreationByNonAdminUsers: z.boolean().optional(), + disableSurveyFeedback: z.boolean().optional(), + disableTrialEnvironmentCreationByNonAdminUsers: z.boolean().optional(), + disableCapacityAllocationByEnvironmentAdmins: z.boolean().optional(), + disableSupportTicketsVisibleByAllUsers: z.boolean().optional(), + disableDocsSearch: z.boolean().optional(), + disableCommunitySearch: z.boolean().optional(), + disableBingVideoSearch: z.boolean().optional(), + shareWithColleaguesUserLimit: z.string().refine(val => { + const num = Number(val); + return Number.isInteger(num) && num >= 0; + }, { + message: 'The value must be a non-negative integer.' + }).optional(), + disableShareWithEveryone: z.boolean().optional(), + enableGuestsToMake: z.boolean().optional(), + disableMembersIndicator: z.boolean().optional(), + disableMakerMatch: z.boolean().optional(), + disablePreferredDataLocationForTeamsEnvironment: z.boolean().optional(), + disableAdminDigest: z.boolean().optional(), + disableDeveloperEnvironmentCreationByNonAdminUsers: z.boolean().optional(), + disableBillingPolicyCreationByNonAdminUsers: z.boolean().optional(), + storageCapacityConsumptionWarningThreshold: z.string().refine(val => { + const num = Number(val); + return Number.isInteger(num) && num >= 0; + }, { + message: 'The value must be a non-negative integer.' + }).optional(), + disableChampionsInvitationReachout: z.boolean().optional(), + disableSkillsMatchInvitationReachout: z.boolean().optional(), + disableCopilot: z.boolean().optional(), + enableOpenAiBotPublishing: z.boolean().optional(), + enableModelDataSharing: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - walkMeOptOut?: boolean; - disableNPSCommentsReachout?: boolean; - disableNewsletterSendout?: boolean; - disableEnvironmentCreationByNonAdminUsers?: boolean; - disablePortalsCreationByNonAdminUsers?: boolean; - disableSurveyFeedback?: boolean; - disableTrialEnvironmentCreationByNonAdminUsers?: boolean; - disableCapacityAllocationByEnvironmentAdmins?: boolean; - disableSupportTicketsVisibleByAllUsers?: boolean; - disableDocsSearch?: boolean; - disableCommunitySearch?: boolean; - disableBingVideoSearch?: boolean; - shareWithColleaguesUserLimit?: number; - disableShareWithEveryone?: boolean; - enableGuestsToMake?: boolean; - disableMembersIndicator?: boolean; - disableMakerMatch?: boolean; - disablePreferredDataLocationForTeamsEnvironment?: boolean; - disableAdminDigest?: boolean; - disableDeveloperEnvironmentCreationByNonAdminUsers?: boolean; - disableBillingPolicyCreationByNonAdminUsers?: boolean; - storageCapacityConsumptionWarningThreshold?: number; - disableChampionsInvitationReachout?: boolean; - disableSkillsMatchInvitationReachout?: boolean; - disableCopilot?: boolean; - enableOpenAiBotPublishing?: boolean; - enableModelDataSharing?: boolean; -} - class PpTenantSettingsSetCommand extends PowerPlatformCommand { public get name(): string { return commands.TENANT_SETTINGS_SET; @@ -47,235 +61,45 @@ class PpTenantSettingsSetCommand extends PowerPlatformCommand { return 'Sets the global Power Platform configuration of the tenant'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initTypes(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - walkMeOptOut: typeof args.options.walkMeOptOut !== 'undefined', - disableNPSCommentsReachout: typeof args.options.disableNPSCommentsReachout !== 'undefined', - disableNewsletterSendout: typeof args.options.disableNewsletterSendout !== 'undefined', - disableEnvironmentCreationByNonAdminUsers: typeof args.options.disableEnvironmentCreationByNonAdminUsers !== 'undefined', - disablePortalsCreationByNonAdminUsers: typeof args.options.disablePortalsCreationByNonAdminUsers !== 'undefined', - disableSurveyFeedback: typeof args.options.disableSurveyFeedback !== 'undefined', - disableTrialEnvironmentCreationByNonAdminUsers: typeof args.options.disableTrialEnvironmentCreationByNonAdminUsers !== 'undefined', - disableCapacityAllocationByEnvironmentAdmins: typeof args.options.disableCapacityAllocationByEnvironmentAdmins !== 'undefined', - disableSupportTicketsVisibleByAllUsers: typeof args.options.disableSupportTicketsVisibleByAllUsers !== 'undefined', - disableDocsSearch: typeof args.options.disableDocsSearch !== 'undefined', - disableCommunitySearch: typeof args.options.disableCommunitySearch !== 'undefined', - disableBingVideoSearch: typeof args.options.disableBingVideoSearch !== 'undefined', - shareWithColleaguesUserLimit: typeof args.options.shareWithColleaguesUserLimit !== 'undefined', - disableShareWithEveryone: typeof args.options.disableShareWithEveryone !== 'undefined', - enableGuestsToMake: typeof args.options.enableGuestsToMake !== 'undefined', - disableMembersIndicator: typeof args.options.disableMembersIndicator !== 'undefined', - disableMakerMatch: typeof args.options.disableMakerMatch !== 'undefined', - disablePreferredDataLocationForTeamsEnvironment: typeof args.options.disablePreferredDataLocationForTeamsEnvironment !== 'undefined', - disableAdminDigest: typeof args.options.disableAdminDigest !== 'undefined', - disableDeveloperEnvironmentCreationByNonAdminUsers: typeof args.options.disableDeveloperEnvironmentCreationByNonAdminUsers !== 'undefined', - disableBillingPolicyCreationByNonAdminUsers: typeof args.options.disableBillingPolicyCreationByNonAdminUsers !== 'undefined', - storageCapacityConsumptionWarningThreshold: typeof args.options.storageCapacityConsumptionWarningThreshold !== 'undefined', - disableChampionsInvitationReachout: typeof args.options.disableChampionsInvitationReachout !== 'undefined', - disableSkillsMatchInvitationReachout: typeof args.options.disableSkillsMatchInvitationReachout !== 'undefined', - disableCopilot: typeof args.options.disableCopilot !== 'undefined', - enableOpenAiBotPublishing: typeof args.options.enableOpenAiBotPublishing !== 'undefined', - enableModelDataSharing: typeof args.options.enableModelDataSharing !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '--walkMeOptOut [walkMeOptOut]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableNPSCommentsReachout [disableNPSCommentsReachout]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableNewsletterSendout [disableNewsletterSendout]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableEnvironmentCreationByNonAdminUsers [disableEnvironmentCreationByNonAdminUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--disablePortalsCreationByNonAdminUsers [disablePortalsCreationByNonAdminUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableSurveyFeedback [disableSurveyFeedback]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableTrialEnvironmentCreationByNonAdminUsers [disableTrialEnvironmentCreationByNonAdminUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableCapacityAllocationByEnvironmentAdmins [disableCapacityAllocationByEnvironmentAdmins]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableSupportTicketsVisibleByAllUsers [disableSupportTicketsVisibleByAllUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableDocsSearch [disableDocsSearch]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableCommunitySearch [disableCommunitySearch]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableBingVideoSearch [disableBingVideoSearch]', - autocomplete: ['true', 'false'] - }, - { - option: '--shareWithColleaguesUserLimit [shareWithColleaguesUserLimit]' - }, - { - option: '--disableShareWithEveryone [disableShareWithEveryone]', - autocomplete: ['true', 'false'] - }, - { - option: '--enableGuestsToMake [enableGuestsToMake]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableMembersIndicator [disableMembersIndicator]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableMakerMatch [disableMakerMatch]', - autocomplete: ['true', 'false'] - }, - { - option: '--disablePreferredDataLocationForTeamsEnvironment [disablePreferredDataLocationForTeamsEnvironment]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableAdminDigest [disableAdminDigest]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableDeveloperEnvironmentCreationByNonAdminUsers [disableDeveloperEnvironmentCreationByNonAdminUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableBillingPolicyCreationByNonAdminUsers [disableBillingPolicyCreationByNonAdminUsers]', - autocomplete: ['true', 'false'] - }, - { - option: '--storageCapacityConsumptionWarningThreshold [storageCapacityConsumptionWarningThreshold]' - }, - { - option: '--disableChampionsInvitationReachout [disableChampionsInvitationReachout]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableSkillsMatchInvitationReachout [disableSkillsMatchInvitationReachout]', - autocomplete: ['true', 'false'] - }, - { - option: '--disableCopilot [disableCopilot]', - autocomplete: ['true', 'false'] - }, - { - option: '--enableOpenAiBotPublishing [enableOpenAiBotPublishing]', - autocomplete: ['true', 'false'] - }, - { - option: '--enableModelDataSharing [enableModelDataSharing]', - autocomplete: ['true', 'false'] - } - ); - } - - #initTypes(): void { - this.types.boolean.push( - 'walkMeOptOut', - 'disableNPSCommentsReachout', - 'disableNewsletterSendout', - 'disableEnvironmentCreationByNonAdminUsers', - 'disablePortalsCreationByNonAdminUsers', - 'disableSurveyFeedback', - 'disableTrialEnvironmentCreationByNonAdminUsers', - 'disableCapacityAllocationByEnvironmentAdmins', - 'disableSupportTicketsVisibleByAllUsers', - 'disableDocsSearch', - 'disableCommunitySearch', - 'disableBingVideoSearch', - 'disableShareWithEveryone', - 'enableGuestsToMake', - 'disableMembersIndicator', - 'disableMakerMatch', - 'disablePreferredDataLocationForTeamsEnvironment', - 'disableAdminDigest', - 'disableDeveloperEnvironmentCreationByNonAdminUsers', - 'disableBillingPolicyCreationByNonAdminUsers', - 'disableChampionsInvitationReachout', - 'disableSkillsMatchInvitationReachout', - 'disableCopilot', - 'enableOpenAiBotPublishing', - 'enableModelDataSharing' - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - - if (args.options.shareWithColleaguesUserLimit !== undefined && (!Number.isInteger(args.options.shareWithColleaguesUserLimit) || args.options.shareWithColleaguesUserLimit < 0)) { - return `'${args.options.shareWithColleaguesUserLimit}' is not a valid number.`; - } - - if (args.options.storageCapacityConsumptionWarningThreshold !== undefined && (!Number.isInteger(args.options.storageCapacityConsumptionWarningThreshold) || args.options.storageCapacityConsumptionWarningThreshold < 0)) { - return `'${args.options.storageCapacityConsumptionWarningThreshold}' is not a valid number.`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => + opts.walkMeOptOut !== undefined || + opts.disableNPSCommentsReachout !== undefined || + opts.disableNewsletterSendout !== undefined || + opts.disableEnvironmentCreationByNonAdminUsers !== undefined || + opts.disablePortalsCreationByNonAdminUsers !== undefined || + opts.disableSurveyFeedback !== undefined || + opts.disableTrialEnvironmentCreationByNonAdminUsers !== undefined || + opts.disableCapacityAllocationByEnvironmentAdmins !== undefined || + opts.disableSupportTicketsVisibleByAllUsers !== undefined || + opts.disableDocsSearch !== undefined || + opts.disableCommunitySearch !== undefined || + opts.disableBingVideoSearch !== undefined || + opts.shareWithColleaguesUserLimit !== undefined || + opts.disableShareWithEveryone !== undefined || + opts.enableGuestsToMake !== undefined || + opts.disableMembersIndicator !== undefined || + opts.disableMakerMatch !== undefined || + opts.disablePreferredDataLocationForTeamsEnvironment !== undefined || + opts.disableAdminDigest !== undefined || + opts.disableDeveloperEnvironmentCreationByNonAdminUsers !== undefined || + opts.disableBillingPolicyCreationByNonAdminUsers !== undefined || + opts.storageCapacityConsumptionWarningThreshold !== undefined || + opts.disableChampionsInvitationReachout !== undefined || + opts.disableSkillsMatchInvitationReachout !== undefined || + opts.disableCopilot !== undefined || + opts.enableOpenAiBotPublishing !== undefined || + opts.enableModelDataSharing !== undefined, { + message: 'Specify at least one option.', + params: { + customCode: 'required' } - - if (typeof args.options.walkMeOptOut === 'undefined' && - typeof args.options.disableNPSCommentsReachout === 'undefined' && - typeof args.options.disableNewsletterSendout === 'undefined' && - typeof args.options.disableEnvironmentCreationByNonAdminUsers === 'undefined' && - typeof args.options.disablePortalsCreationByNonAdminUsers === 'undefined' && - typeof args.options.disableSurveyFeedback === 'undefined' && - typeof args.options.disableTrialEnvironmentCreationByNonAdminUsers === 'undefined' && - typeof args.options.disableCapacityAllocationByEnvironmentAdmins === 'undefined' && - typeof args.options.disableSupportTicketsVisibleByAllUsers === 'undefined' && - typeof args.options.disableDocsSearch === 'undefined' && - typeof args.options.disableCommunitySearch === 'undefined' && - typeof args.options.disableBingVideoSearch === 'undefined' && - typeof args.options.shareWithColleaguesUserLimit === 'undefined' && - typeof args.options.disableShareWithEveryone === 'undefined' && - typeof args.options.enableGuestsToMake === 'undefined' && - typeof args.options.disableMembersIndicator === 'undefined' && - typeof args.options.disableMakerMatch === 'undefined' && - typeof args.options.disablePreferredDataLocationForTeamsEnvironment === 'undefined' && - typeof args.options.disableAdminDigest === 'undefined' && - typeof args.options.disableDeveloperEnvironmentCreationByNonAdminUsers === 'undefined' && - typeof args.options.disableBillingPolicyCreationByNonAdminUsers === 'undefined' && - typeof args.options.storageCapacityConsumptionWarningThreshold === 'undefined' && - typeof args.options.disableChampionsInvitationReachout === 'undefined' && - typeof args.options.disableSkillsMatchInvitationReachout === 'undefined' && - typeof args.options.disableCopilot === 'undefined' && - typeof args.options.enableOpenAiBotPublishing === 'undefined' && - typeof args.options.enableModelDataSharing === 'undefined') { - return 'Specify at least one option.'; - } - - return true; - } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -296,7 +120,7 @@ class PpTenantSettingsSetCommand extends PowerPlatformCommand { disableBingVideoSearch: args.options.disableBingVideoSearch }, teamsIntegration: { - shareWithColleaguesUserLimit: args.options.shareWithColleaguesUserLimit + shareWithColleaguesUserLimit: args.options.shareWithColleaguesUserLimit !== undefined ? Number(args.options.shareWithColleaguesUserLimit) : undefined }, powerApps: { disableShareWithEveryone: args.options.disableShareWithEveryone, @@ -313,7 +137,7 @@ class PpTenantSettingsSetCommand extends PowerPlatformCommand { }, licensing: { disableBillingPolicyCreationByNonAdminUsers: args.options.disableBillingPolicyCreationByNonAdminUsers, - storageCapacityConsumptionWarningThreshold: args.options.storageCapacityConsumptionWarningThreshold + storageCapacityConsumptionWarningThreshold: args.options.storageCapacityConsumptionWarningThreshold !== undefined ? Number(args.options.storageCapacityConsumptionWarningThreshold) : undefined }, champions: { disableChampionsInvitationReachout: args.options.disableChampionsInvitationReachout, From 37ef919aa242d4fd0bbbb0e81db93e241900ad03 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Fri, 26 Jun 2026 11:50:47 +0200 Subject: [PATCH 2/3] fix: use error: in refine calls and fix environmentName reference - Changed message: to error: in all refine() calls to match repo convention - Fixed args.options.environment to args.options.environmentName in solution-publish.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .impeccable/hook.cache.json | 1 + src/m365/pp/commands/gateway/gateway-get.ts | 2 +- src/m365/pp/commands/managementapp/managementapp-add.ts | 6 +++--- src/m365/pp/commands/solution/solution-get.ts | 4 ++-- src/m365/pp/commands/solution/solution-publish.ts | 6 +++--- src/m365/pp/commands/solution/solution-publisher-add.ts | 6 +++--- src/m365/pp/commands/solution/solution-publisher-get.ts | 4 ++-- src/m365/pp/commands/solution/solution-publisher-remove.ts | 4 ++-- src/m365/pp/commands/solution/solution-remove.ts | 4 ++-- src/m365/pp/commands/tenant/tenant-settings-set.ts | 6 +++--- 10 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 .impeccable/hook.cache.json diff --git a/.impeccable/hook.cache.json b/.impeccable/hook.cache.json new file mode 100644 index 00000000000..22c7d8b2569 --- /dev/null +++ b/.impeccable/hook.cache.json @@ -0,0 +1 @@ +{"version":1,"sessions":{"60b0061d-4551-4329-b6f1-1e0c26dba8ee":{"updatedAt":1782371249716,"files":{"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/gateway/gateway-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-add.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publish.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-add.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-remove.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-remove.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/tenant/tenant-settings-set.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/gateway/gateway-get.spec.ts":{"editCount":5,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-add.spec.ts":{"editCount":6,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-list.spec.ts":{"editCount":4,"findings":[]}}}}} \ No newline at end of file diff --git a/src/m365/pp/commands/gateway/gateway-get.ts b/src/m365/pp/commands/gateway/gateway-get.ts index 6c03070f376..0853981c6d6 100644 --- a/src/m365/pp/commands/gateway/gateway-get.ts +++ b/src/m365/pp/commands/gateway/gateway-get.ts @@ -10,7 +10,7 @@ import commands from "../../commands.js"; export const options = z.strictObject({ ...globalOptionsZod.shape, id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).alias('i') }); diff --git a/src/m365/pp/commands/managementapp/managementapp-add.ts b/src/m365/pp/commands/managementapp/managementapp-add.ts index 2b40a081be2..ed02be568bc 100644 --- a/src/m365/pp/commands/managementapp/managementapp-add.ts +++ b/src/m365/pp/commands/managementapp/managementapp-add.ts @@ -10,10 +10,10 @@ import { entraApp } from '../../../../utils/entraApp.js'; export const options = z.strictObject({ ...globalOptionsZod.shape, appId: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional(), objectId: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional(), name: z.string().optional() }); @@ -40,7 +40,7 @@ class PpManagementAppAddCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.appId, opts.objectId, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'appId', 'objectId', or 'name', but not multiple.`, + error: `Specify either 'appId', 'objectId', or 'name', but not multiple.`, params: { customCode: 'optionSet', options: ['appId', 'objectId', 'name'] diff --git a/src/m365/pp/commands/solution/solution-get.ts b/src/m365/pp/commands/solution/solution-get.ts index 405b63e0ef7..c7fdeb3d11f 100644 --- a/src/m365/pp/commands/solution/solution-get.ts +++ b/src/m365/pp/commands/solution/solution-get.ts @@ -13,7 +13,7 @@ export const options = z.strictObject({ ...globalOptionsZod.shape, environmentName: z.string().alias('e'), id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional().alias('i'), name: z.string().optional().alias('n'), asAdmin: z.boolean().optional() @@ -41,7 +41,7 @@ class PpSolutionGetCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'id' or 'name', but not both.`, + error: `Specify either 'id' or 'name', but not both.`, params: { customCode: 'optionSet', options: ['id', 'name'] diff --git a/src/m365/pp/commands/solution/solution-publish.ts b/src/m365/pp/commands/solution/solution-publish.ts index 2ee379e6bc9..37484da5ddd 100644 --- a/src/m365/pp/commands/solution/solution-publish.ts +++ b/src/m365/pp/commands/solution/solution-publish.ts @@ -13,7 +13,7 @@ export const options = z.strictObject({ ...globalOptionsZod.shape, environmentName: z.string().alias('e'), id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional().alias('i'), name: z.string().optional().alias('n'), asAdmin: z.boolean().optional(), @@ -47,7 +47,7 @@ class PpSolutionPublishCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'id' or 'name', but not both.`, + error: `Specify either 'id' or 'name', but not both.`, params: { customCode: 'optionSet', options: ['id', 'name'] @@ -57,7 +57,7 @@ class PpSolutionPublishCommand extends PowerPlatformCommand { public async commandAction(logger: Logger, args: any): Promise { try { - const dynamicsApiUrl = await powerPlatform.getDynamicsInstanceApiUrl(args.options.environment, args.options.asAdmin); + const dynamicsApiUrl = await powerPlatform.getDynamicsInstanceApiUrl(args.options.environmentName, args.options.asAdmin); const solutionId = await this.getSolutionId(args, dynamicsApiUrl, logger); const solutionComponents = await this.getSolutionComponents(dynamicsApiUrl, solutionId, logger); const parameterXml = await this.buildXmlRequestObject(solutionComponents, logger); diff --git a/src/m365/pp/commands/solution/solution-publisher-add.ts b/src/m365/pp/commands/solution/solution-publisher-add.ts index 09998cca785..b2faee7785b 100644 --- a/src/m365/pp/commands/solution/solution-publisher-add.ts +++ b/src/m365/pp/commands/solution/solution-publisher-add.ts @@ -11,19 +11,19 @@ export const options = z.strictObject({ environmentName: z.string().alias('e'), name: z.string() .refine(val => /^[a-zA-Z_][A-Za-z0-9_]+$/.test(val), { - message: 'Option name may only consist of alphanumeric characters and underscores. The first character cannot be a number.' + error: 'Option name may only consist of alphanumeric characters and underscores. The first character cannot be a number.' }).alias('n'), displayName: z.string(), prefix: z.string() .refine(val => /^(?!mscrm.*$)[a-zA-Z][A-Za-z0-9]{1,7}$/.test(val), { - message: `Option prefix may only consist of alphanumeric characters. The first character cannot be a number and may not start with 'mscrm'. It must be between 2 and 8 characters long.` + error: `Option prefix may only consist of alphanumeric characters. The first character cannot be a number and may not start with 'mscrm'. It must be between 2 and 8 characters long.` }), choiceValuePrefix: z.string() .refine(val => { const num = Number(val); return Number.isInteger(num) && num >= 10000 && num <= 99999; }, { - message: 'Option choiceValuePrefix should be an integer between 10000 and 99999.' + error: 'Option choiceValuePrefix should be an integer between 10000 and 99999.' }), asAdmin: z.boolean().optional() }); diff --git a/src/m365/pp/commands/solution/solution-publisher-get.ts b/src/m365/pp/commands/solution/solution-publisher-get.ts index a1c5365595c..9aaa7e23f62 100644 --- a/src/m365/pp/commands/solution/solution-publisher-get.ts +++ b/src/m365/pp/commands/solution/solution-publisher-get.ts @@ -12,7 +12,7 @@ export const options = z.strictObject({ ...globalOptionsZod.shape, environmentName: z.string().alias('e'), id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional().alias('i'), name: z.string().optional().alias('n'), asAdmin: z.boolean().optional() @@ -40,7 +40,7 @@ class PpSolutionPublisherGetCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'id' or 'name', but not both.`, + error: `Specify either 'id' or 'name', but not both.`, params: { customCode: 'optionSet', options: ['id', 'name'] diff --git a/src/m365/pp/commands/solution/solution-publisher-remove.ts b/src/m365/pp/commands/solution/solution-publisher-remove.ts index e69e1e60f1d..c904012e441 100644 --- a/src/m365/pp/commands/solution/solution-publisher-remove.ts +++ b/src/m365/pp/commands/solution/solution-publisher-remove.ts @@ -13,7 +13,7 @@ export const options = z.strictObject({ ...globalOptionsZod.shape, environmentName: z.string().alias('e'), id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional().alias('i'), name: z.string().optional().alias('n'), asAdmin: z.boolean().optional(), @@ -43,7 +43,7 @@ class PpSolutionPublisherRemoveCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'id' or 'name', but not both.`, + error: `Specify either 'id' or 'name', but not both.`, params: { customCode: 'optionSet', options: ['id', 'name'] diff --git a/src/m365/pp/commands/solution/solution-remove.ts b/src/m365/pp/commands/solution/solution-remove.ts index 246ee5f45e8..9943f0a38d7 100644 --- a/src/m365/pp/commands/solution/solution-remove.ts +++ b/src/m365/pp/commands/solution/solution-remove.ts @@ -12,7 +12,7 @@ export const options = z.strictObject({ ...globalOptionsZod.shape, environmentName: z.string().alias('e'), id: z.string().refine(val => validation.isValidGuid(val), { - message: 'The value must be a valid GUID.' + error: 'The value must be a valid GUID.' }).optional().alias('i'), name: z.string().optional().alias('n'), asAdmin: z.boolean().optional(), @@ -42,7 +42,7 @@ class PpSolutionRemoveCommand extends PowerPlatformCommand { public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { return schema .refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, { - message: `Specify either 'id' or 'name', but not both.`, + error: `Specify either 'id' or 'name', but not both.`, params: { customCode: 'optionSet', options: ['id', 'name'] diff --git a/src/m365/pp/commands/tenant/tenant-settings-set.ts b/src/m365/pp/commands/tenant/tenant-settings-set.ts index d31b124a172..55f7254b97e 100644 --- a/src/m365/pp/commands/tenant/tenant-settings-set.ts +++ b/src/m365/pp/commands/tenant/tenant-settings-set.ts @@ -23,7 +23,7 @@ export const options = z.strictObject({ const num = Number(val); return Number.isInteger(num) && num >= 0; }, { - message: 'The value must be a non-negative integer.' + error: 'The value must be a non-negative integer.' }).optional(), disableShareWithEveryone: z.boolean().optional(), enableGuestsToMake: z.boolean().optional(), @@ -37,7 +37,7 @@ export const options = z.strictObject({ const num = Number(val); return Number.isInteger(num) && num >= 0; }, { - message: 'The value must be a non-negative integer.' + error: 'The value must be a non-negative integer.' }).optional(), disableChampionsInvitationReachout: z.boolean().optional(), disableSkillsMatchInvitationReachout: z.boolean().optional(), @@ -95,7 +95,7 @@ class PpTenantSettingsSetCommand extends PowerPlatformCommand { opts.disableCopilot !== undefined || opts.enableOpenAiBotPublishing !== undefined || opts.enableModelDataSharing !== undefined, { - message: 'Specify at least one option.', + error: 'Specify at least one option.', params: { customCode: 'required' } From c0ad63ed728b1a27a0f3e80d9627d21537102c71 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Fri, 26 Jun 2026 11:50:57 +0200 Subject: [PATCH 3/3] chore: remove .impeccable cache file from tracking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 +- .impeccable/hook.cache.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .impeccable/hook.cache.json diff --git a/.gitignore b/.gitignore index 4146a43b816..fd6f8370b91 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,4 @@ docs/.env.production.local docs/npm-debug.log* docs/yarn-debug.log* -docs/yarn-error.log* \ No newline at end of file +docs/yarn-error.log*.impeccable/ diff --git a/.impeccable/hook.cache.json b/.impeccable/hook.cache.json deleted file mode 100644 index 22c7d8b2569..00000000000 --- a/.impeccable/hook.cache.json +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"sessions":{"60b0061d-4551-4329-b6f1-1e0c26dba8ee":{"updatedAt":1782371249716,"files":{"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/gateway/gateway-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-add.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publish.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-add.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-get.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-list.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-publisher-remove.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/solution/solution-remove.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/tenant/tenant-settings-set.ts":{"editCount":1,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/gateway/gateway-get.spec.ts":{"editCount":5,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-add.spec.ts":{"editCount":6,"findings":[]},"/Users/waldek/.copilot/copilot-worktrees/cli-microsoft365/waldekmastykarz-didactic-doodle/src/m365/pp/commands/managementapp/managementapp-list.spec.ts":{"editCount":4,"findings":[]}}}}} \ No newline at end of file