From f6310b9bb0af3308338e8a7e1b3d95bc3e3f078a Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sat, 27 Jun 2026 17:12:26 +0200 Subject: [PATCH 1/3] Migrates 'spo applicationcustomizer' and 'spo apppage' commands to Zod. Closes #7320 --- .../applicationcustomizer-add.spec.ts | 51 +++-- .../applicationcustomizer-add.ts | 138 ++++-------- .../applicationcustomizer-get.spec.ts | 209 +++++++----------- .../applicationcustomizer-get.ts | 118 +++------- .../applicationcustomizer-list.spec.ts | 59 ++--- .../applicationcustomizer-list.ts | 59 ++--- .../applicationcustomizer-remove.spec.ts | 79 ++++--- .../applicationcustomizer-remove.ts | 113 +++------- .../applicationcustomizer-set.spec.ts | 97 ++++---- .../applicationcustomizer-set.ts | 176 ++++++--------- 10 files changed, 418 insertions(+), 681 deletions(-) diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts index afda74b1b0a..ab54064e2fb 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts @@ -10,7 +10,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 './applicationcustomizer-add.js'; +import command, { options } from './applicationcustomizer-add.js'; describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { const webUrl = 'https://contoso.sharepoint.com'; @@ -48,6 +48,7 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { let log: any[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let loggerLogToStderrSpy: sinon.SinonSpy; before(() => { @@ -57,6 +58,7 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -103,7 +105,7 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { throw customActionError; }); - await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, scope: 'Web' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, scope: 'Web' }) }); assert.deepStrictEqual(postStub.firstCall.args[0].data, { Title: title, Name: title, @@ -124,7 +126,7 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { throw customActionError; }); - await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, clientSideComponentProperties: clientSideComponentProperties, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, clientSideComponentProperties: clientSideComponentProperties, verbose: true }) }); assert.deepStrictEqual(postStub.firstCall.args[0].data, { Title: title, Name: title, @@ -146,7 +148,7 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { throw customActionError; }); - await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, hostProperties: clientSideComponentProperties, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, hostProperties: clientSideComponentProperties, verbose: true }) }); assert.deepStrictEqual(postStub.firstCall.args[0].data, { Title: title, Name: title, @@ -158,33 +160,38 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { assert(loggerLogToStderrSpy.called); }); - it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: 'foo', title: title, clientSideComponentId: clientSideComponentId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the webUrl option is not a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: 'foo', title: title, clientSideComponentId: clientSideComponentId }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentId option is not a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentId option is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the scope option is not a valid scope', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, scope: 'Invalid scope' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the scope option is not a valid scope', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, scope: 'Invalid scope' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentProperties option is not a valid json string', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: 'invalid json string' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the hostProperties option is not a valid json string', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, hostProperties: 'invalid json string' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentProperties option is not a valid json string', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: 'invalid json string' }); + assert.notStrictEqual(actual.success, true); }); - it('passes validation if all options are passed', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: clientSideComponentProperties, hostProperties: clientSideComponentProperties, scope: 'Site' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if the hostProperties option is not a valid json string', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, hostProperties: 'invalid json string' }); + assert.notStrictEqual(actual.success, true); + }); + + it('passes validation if all options are passed', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: clientSideComponentProperties, hostProperties: clientSideComponentProperties, scope: 'Site' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts index 1ee837027d3..53e5354ce5f 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts @@ -1,28 +1,54 @@ +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 { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; import { CustomAction } from '../customaction/customaction.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + title: z.string().alias('t'), + webUrl: z.string().refine(val => validation.isValidSharePointUrl(val) === true, { + error: e => `${e.input} is not a valid SharePoint URL` + }).alias('u'), + clientSideComponentId: z.string().refine(val => validation.isValidGuid(val), { + error: e => `${e.input} is not a valid GUID` + }).alias('i'), + description: z.string().optional(), + clientSideComponentProperties: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: e => `An error has occurred while parsing clientSideComponentProperties: ${e.input}` + }).optional(), + hostProperties: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: e => `An error has occurred while parsing hostProperties: ${e.input}` + }).optional(), + scope: z.enum(['Site', 'Web']).optional().alias('s') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - title: string; - webUrl: string; - clientSideComponentId: string; - description?: string; - clientSideComponentProperties?: string; - hostProperties?: string; - scope?: string; -} - class SpoApplicationCustomizerAddCommand extends SpoCommand { - private static readonly scopes: string[] = ['Site', 'Web']; - public get name(): string { return commands.APPLICATIONCUSTOMIZER_ADD; } @@ -31,90 +57,8 @@ class SpoApplicationCustomizerAddCommand extends SpoCommand { return 'Add an application customizer to a site.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-t, --title ' - }, - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-i, --clientSideComponentId <clientSideComponentId>' - }, - { - option: '--description [description]' - }, - { - option: '--clientSideComponentProperties [clientSideComponentProperties]' - }, - { - option: '--hostProperties [hostProperties]' - }, - { - option: '-s, --scope [scope]', autocomplete: SpoApplicationCustomizerAddCommand.scopes - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - description: typeof args.options.description !== 'undefined', - clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', - hostProperties: typeof args.options.hostProperties !== 'undefined', - scope: typeof args.options.scope !== 'undefined' - }); - }); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.webUrl) { - const isValidSharePointUrl: boolean | string = validation.isValidSharePointUrl(args.options.webUrl); - if (isValidSharePointUrl !== true) { - return isValidSharePointUrl; - } - } - - if (!validation.isValidGuid(args.options.clientSideComponentId)) { - return `${args.options.clientSideComponentId} is not a valid GUID`; - } - - if (args.options.clientSideComponentProperties) { - try { - JSON.parse(args.options.clientSideComponentProperties); - } - catch (e) { - return `An error has occurred while parsing clientSideComponentProperties: ${e}`; - } - } - - if (args.options.hostProperties) { - try { - JSON.parse(args.options.hostProperties); - } - catch (e) { - return `An error has occurred while parsing hostProperties: ${e}`; - } - } - - if (args.options.scope && SpoApplicationCustomizerAddCommand.scopes.indexOf(args.options.scope) < 0) { - return `${args.options.scope} is not a valid value for allowedMembers. Valid values are ${SpoApplicationCustomizerAddCommand.scopes.join(', ')}`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.spec.ts index b655f66a928..c598bab23ce 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.spec.ts @@ -11,7 +11,7 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './applicationcustomizer-get.js'; +import command, { options } from './applicationcustomizer-get.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.APPLICATIONCUSTOMIZER_GET, () => { @@ -73,6 +73,7 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -82,6 +83,7 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { auth.connection.active = true; auth.connection.spoUrl = webUrl; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -122,138 +124,89 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'abc', webUrl: webUrl } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'abc', webUrl: webUrl }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentId is not a valid GUID', async () => { - const actual = await command.validate({ options: { clientSideComponentId: 'abc', webUrl: webUrl } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ clientSideComponentId: 'abc', webUrl: webUrl }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the url option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ - options: - { - id: id, - webUrl: 'foo' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation when all options are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if the url option is not a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ + id: id, + webUrl: 'foo' }); - - const actual = await command.validate({ - options: { - title: title, - id: id, - clientSideComponentId: clientSideComponentId, - webUrl: webUrl - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation when no options are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation when all options are specified', () => { + const actual = commandOptionsSchema.safeParse({ + title: title, + id: id, + clientSideComponentId: clientSideComponentId, + webUrl: webUrl }); - - const actual = await command.validate({ - options: { - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation when title and id options are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } + it('fails validation when no options are specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); + }); - return defaultValue; + it('fails validation when title and id options are specified', () => { + const actual = commandOptionsSchema.safeParse({ + title: title, + id: id, + webUrl: webUrl }); - - const actual = await command.validate({ - options: { - title: title, - id: id, - webUrl: webUrl - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation when title and clientSideComponentId options are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation when title and clientSideComponentId options are specified', () => { + const actual = commandOptionsSchema.safeParse({ + title: title, + clientSideComponentId: clientSideComponentId, + webUrl: webUrl }); - - const actual = await command.validate({ - options: { - title: title, - clientSideComponentId: clientSideComponentId, - webUrl: webUrl - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation when id and clientSideComponentId options are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation when id and clientSideComponentId options are specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: id, + clientSideComponentId: clientSideComponentId, + webUrl: webUrl }); + assert.strictEqual(actual.success, false); + }); - const actual = await command.validate({ - options: { - id: id, - clientSideComponentId: clientSideComponentId, - webUrl: webUrl - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the scope is not a valid application customizer scope', () => { + const actual = commandOptionsSchema.safeParse({ id: id, webUrl: webUrl, scope: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the scope is not a valid application customizer scope', async () => { - const actual = await command.validate({ options: { id: id, webUrl: webUrl, scope: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('passes validation if id is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: id, webUrl: webUrl }); + assert.strictEqual(actual.success, true); }); - it('passes validation if id is a valid GUID', async () => { - const actual = await command.validate({ options: { id: id, webUrl: webUrl } }, commandInfo); - assert.strictEqual(actual, true); + it('passed validation when title specified', () => { + const actual = commandOptionsSchema.safeParse({ title: title, webUrl: webUrl }); + assert.strictEqual(actual.success, true); }); - it('passed validation when title specified', async () => { - const actual = await command.validate({ options: { title: title, webUrl: webUrl } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if clientSideComponentId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl }); + assert.strictEqual(actual.success, true); }); - it('passes validation if clientSideComponentId is a valid GUID', async () => { - const actual = await command.validate({ options: { clientSideComponentId: clientSideComponentId, webUrl: webUrl } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, title: title, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('humanize scope shows correct value when scope odata is 2', () => { @@ -281,11 +234,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, scope: 'Web' - } + }) }); assert(loggerLogSpy.calledOnceWithExactly(applicationCustomizerGetOutput)); @@ -307,11 +260,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, debug: true - } + }) }); assert(loggerLogSpy.calledOnceWithExactly(applicationCustomizerGetOutput)); @@ -333,11 +286,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, debug: true - } + }) }); assert(loggerLogSpy.calledOnceWithExactly(applicationCustomizerGetOutput)); @@ -353,11 +306,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, clientSideComponentProperties: true - } + }) }); assert(loggerLogSpy.calledOnceWithExactly(JSON.parse(applicationCustomizerGetOutput.ClientSideComponentProperties))); @@ -375,11 +328,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError(`No application customizer with id '${id}' found`)); }); @@ -396,11 +349,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError(`No application customizer with title '${title}' found`)); }); @@ -414,11 +367,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError(`No application customizer with Client Side Component Id '${clientSideComponentId}' found`)); }); @@ -487,11 +440,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError("Multiple application customizers with title 'Some customizer' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -560,11 +513,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError("Multiple application customizers with Client Side Component Id '7096cded-b83d-4eab-96f0-df477ed7c0bc' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -653,11 +606,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, debug: true - } + }) }); assert(loggerLogSpy.calledOnceWithExactly(applicationCustomizerGetOutput)); @@ -693,11 +646,11 @@ describe(commands.APPLICATIONCUSTOMIZER_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, scope: 'Web' - } + }) }), new CommandError(`No application customizer with id '${id}' found`)); }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts index c5824d12e29..ed212f00aea 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts @@ -1,29 +1,31 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import { formatting } from '../../../../utils/formatting.js'; import { spo } from '../../../../utils/spo.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; import { CustomAction } from '../customaction/customaction.js'; import { cli } from '../../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + title: z.string().optional().alias('t'), + id: z.string().optional().alias('i'), + clientSideComponentId: z.string().optional().alias('c'), + scope: z.enum(['All', 'Site', 'Web']).optional().alias('s'), + clientSideComponentProperties: z.boolean().optional().alias('p') +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - title?: string; - id?: string; - clientSideComponentId?: string; - scope?: string; - clientSideComponentProperties?: boolean; -} - class SpoApplicationCustomizerGetCommand extends SpoCommand { - public readonly allowedScopes: string[] = ['All', 'Site', 'Web']; - public get name(): string { return commands.APPLICATIONCUSTOMIZER_GET; } @@ -32,79 +34,31 @@ class SpoApplicationCustomizerGetCommand extends SpoCommand { return 'Get an application customizer that is added to a site.'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - this.#initTypes(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - title: typeof args.options.title !== 'undefined', - id: typeof args.options.id !== 'undefined', - clientSideComponentId: typeof args.options.clientSideComponentId !== 'undefined', - scope: typeof args.options.scope !== 'undefined', - clientSideComponentProperties: !!args.options.clientSideComponentProperties - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-t, --title [title]' - }, - { - option: '-i, --id [id]' - }, - { - option: '-c, --clientSideComponentId [clientSideComponentId]' - }, - { - option: '-s, --scope [scope]', - autocomplete: this.allowedScopes - }, - { - option: '-p, --clientSideComponentProperties' - } - ); + public get schema(): z.ZodType | 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<any> | undefined { + return schema + .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { + error: () => 'SharePoint Online site URL must be a string.', + path: ['webUrl'] + }) + .refine(args => !args.id || validation.isValidGuid(args.id), { + error: () => 'id is not a valid GUID', + path: ['id'] + }) + .refine(args => !args.clientSideComponentId || validation.isValidGuid(args.clientSideComponentId), { + error: () => 'clientSideComponentId is not a valid GUID', + path: ['clientSideComponentId'] + }) + .refine(args => [args.title, args.id, args.clientSideComponentId].filter(value => value !== undefined).length === 1, { + error: `Specify either 'title', 'id', or 'clientSideComponentId'.`, + params: { + customCode: 'optionSet', + options: ['title', 'id', 'clientSideComponentId'] } - - if (args.options.clientSideComponentId && !validation.isValidGuid(args.options.clientSideComponentId)) { - return `${args.options.clientSideComponentId} is not a valid GUID`; - } - - if (args.options.scope && this.allowedScopes.indexOf(args.options.scope) === -1) { - return `'${args.options.scope}' is not a valid application customizer scope. Allowed values are: ${this.allowedScopes.join(',')}`; - } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['title', 'id', 'clientSideComponentId'] }); - } - - #initTypes(): void { - this.types.string.push('webUrl', 'title', 'id', 'clientSideComponentId', 'scope'); - this.types.boolean.push('clientSideComponentProperties'); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.spec.ts index 252182fc59c..2245d233da3 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.spec.ts @@ -11,12 +11,13 @@ 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 './applicationcustomizer-list.js'; +import command, { options } from './applicationcustomizer-list.js'; describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let loggerLogSpy: sinon.SinonSpy; //#region Mocked Responses @@ -59,6 +60,7 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -100,34 +102,33 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['Name', 'Location', 'Scope', 'Id']); }); - it('fails validation if the url option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ - options: - { - webUrl: 'foo' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the url option is not a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ + webUrl: 'foo' + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the scope is not a valid scope', async () => { - const actual = await command.validate({ - options: - { - webUrl: validWebUrl, scope: 'Invalid Scope' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the scope is not a valid scope', () => { + const actual = commandOptionsSchema.safeParse({ + webUrl: validWebUrl, scope: 'Invalid Scope' + }); + assert.strictEqual(actual.success, false); }); - it('passes validation when a valid webUrl specified', async () => { - const actual = await command.validate({ - options: - { - webUrl: "https://contoso.sharepoint.com" - } - }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when a valid webUrl specified', () => { + const actual = commandOptionsSchema.safeParse({ + webUrl: "https://contoso.sharepoint.com" + }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ + webUrl: validWebUrl, + unknownOption: 'value' + }); + assert.strictEqual(actual.success, false); }); it('retrieves applicationcustomizers', async () => { @@ -143,7 +144,7 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, webUrl: validWebUrl } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, webUrl: validWebUrl }) }); assert(loggerLogSpy.calledWith([ ...applicationcustomizerResponse.value, ...applicationcustomizerResponse.value @@ -159,7 +160,7 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { webUrl: validWebUrl, scope: 'Site' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: validWebUrl, scope: 'Site' }) }); assert(loggerLogSpy.calledWith(applicationcustomizerResponse.value)); }); @@ -172,7 +173,7 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { webUrl: validWebUrl, scope: 'Web' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: validWebUrl, scope: 'Web' }) }); assert(loggerLogSpy.calledWith(applicationcustomizerResponse.value)); }); @@ -185,7 +186,7 @@ describe(commands.APPLICATIONCUSTOMIZER_LIST, () => { } }); - await assert.rejects(command.action(logger, { options: { webUrl: validWebUrl, scope: 'Site' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ webUrl: validWebUrl, scope: 'Site' }) }), new CommandError(error)); }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts index 954b8987cfb..bc5318a9335 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts @@ -1,22 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import { spo } from '../../../../utils/spo.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + scope: z.enum(['All', 'Site', 'Web']).optional().alias('s') +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - scope?: string -} - class SpoApplicationCustomizerListCommand extends SpoCommand { - private static readonly scopes: string[] = ['All', 'Site', 'Web']; - public get name(): string { return commands.APPLICATIONCUSTOMIZER_LIST; } @@ -29,46 +31,17 @@ class SpoApplicationCustomizerListCommand extends SpoCommand { return ['Name', 'Location', 'Scope', 'Id']; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); + public get schema(): z.ZodType | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-s, --scope [scope]', - autocomplete: SpoApplicationCustomizerListCommand.scopes - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - scope: typeof args.options.scope !== 'undefined' - }); + public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { + return schema.refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { + error: () => 'SharePoint Online site URL must be a string.', + path: ['webUrl'] }); } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.scope && SpoApplicationCustomizerListCommand.scopes.indexOf(args.options.scope) < 0) { - return `${args.options.scope} is not a valid scope. Allowed values are ${SpoApplicationCustomizerListCommand.scopes.join(', ')}`; - } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); - } - public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { if (this.verbose) { await logger.logToStderr(`Retrieving application customizers...`); diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.spec.ts index deffa58baf5..6e80b7093b1 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.spec.ts @@ -12,11 +12,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 './applicationcustomizer-remove.js'; +import command, { options } from './applicationcustomizer-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const webUrl = 'https://contoso.sharepoint.com'; const id = '14125658-a9bc-4ddf-9c75-1b5767c9a337'; const clientSideComponentId = '015e0fcf-fe9d-4037-95af-0a4776cdfbb4'; @@ -119,6 +120,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { sinon.stub(session, 'getId').callsFake(() => ''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => { if (settingName === 'prompt') { return false; @@ -174,58 +176,55 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: 'foo', id: id } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the webUrl option is not a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: 'foo', id: id }); + assert.strictEqual(actual.success, false); }); - it('passes validation if the webUrl option is a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if the webUrl option is a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id }); + assert.strictEqual(actual.success, true); }); - it('passes validation if at least one of the parameters has a value', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if at least one of the parameters has a value', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id }); + assert.strictEqual(actual.success, true); }); - it('fails validation when all parameters are empty', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); + it('fails validation when all parameters are empty', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: null, clientSideComponentId: null, title: '' }); + assert.strictEqual(actual.success, false); + }); - const actual = await command.validate({ options: { webUrl: webUrl, id: null, clientSideComponentId: null, title: '' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentId option is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, clientSideComponentId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentId option is not a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, clientSideComponentId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id option is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the id option is not a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the scope option is not a valid scope', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, scope: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the scope option is not a valid scope', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, scope: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('should prompt before removing application customizer when confirmation argument not passed', async () => { - await command.action(logger, { options: { webUrl: webUrl, id: id } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, id: id }) }); assert(promptIssued); }); it('aborts removing application customizer when prompt not confirmed', async () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { webUrl: webUrl, id: id } }); + await command.action(logger, { options: commandOptionsSchema.parse({ webUrl: webUrl, id: id }) }); assert(requests.length === 0); }); @@ -239,7 +238,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { await assert.rejects( command.action(logger, { - options: { id: id, webUrl: webUrl, force: true } + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, force: true }) } ), new CommandError(`No application customizer with id '${id}' found`)); }); @@ -254,7 +253,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { await assert.rejects( command.action(logger, { - options: { title: title, webUrl: webUrl, force: true } + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, force: true }) } ), new CommandError(`No application customizer with title '${title}' found`)); }); @@ -269,7 +268,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { await assert.rejects( command.action(logger, { - options: { clientSideComponentId: clientSideComponentId, webUrl: webUrl, force: true } + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, force: true }) } ), new CommandError(`No application customizer with ClientSideComponentId '${clientSideComponentId}' found`)); }); @@ -292,7 +291,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { await assert.rejects( command.action(logger, { - options: { title: title, webUrl: webUrl, scope: 'Site', force: true } + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, scope: 'Site', force: true }) } ), new CommandError("Multiple application customizer with title 'SiteGuidedTour' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -315,7 +314,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { await assert.rejects( command.action(logger, { - options: { clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Site', force: true } + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Site', force: true }) } ), new CommandError("Multiple application customizer with ClientSideComponentId '015e0fcf-fe9d-4037-95af-0a4776cdfbb4' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -334,7 +333,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(singleResponse.value[0]); const deleteCallsSpy: sinon.SinonStub = defaultDeleteCallsStub(); - await command.action(logger, { options: { verbose: true, title: title, webUrl: webUrl, scope: 'Web', force: true } } as any); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, title: title, webUrl: webUrl, scope: 'Web', force: true }) }); assert(deleteCallsSpy.calledOnce); }); @@ -350,7 +349,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { verbose: true, id: id, webUrl: webUrl, scope: 'Web' } } as any); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, id: id, webUrl: webUrl, scope: 'Web' }) }); assert(deleteCallsSpy.calledOnce); }); @@ -365,7 +364,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { }); const deleteCallsSpy: sinon.SinonStub = defaultDeleteCallsStub(); - await command.action(logger, { options: { verbose: true, id: id, webUrl: webUrl, scope: 'Site', force: true } } as any); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, id: id, webUrl: webUrl, scope: 'Site', force: true }) }); assert(deleteCallsSpy.calledOnce); }); @@ -381,7 +380,7 @@ describe(commands.APPLICATIONCUSTOMIZER_REMOVE, () => { }); const deleteCallsSpy: sinon.SinonStub = defaultDeleteCallsStub(); - await command.action(logger, { options: { verbose: true, clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web', force: true } } as any); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web', force: true }) }); assert(deleteCallsSpy.calledOnce); }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts index 23205a60af5..efb3567eb15 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts @@ -1,6 +1,7 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; @@ -9,22 +10,23 @@ import { spo } from '../../../../utils/spo.js'; import { formatting } from '../../../../utils/formatting.js'; import { CustomAction } from '../../commands/customaction/customaction.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + title: z.string().optional().alias('t'), + id: z.string().optional().alias('i'), + clientSideComponentId: z.string().optional().alias('c'), + scope: z.enum(['Site', 'Web', 'All']).optional().alias('s'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - title?: string; - id?: string; - clientSideComponentId?: string; - scope?: string; - force?: boolean; -} - class SpoApplicationCustomizerRemoveCommand extends SpoCommand { - private readonly allowedScopes: string[] = ['Site', 'Web', 'All']; - public get name(): string { return commands.APPLICATIONCUSTOMIZER_REMOVE; } @@ -33,74 +35,31 @@ class SpoApplicationCustomizerRemoveCommand extends SpoCommand { return 'Removes an application customizer that is added to a site'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-t, --title [title]' - }, - { - option: '-i, --id [id]' - }, - { - option: '-c, --clientSideComponentId [clientSideComponentId]' - }, - { - option: '-s, --scope [scope]', autocomplete: this.allowedScopes - }, - { - option: '-f, --force' - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - title: typeof args.options.title !== 'undefined', - id: typeof args.options.id !== 'undefined', - clientSideComponentId: typeof args.options.clientSideComponentId !== 'undefined', - scope: typeof args.options.scope !== 'undefined', - force: !!args.options.force - }); - }); + public get schema(): z.ZodType | 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<any> | undefined { + return schema + .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { + error: () => 'SharePoint Online site URL must be a string.', + path: ['webUrl'] + }) + .refine(args => !args.id || validation.isValidGuid(args.id), { + error: () => 'id is not a valid GUID', + path: ['id'] + }) + .refine(args => !args.clientSideComponentId || validation.isValidGuid(args.clientSideComponentId), { + error: () => 'clientSideComponentId is not a valid GUID', + path: ['clientSideComponentId'] + }) + .refine(args => [args.id, args.title, args.clientSideComponentId].filter(value => value !== undefined).length === 1, { + error: `Specify either 'id', 'title', or 'clientSideComponentId'.`, + params: { + customCode: 'optionSet', + options: ['id', 'title', 'clientSideComponentId'] } - - if (args.options.clientSideComponentId && !validation.isValidGuid(args.options.clientSideComponentId)) { - return `${args.options.clientSideComponentId} is not a valid GUID`; - } - - if (args.options.scope && this.allowedScopes.indexOf(args.options.scope) === -1) { - return `'${args.options.scope}' is not a valid application customizer scope. Allowed values are: ${this.allowedScopes.join(',')}`; - } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'title', 'clientSideComponentId'] } - ); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts index 0b3fba8251b..aabb6f2a8fd 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.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 './applicationcustomizer-set.js'; +import command, { options } from './applicationcustomizer-set.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.APPLICATIONCUSTOMIZER_SET, () => { @@ -27,6 +27,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { const hostProperties = '{"preAllocatedApplicationCustomizerTopHeight":"50","preAllocatedApplicationCustomizerBottomHeight":"50"}'; let log: any[]; let logger: Logger; + let commandOptionsSchema: typeof options; const singleResponse = { value: [ @@ -120,6 +121,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { sinon.stub(session, 'getId').callsFake(() => ''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => { if (settingName === 'prompt') { return false; @@ -166,62 +168,59 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: 'foo', id: id } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the webUrl option is not a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: 'foo', id: id }); + assert.strictEqual(actual.success, false); }); - it('passes validation if the webUrl option is a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, newTitle: newTitle } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if the webUrl option is a valid SharePoint site URL', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, newTitle: newTitle }); + assert.strictEqual(actual.success, true); }); - it('passes validation if at least one of the parameters has a value', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, clientSideComponentProperties: clientSideComponentProperties } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if at least one of the parameters has a value', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, clientSideComponentProperties: clientSideComponentProperties }); + assert.strictEqual(actual.success, true); }); - it('passes validation when an empty description is passed', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, description: '' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when an empty description is passed', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, description: '' }); + assert.strictEqual(actual.success, true); }); - it('fails validation when all parameters are empty', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); + it('fails validation when all parameters are empty', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: null, clientSideComponentId: null, title: '', newTitle: newTitle }); + assert.strictEqual(actual.success, false); + }); - const actual = await command.validate({ options: { webUrl: webUrl, id: null, clientSideComponentId: null, title: '', newTitle: newTitle } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentId option is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, clientSideComponentId: 'invalid', newTitle: newTitle }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentId option is not a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, clientSideComponentId: 'invalid', newTitle: newTitle } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id option is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: 'invalid', newTitle: newTitle }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the id option is not a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: 'invalid', newTitle: newTitle } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the scope option is not a valid scope', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, scope: 'invalid', newTitle: newTitle }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the scope option is not a valid scope', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, scope: 'invalid', newTitle: newTitle } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the clientSideComponentProperties option is not a valid json string', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, clientSideComponentProperties: 'invalid json string' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the clientSideComponentProperties option is not a valid json string', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, clientSideComponentProperties: 'invalid json string' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the hostProperties option is not a valid json string', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, hostProperties: 'invalid json string' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the hostProperties option is not a valid json string', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, id: id, hostProperties: 'invalid json string' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: id, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('handles error when no application customizer with the specified id found', async () => { @@ -234,7 +233,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { await assert.rejects( command.action(logger, { - options: { id: id, webUrl: webUrl, newTitle: newTitle } + options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, newTitle: newTitle }) } ), new CommandError(`No application customizer with id '${id}' found`)); }); @@ -249,7 +248,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { await assert.rejects( command.action(logger, { - options: { title: title, webUrl: webUrl, newTitle: newTitle } + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, newTitle: newTitle }) } ), new CommandError(`No application customizer with title '${title}' found`)); }); @@ -264,7 +263,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { await assert.rejects( command.action(logger, { - options: { clientSideComponentId: clientSideComponentId, webUrl: webUrl, newTitle: newTitle } + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, newTitle: newTitle }) } ), new CommandError(`No application customizer with ClientSideComponentId '${clientSideComponentId}' found`)); }); @@ -287,7 +286,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { await assert.rejects( command.action(logger, { - options: { title: title, webUrl: webUrl, scope: 'Site', newTitle: newTitle } + options: commandOptionsSchema.parse({ title: title, webUrl: webUrl, scope: 'Site', newTitle: newTitle }) } ), new CommandError("Multiple application customizer with title 'SiteGuidedTour' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -310,7 +309,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { await assert.rejects( command.action(logger, { - options: { clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Site', newTitle: newTitle } + options: commandOptionsSchema.parse({ clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Site', newTitle: newTitle }) } ), new CommandError("Multiple application customizer with ClientSideComponentId '015e0fcf-fe9d-4037-95af-0a4776cdfbb4' found. Found: a70d8013-3b9f-4601-93a5-0e453ab9a1f3, 63aa745f-b4dd-4055-a4d7-d9032a0cfc59.")); }); @@ -326,7 +325,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(singleResponse.value[0]); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { verbose: true, title: title, webUrl: webUrl, scope: 'Site', newTitle: newTitle } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, title: title, webUrl: webUrl, scope: 'Site', newTitle: newTitle }) }); assert(updateCallsSpy.calledOnce); }); @@ -339,7 +338,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { }); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { verbose: true, id: id, webUrl: webUrl, scope: 'Web', newTitle: newTitle, description: description } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, id: id, webUrl: webUrl, scope: 'Web', newTitle: newTitle, description: description }) }); assert(updateCallsSpy.calledOnce); }); @@ -354,7 +353,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { }); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { verbose: true, id: id, webUrl: webUrl, scope: 'Site', newTitle: newTitle } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, id: id, webUrl: webUrl, scope: 'Site', newTitle: newTitle }) }); assert(updateCallsSpy.calledOnce); }); @@ -370,7 +369,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { }); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { verbose: true, clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web', clientSideComponentProperties: clientSideComponentProperties } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, clientSideComponentId: clientSideComponentId, webUrl: webUrl, scope: 'Web', clientSideComponentProperties: clientSideComponentProperties }) }); assert(updateCallsSpy.calledOnce); }); @@ -383,7 +382,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { }); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { id: id, webUrl: webUrl, scope: 'Web', description: '' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, scope: 'Web', description: '' }) }); assert(updateCallsSpy.calledOnce); assert.deepStrictEqual(updateCallsSpy.firstCall.args[0].data, { @@ -401,7 +400,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { }); const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); - await command.action(logger, { options: { id: id, webUrl: webUrl, scope: 'Web', hostProperties: hostProperties } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: id, webUrl: webUrl, scope: 'Web', hostProperties: hostProperties }) }); assert(updateCallsSpy.calledOnce); assert.deepStrictEqual(updateCallsSpy.firstCall.args[0].data, { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts index 2616e8d028d..9b028795b3d 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.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 { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; @@ -9,25 +10,46 @@ import { formatting } from '../../../../utils/formatting.js'; import { CustomAction } from '../../commands/customaction/customaction.js'; import { cli } from '../../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + title: z.string().optional().alias('t'), + id: z.string().optional().alias('i'), + clientSideComponentId: z.string().optional().alias('c'), + newTitle: z.string().optional(), + description: z.string().optional(), + clientSideComponentProperties: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: e => `An error has occurred while parsing clientSideComponentProperties: ${e.input}` + }).optional().alias('p'), + hostProperties: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: e => `An error has occurred while parsing hostProperties: ${e.input}` + }).optional(), + scope: z.enum(['Site', 'Web', 'All']).optional().alias('s') +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - title?: string; - id?: string; - clientSideComponentId?: string; - newTitle?: string; - description?: string; - clientSideComponentProperties?: string; - hostProperties?: string; - scope?: string; -} - class SpoApplicationCustomizerSetCommand extends SpoCommand { - private readonly allowedScopes: string[] = ['Site', 'Web', 'All']; - public get name(): string { return commands.APPLICATIONCUSTOMIZER_SET; } @@ -36,108 +58,34 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { return 'Updates an existing Application Customizer on a site'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); + public get schema(): z.ZodType | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-t, --title [title]' - }, - { - option: '-i, --id [id]' - }, - { - option: '-c, --clientSideComponentId [clientSideComponentId]' - }, - { - option: '--newTitle [newTitle]' - }, - { - option: '--description [description]' - }, - { - option: '-p, --clientSideComponentProperties [clientSideComponentProperties]' - }, - { - option: '--hostProperties [hostProperties]' - }, - { - option: '-s, --scope [scope]', autocomplete: this.allowedScopes - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - title: typeof args.options.title !== 'undefined', - id: typeof args.options.id !== 'undefined', - clientSideComponentId: typeof args.options.clientSideComponentId !== 'undefined', - newTitle: typeof args.options.newTitle !== 'undefined', - description: typeof args.options.description !== 'undefined', - clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', - hostProperties: typeof args.options.hostProperties !== 'undefined', - scope: typeof args.options.scope !== 'undefined' - }); - }); - } - - #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`; - } - - if (args.options.clientSideComponentId && !validation.isValidGuid(args.options.clientSideComponentId)) { - return `${args.options.clientSideComponentId} is not a valid GUID`; - } - - if (args.options.scope && this.allowedScopes.indexOf(args.options.scope) === -1) { - return `'${args.options.scope}' is not a valid application customizer scope. Allowed values are: ${this.allowedScopes.join(',')}`; - } - - if (args.options.clientSideComponentProperties) { - try { - JSON.parse(args.options.clientSideComponentProperties); - } - catch (e) { - return `An error has occurred while parsing clientSideComponentProperties: ${e}`; - } - } - - if (args.options.hostProperties) { - try { - JSON.parse(args.options.hostProperties); - } - catch (e) { - return `An error has occurred while parsing hostProperties: ${e}`; - } - } - - if (!args.options.newTitle && args.options.description === undefined && !args.options.clientSideComponentProperties && args.options.hostProperties === undefined) { - return `Please specify an option to be updated`; + public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { + return schema + .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { + error: () => 'SharePoint Online site URL must be a string.', + path: ['webUrl'] + }) + .refine(args => !args.id || validation.isValidGuid(args.id), { + error: () => 'id is not a valid GUID', + path: ['id'] + }) + .refine(args => !args.clientSideComponentId || validation.isValidGuid(args.clientSideComponentId), { + error: () => 'clientSideComponentId is not a valid GUID', + path: ['clientSideComponentId'] + }) + .refine(args => [args.id, args.title, args.clientSideComponentId].filter(value => value !== undefined).length === 1, { + error: `Specify either 'id', 'title', or 'clientSideComponentId'.`, + params: { + customCode: 'optionSet', + options: ['id', 'title', 'clientSideComponentId'] } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['id', 'title', 'clientSideComponentId'] } - ); + }) + .refine(args => args.newTitle !== undefined || args.description !== undefined || args.clientSideComponentProperties !== undefined || args.hostProperties !== undefined, { + error: `Please specify an option to be updated` + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { From 457467957b4750b8f39f730c4a9c7ec4b6f7677a Mon Sep 17 00:00:00 2001 From: Martin Machacek <machacek@edhouse.cz> Date: Sat, 27 Jun 2026 17:21:11 +0200 Subject: [PATCH 2/3] Migrates 'spo applicationcustomizer' and 'spo apppage' commands to Zod. Closes #7320 --- .../spo/commands/apppage/apppage-add.spec.ts | 58 ++---- src/m365/spo/commands/apppage/apppage-add.ts | 82 +++----- .../spo/commands/apppage/apppage-set.spec.ts | 197 ++++++------------ src/m365/spo/commands/apppage/apppage-set.ts | 66 +++--- 4 files changed, 137 insertions(+), 266 deletions(-) diff --git a/src/m365/spo/commands/apppage/apppage-add.spec.ts b/src/m365/spo/commands/apppage/apppage-add.spec.ts index 5ae6c24de65..f8af0f37be6 100644 --- a/src/m365/spo/commands/apppage/apppage-add.spec.ts +++ b/src/m365/spo/commands/apppage/apppage-add.spec.ts @@ -11,12 +11,13 @@ 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 './apppage-add.js'; +import command, { options } from './apppage-add.js'; describe(commands.APPPAGE_ADD, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -25,6 +26,7 @@ describe(commands.APPPAGE_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -143,7 +145,7 @@ describe(commands.APPPAGE_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, title: 'test-single', webUrl: 'https://contoso.sharepoint.com/', webPartData: "{\"id\": \"e84a4f44-30d2-4962-b203-f8bf42114860\", \"instanceId\": \"15353e8b-cb55-4794-b871-4cd74abf78b4\", \"title\": \"Milestone Tracking\", \"description\": \"A tool used for tracking project milestones\", \"serverProcessedContent\": { \"htmlStrings\": {}, \"searchablePlainTexts\": {}, \"imageSources\": {}, \"links\": {} }, \"dataVersion\": \"1.0\", \"properties\": {\"description\": \"Milestone Tracking\"}}" } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, title: 'test-single', webUrl: 'https://contoso.sharepoint.com/', webPartData: "{\"id\": \"e84a4f44-30d2-4962-b203-f8bf42114860\", \"instanceId\": \"15353e8b-cb55-4794-b871-4cd74abf78b4\", \"title\": \"Milestone Tracking\", \"description\": \"A tool used for tracking project milestones\", \"serverProcessedContent\": { \"htmlStrings\": {}, \"searchablePlainTexts\": {}, \"imageSources\": {}, \"links\": {} }, \"dataVersion\": \"1.0\", \"properties\": {\"description\": \"Milestone Tracking\"}}" }) }); }); it('creates a single-part app page showing on quicklaunch', async () => { @@ -228,7 +230,7 @@ describe(commands.APPPAGE_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, addToQuickLaunch: true, title: 'test-single', webUrl: 'https://contoso.sharepoint.com/', webPartData: "{\"id\": \"e84a4f44-30d2-4962-b203-f8bf42114860\", \"instanceId\": \"15353e8b-cb55-4794-b871-4cd74abf78b4\", \"title\": \"Milestone Tracking\", \"description\": \"A tool used for tracking project milestones\", \"serverProcessedContent\": { \"htmlStrings\": {}, \"searchablePlainTexts\": {}, \"imageSources\": {}, \"links\": {} }, \"dataVersion\": \"1.0\", \"properties\": {\"description\": \"Milestone Tracking\"}}" } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, addToQuickLaunch: true, title: 'test-single', webUrl: 'https://contoso.sharepoint.com/', webPartData: "{\"id\": \"e84a4f44-30d2-4962-b203-f8bf42114860\", \"instanceId\": \"15353e8b-cb55-4794-b871-4cd74abf78b4\", \"title\": \"Milestone Tracking\", \"description\": \"A tool used for tracking project milestones\", \"serverProcessedContent\": { \"htmlStrings\": {}, \"searchablePlainTexts\": {}, \"imageSources\": {}, \"links\": {} }, \"dataVersion\": \"1.0\", \"properties\": {\"description\": \"Milestone Tracking\"}}" }) }); }); it('fails to create a single-part app page if creating page failed', async () => { @@ -239,7 +241,7 @@ describe(commands.APPPAGE_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) }) }), new CommandError(`Failed to create a single-part app page`)); }); @@ -259,7 +261,7 @@ describe(commands.APPPAGE_ADD, () => { }); - await assert.rejects(command.action(logger, { options: { title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) }) }), new CommandError(`Page not found`)); }); @@ -337,48 +339,22 @@ describe(commands.APPPAGE_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ title: 'failme', webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) }) }), new CommandError('An error has occurred')); }); - it('supports specifying title', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--title') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if webPartData is not a valid JSON string', () => { + const actual = commandOptionsSchema.safeParse({ title: 'Contoso', webUrl: 'https://contoso', webPartData: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying webUrl', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--webUrl') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ title: 'Contoso', webPartData: '{}', webUrl: 'https://contoso.sharepoint.com', unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying webPartData', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--webPartData') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - it('fails validation if webPartData is not a valid JSON string', async () => { - const actual = await command.validate({ options: { title: 'Contoso', webUrl: 'https://contoso', webPartData: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('validation passes on all required options', async () => { - const actual = await command.validate({ options: { title: 'Contoso', webPartData: '{}', webUrl: 'https://contoso.sharepoint.com' } }, commandInfo); - assert.strictEqual(actual, true); + it('validation passes on all required options', () => { + const actual = commandOptionsSchema.safeParse({ title: 'Contoso', webPartData: '{}', webUrl: 'https://contoso.sharepoint.com' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/spo/commands/apppage/apppage-add.ts b/src/m365/spo/commands/apppage/apppage-add.ts index 0e621cdcefe..ee977f07501 100644 --- a/src/m365/spo/commands/apppage/apppage-add.ts +++ b/src/m365/spo/commands/apppage/apppage-add.ts @@ -1,21 +1,35 @@ +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 { urlUtil } from '../../../../utils/urlUtil.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + title: z.string().alias('t'), + webPartData: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: 'Specified webPartData is not a valid JSON string.' + }).alias('d'), + addToQuickLaunch: z.boolean().optional() +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - title: string; - webPartData: string; - addToQuickLaunch: boolean; -} - class SpoAppPageAddCommand extends SpoCommand { public get name(): string { return commands.APPPAGE_ADD; @@ -25,55 +39,15 @@ class SpoAppPageAddCommand extends SpoCommand { return 'Creates a single-part app page'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); + public get schema(): z.ZodType | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - addToQuickLaunch: args.options.addToQuickLaunch - }); + public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { + Object.assign(this.telemetryProperties, { + addToQuickLaunch: args.options.addToQuickLaunch }); - } - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-t, --title <title>' - }, - { - option: '-d, --webPartData <webPartData>' - }, - { - option: '--addToQuickLaunch' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - try { - JSON.parse(args.options.webPartData); - } - catch (e) { - return `Specified webPartData is not a valid JSON string. Error: ${e}`; - } - - return true; - } - ); - } - - public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { const createPageRequestOptions: CliRequestOptions = { url: `${args.options.webUrl}/_api/sitepages/Pages/CreateAppPage`, headers: { @@ -126,4 +100,4 @@ class SpoAppPageAddCommand extends SpoCommand { } } -export default new SpoAppPageAddCommand(); \ No newline at end of file +export default new SpoAppPageAddCommand(); diff --git a/src/m365/spo/commands/apppage/apppage-set.spec.ts b/src/m365/spo/commands/apppage/apppage-set.spec.ts index 8b21f557485..ab380bcdbef 100644 --- a/src/m365/spo/commands/apppage/apppage-set.spec.ts +++ b/src/m365/spo/commands/apppage/apppage-set.spec.ts @@ -1,23 +1,23 @@ 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"; -import { pid } from "../../../../utils/pid.js"; -import { session } from "../../../../utils/session.js"; -import { sinonUtil } from "../../../../utils/sinonUtil.js"; -import { telemetry } from "../../../../telemetry.js"; -import commands from "../../commands.js"; -import command from './apppage-set.js'; -import { settingsNames } from '../../../../settingsNames.js'; +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'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { telemetry } from '../../../../telemetry.js'; +import commands from '../../commands.js'; +import command, { options } from './apppage-set.js'; describe(commands.APPPAGE_SET, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -26,6 +26,7 @@ describe(commands.APPPAGE_SET, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -45,8 +46,7 @@ describe(commands.APPPAGE_SET, () => { afterEach(() => { sinonUtil.restore([ - request.post, - cli.getSettingWithDefaultValue + request.post ]); }); @@ -55,159 +55,98 @@ describe(commands.APPPAGE_SET, () => { auth.connection.active = false; }); - it("has correct name", () => { + it('has correct name', () => { assert.strictEqual(command.name, commands.APPPAGE_SET); }); - it("has a description", () => { + it('has a description', () => { assert.notStrictEqual(command.description, null); }); - it("fails to update the single-part app page if request is rejected", async () => { - sinon.stub(request, "post").callsFake(async opts => { - if ( - (opts.url as string).indexOf(`_api/sitepages/Pages/UpdateFullPageApp`) > -1 && - opts.data.serverRelativeUrl.indexOf("failme") - ) { - throw "Failed to update the single-part app page"; + it('fails to update the single-part app page if request is rejected', async () => { + sinon.stub(request, 'post').callsFake(async opts => { + if ((opts.url as string).indexOf('_api/sitepages/Pages/UpdateFullPageApp') > -1 && + opts.data.serverRelativeUrl.indexOf('failme')) { + throw 'Failed to update the single-part app page'; } throw 'Invalid request'; }); await assert.rejects(command.action(logger, { - options: { - name: "failme", - webUrl: "https://contoso.sharepoint.com/", + options: commandOptionsSchema.parse({ + name: 'failme', + webUrl: 'https://contoso.sharepoint.com/', webPartData: JSON.stringify({}) - } - }), new CommandError(`Failed to update the single-part app page`)); + }) + }), new CommandError('Failed to update the single-part app page')); }); - it("Update the single-part app pag", async () => { - sinon.stub(request, "post").callsFake(async opts => { - if ( - (opts.url as string).indexOf(`_api/sitepages/Pages/UpdateFullPageApp`) > -1 - ) { + it('updates the single-part app page', async () => { + sinon.stub(request, 'post').callsFake(async opts => { + if ((opts.url as string).indexOf('_api/sitepages/Pages/UpdateFullPageApp') > -1) { return; } throw 'Invalid request'; }); await command.action(logger, { - options: { - pageName: "demo", - webUrl: "https://contoso.sharepoint.com/teams/sales", + options: commandOptionsSchema.parse({ + name: 'demo', + webUrl: 'https://contoso.sharepoint.com/teams/sales', webPartData: JSON.stringify({}) - } + }) }); }); - it("supports specifying name", () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf("--name") > -1) { - containsOption = true; - } + it('fails validation if name not specified', () => { + const actual = commandOptionsSchema.safeParse({ + webPartData: JSON.stringify({ abc: 'def' }), + webUrl: 'https://contoso.sharepoint.com' }); - assert(containsOption); + assert.strictEqual(actual.success, false); }); - it("supports specifying webUrl", () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf("--webUrl") > -1) { - containsOption = true; - } + it('fails validation if webPartData not specified', () => { + const actual = commandOptionsSchema.safeParse({ + name: 'Contoso.aspx', + webUrl: 'https://contoso.sharepoint.com' }); - assert(containsOption); + assert.strictEqual(actual.success, false); }); - it("supports specifying webPartData", () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf("--webPartData") > -1) { - containsOption = true; - } + it('fails validation if webUrl not specified', () => { + const actual = commandOptionsSchema.safeParse({ + webPartData: JSON.stringify({ abc: 'def' }), + name: 'page.aspx' }); - assert(containsOption); + assert.strictEqual(actual.success, false); }); - it("fails validation if name not specified", async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if webPartData is not a valid JSON string', () => { + const actual = commandOptionsSchema.safeParse({ + name: 'Contoso.aspx', + webUrl: 'https://contoso', + webPartData: 'abc' }); - - const actual = await command.validate({ - options: { - webPartData: JSON.stringify({ abc: "def" }), - webUrl: "https://contoso.sharepoint.com" - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it("fails validation if webPartData not specified", async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ + name: 'Contoso.aspx', + webPartData: '{}', + webUrl: 'https://contoso.sharepoint.com', + unknownOption: 'value' }); - - const actual = await command.validate({ - options: { - name: "Contoso.aspx", - webUrl: "https://contoso.sharepoint.com" - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it("fails validation if webUrl not specified", async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('validation passes on all required options', () => { + const actual = commandOptionsSchema.safeParse({ + name: 'Contoso.aspx', + webPartData: '{}', + webUrl: 'https://contoso.sharepoint.com' }); - - const actual = await command.validate({ - options: { - webPartData: JSON.stringify({ abc: "def" }), - name: "page.aspx" - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it("fails validation if webPartData is not a valid JSON string", async () => { - const actual = await command.validate({ - options: { - name: "Contoso.aspx", - webUrl: "https://contoso", - webPartData: "abc" - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it("validation passes on all required options", async () => { - const actual = await command.validate({ - options: { - name: "Contoso.aspx", - webPartData: "{}", - webUrl: "https://contoso.sharepoint.com" - } - }, commandInfo); - assert.strictEqual(actual, true); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/spo/commands/apppage/apppage-set.ts b/src/m365/spo/commands/apppage/apppage-set.ts index b5aee00b02f..5d945b56901 100644 --- a/src/m365/spo/commands/apppage/apppage-set.ts +++ b/src/m365/spo/commands/apppage/apppage-set.ts @@ -1,20 +1,34 @@ +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 { urlUtil } from '../../../../utils/urlUtil.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string().alias('u'), + name: z.string().alias('n'), + webPartData: z.string().refine(val => { + try { + JSON.parse(val); + return true; + } + catch { + return false; + } + }, { + error: 'Specified webPartData is not a valid JSON string.' + }).alias('d') +}); + +declare type Options = z.infer<typeof options>; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - name: string; - webPartData: string; -} - class SpoAppPageSetCommand extends SpoCommand { public get name(): string { return commands.APPPAGE_SET; @@ -24,40 +38,8 @@ class SpoAppPageSetCommand extends SpoCommand { return 'Updates the single-part app page'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl <webUrl>' - }, - { - option: '-n, --name <name>' - }, - { - option: '-d, --webPartData <webPartData>' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - try { - JSON.parse(args.options.webPartData); - } - catch (e) { - return `Specified webPartData is not a valid JSON string. Error: ${e}`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { @@ -82,4 +64,4 @@ class SpoAppPageSetCommand extends SpoCommand { } } } -export default new SpoAppPageSetCommand(); \ No newline at end of file +export default new SpoAppPageSetCommand(); From 311b1004dbf14512877110c35f030620592d89ad Mon Sep 17 00:00:00 2001 From: Martin Machacek <machacek@edhouse.cz> Date: Wed, 1 Jul 2026 10:14:01 +0200 Subject: [PATCH 3/3] Migrates 'spo applicationcustomizer' and 'spo apppage' commands to Zod. Closes #7320 --- .../applicationcustomizer/applicationcustomizer-add.ts | 4 ++-- .../applicationcustomizer/applicationcustomizer-get.ts | 2 +- .../applicationcustomizer/applicationcustomizer-list.ts | 2 +- .../applicationcustomizer/applicationcustomizer-remove.ts | 2 +- .../applicationcustomizer/applicationcustomizer-set.ts | 6 +++--- src/m365/spo/commands/apppage/apppage-add.ts | 4 ---- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts index 53e5354ce5f..c3192eff4b0 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts @@ -26,7 +26,7 @@ export const options = z.strictObject({ return false; } }, { - error: e => `An error has occurred while parsing clientSideComponentProperties: ${e.input}` + error: 'Specified clientSideComponentProperties is not a valid JSON string.' }).optional(), hostProperties: z.string().refine(val => { try { @@ -37,7 +37,7 @@ export const options = z.strictObject({ return false; } }, { - error: e => `An error has occurred while parsing hostProperties: ${e.input}` + error: 'Specified hostProperties is not a valid JSON string.' }).optional(), scope: z.enum(['Site', 'Web']).optional().alias('s') }); diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts index ed212f00aea..6dc9a6d5665 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-get.ts @@ -41,7 +41,7 @@ class SpoApplicationCustomizerGetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { return schema .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { - error: () => 'SharePoint Online site URL must be a string.', + error: e => validation.isValidSharePointUrl((e.input as Options).webUrl) as string, path: ['webUrl'] }) .refine(args => !args.id || validation.isValidGuid(args.id), { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts index bc5318a9335..c2e3c1d785b 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-list.ts @@ -37,7 +37,7 @@ class SpoApplicationCustomizerListCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { return schema.refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { - error: () => 'SharePoint Online site URL must be a string.', + error: e => validation.isValidSharePointUrl((e.input as Options).webUrl) as string, path: ['webUrl'] }); } diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts index efb3567eb15..e6b24227ce1 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-remove.ts @@ -42,7 +42,7 @@ class SpoApplicationCustomizerRemoveCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { return schema .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { - error: () => 'SharePoint Online site URL must be a string.', + error: e => validation.isValidSharePointUrl((e.input as Options).webUrl) as string, path: ['webUrl'] }) .refine(args => !args.id || validation.isValidGuid(args.id), { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts index 9b028795b3d..bd12bd251e7 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts @@ -27,7 +27,7 @@ export const options = z.strictObject({ return false; } }, { - error: e => `An error has occurred while parsing clientSideComponentProperties: ${e.input}` + error: 'Specified clientSideComponentProperties is not a valid JSON string.' }).optional().alias('p'), hostProperties: z.string().refine(val => { try { @@ -38,7 +38,7 @@ export const options = z.strictObject({ return false; } }, { - error: e => `An error has occurred while parsing hostProperties: ${e.input}` + error: 'Specified hostProperties is not a valid JSON string.' }).optional(), scope: z.enum(['Site', 'Web', 'All']).optional().alias('s') }); @@ -65,7 +65,7 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { return schema .refine(args => validation.isValidSharePointUrl(args.webUrl) === true, { - error: () => 'SharePoint Online site URL must be a string.', + error: e => validation.isValidSharePointUrl((e.input as Options).webUrl) as string, path: ['webUrl'] }) .refine(args => !args.id || validation.isValidGuid(args.id), { diff --git a/src/m365/spo/commands/apppage/apppage-add.ts b/src/m365/spo/commands/apppage/apppage-add.ts index ee977f07501..5b819344957 100644 --- a/src/m365/spo/commands/apppage/apppage-add.ts +++ b/src/m365/spo/commands/apppage/apppage-add.ts @@ -44,10 +44,6 @@ class SpoAppPageAddCommand extends SpoCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise<void> { - Object.assign(this.telemetryProperties, { - addToQuickLaunch: args.options.addToQuickLaunch - }); - const createPageRequestOptions: CliRequestOptions = { url: `${args.options.webUrl}/_api/sitepages/Pages/CreateAppPage`, headers: {