diff --git a/src/m365/outlook/commands/mail/mail-send.spec.ts b/src/m365/outlook/commands/mail/mail-send.spec.ts index 21919a5f25c..e7a5cf36443 100644 --- a/src/m365/outlook/commands/mail/mail-send.spec.ts +++ b/src/m365/outlook/commands/mail/mail-send.spec.ts @@ -14,12 +14,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 './mail-send.js'; +import command, { options } from './mail-send.js'; describe(commands.MAIL_SEND, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -32,6 +33,7 @@ describe(commands.MAIL_SEND, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -311,14 +313,18 @@ describe(commands.MAIL_SEND, () => { new CommandError(`An error has occurred`)); }); - it('fails validation if bodyContentType is invalid', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines schema', () => { + assert.notStrictEqual(command.schema, undefined); }); - it('fails validation if importance is invalid', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', importance: 'Invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if bodyContentType is invalid', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Invalid' }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if importance is invalid', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', importance: 'Invalid' }); + assert.notStrictEqual(actual.success, true); }); it('fails validation if file doesn\'t exist', async () => { @@ -364,39 +370,48 @@ describe(commands.MAIL_SEND, () => { assert.notStrictEqual(actual, true); }); - it('passes validation when subject, to and bodyContents are specified', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo); + it('passes validation when valid attachments are specified', async () => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'lstatSync').returns({ isFile: () => true } as any); + sinon.stub(fs, 'readFileSync').returns('file content'); + + const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', attachment: 'C:/File.txt' } }, commandInfo); assert.strictEqual(actual, true); }); - it('passes validation when multiple to emails are specified', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com,mail2@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when subject, to and bodyContents are specified', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when multiple to emails separated with command and space are specified', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com, mail2@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when multiple to emails are specified', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com,mail2@domain.com', bodyContents: 'Lorem ipsum' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when bodyContentType is set to Text', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Text' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when multiple to emails separated with command and space are specified', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com, mail2@domain.com', bodyContents: 'Lorem ipsum' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when bodyContentType is set to HTML', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'HTML' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when bodyContentType is set to Text', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Text' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when saveToSentItems is set to false', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: false } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when bodyContentType is set to HTML', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'HTML' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when saveToSentItems is set to true', async () => { - const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: true } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when saveToSentItems is set to false', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: false }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation when saveToSentItems is set to true', () => { + const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: true }); + assert.strictEqual(actual.success, true); }); it('sends email using a specified group mailbox', async () => { diff --git a/src/m365/outlook/commands/mail/mail-send.ts b/src/m365/outlook/commands/mail/mail-send.ts index 92fc9d3a99c..6c4e13394d7 100644 --- a/src/m365/outlook/commands/mail/mail-send.ts +++ b/src/m365/outlook/commands/mail/mail-send.ts @@ -2,31 +2,35 @@ import fs from 'fs'; import path from 'path'; import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { accessToken } from '../../../../utils/accessToken.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { z } from 'zod'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + subject: z.string().alias('s'), + to: z.string().alias('t'), + cc: z.string().optional(), + bcc: z.string().optional(), + sender: z.string().optional(), + mailbox: z.string().optional().alias('m'), + bodyContents: z.string(), + bodyContentType: z.enum(['Text', 'HTML']).optional(), + importance: z.enum(['low', 'normal', 'high']).optional(), + attachment: z.union([z.string(), z.string().array()]).optional(), + saveToSentItems: z.boolean().optional() +}); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - subject: string; - to: string; - cc?: string; - bcc?: string; - sender?: string; - mailbox?: string; - bodyContents: string; - bodyContentType?: string; - importance?: string; - attachment?: string | string[]; - saveToSentItems?: boolean; -} - class OutlookMailSendCommand extends GraphCommand { public get name(): string { return commands.MAIL_SEND; @@ -36,88 +40,19 @@ class OutlookMailSendCommand extends GraphCommand { return 'Sends an email'; } + public get schema(): z.ZodType | undefined { + return options; + } + constructor() { super(); - this.#initTelemetry(); - this.#initOptions(); - this.#initTypes(); this.#initValidators(); } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - cc: typeof args.options.cc !== 'undefined', - bcc: typeof args.options.bcc !== 'undefined', - bodyContentType: args.options.bodyContentType, - saveToSentItems: args.options.saveToSentItems, - importance: args.options.importance, - mailbox: typeof args.options.mailbox !== 'undefined', - sender: typeof args.options.sender !== 'undefined', - attachment: typeof args.options.attachment !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-s, --subject ' - }, - { - option: '-t, --to ' - }, - { - option: '--cc [cc]' - }, - { - option: '--bcc [bcc]' - }, - { - option: '--sender [sender]' - }, - { - option: '-m, --mailbox [mailbox]' - }, - { - option: '--bodyContents ' - }, - { - option: '--bodyContentType [bodyContentType]', - autocomplete: ['Text', 'HTML'] - }, - { - option: '--importance [importance]', - autocomplete: ['low', 'normal', 'high'] - }, - { - option: '--attachment [attachment]' - }, - { - option: '--saveToSentItems [saveToSentItems]', - autocomplete: ['true', 'false'] - } - ); - } - - #initTypes(): void { - this.types.boolean.push('saveToSentItems'); - } - #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (args.options.bodyContentType && - args.options.bodyContentType !== 'Text' && - args.options.bodyContentType !== 'HTML') { - return `${args.options.bodyContentType} is not a valid value for the bodyContentType option. Allowed values are Text|HTML`; - } - - if (args.options.importance && ['low', 'normal', 'high'].indexOf(args.options.importance) === -1) { - return `'${args.options.importance}' is not a valid value for the importance option. Allowed values are low|normal|high`; - } - if (args.options.attachment) { const attachments: string[] = typeof args.options.attachment === 'string' ? [args.options.attachment] : args.options.attachment; diff --git a/src/m365/outlook/commands/message/message-get.spec.ts b/src/m365/outlook/commands/message/message-get.spec.ts index cd9e781a3ed..c66f3207693 100644 --- a/src/m365/outlook/commands/message/message-get.spec.ts +++ b/src/m365/outlook/commands/message/message-get.spec.ts @@ -12,7 +12,7 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './message-get.js'; +import command, { options } from './message-get.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.MESSAGE_GET, () => { @@ -77,6 +77,7 @@ describe(commands.MESSAGE_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -89,6 +90,7 @@ describe(commands.MESSAGE_GET, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -260,6 +262,34 @@ describe(commands.MESSAGE_GET, () => { assert.equal(actual, true); }); + it('defines schema', () => { + assert.notStrictEqual(command.schema, undefined); + }); + + it('defines refined schema', () => { + assert.notStrictEqual(command.getRefinedSchema(command.schema as any), undefined); + }); + + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userId: 'invalid-guid' }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if userName is not a valid UPN', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userName: 'invalid-upn' }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation if both userId and userName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userId: userId, userName: userName }); + assert.strictEqual(actual.success, false); + }); + + it('passes validation if only userId is specified', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userId: userId }); + assert.strictEqual(actual.success, true); + }); + it('throws an error when the upn or userName is not defined when signed in using app only authentication', async () => { sinonUtil.restore([accessToken.isAppOnlyAccessToken]); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); diff --git a/src/m365/outlook/commands/message/message-get.ts b/src/m365/outlook/commands/message/message-get.ts index f4d3ff066db..0e7c4a0254f 100644 --- a/src/m365/outlook/commands/message/message-get.ts +++ b/src/m365/outlook/commands/message/message-get.ts @@ -1,21 +1,32 @@ import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { accessToken } from '../../../../utils/accessToken.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { z } from 'zod'; +import { validation } from '../../../../utils/validation.js'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().alias('i'), + userId: z.string() + .refine(userId => validation.isValidGuid(userId), { + error: e => `'${e.input}' is not a valid GUID.` + }).optional(), + userName: z.string() + .refine(userName => validation.isValidUserPrincipalName(userName), { + error: e => `'${e.input}' is not a valid UPN.` + }).optional() +}); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - userId?: string; - userName?: string; -} - class OutlookMessageGetCommand extends GraphCommand { public get name(): string { return commands.MESSAGE_GET; @@ -25,34 +36,19 @@ class OutlookMessageGetCommand extends GraphCommand { return 'Retrieves specified message'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); + public get schema(): z.ZodType | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined' + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => !(options.userId && options.userName), { + error: 'Specify either userId or userName, but not both', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] + } }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - } - ); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/outlook/commands/message/message-list.spec.ts b/src/m365/outlook/commands/message/message-list.spec.ts index 17af4ed9993..f21585c506b 100644 --- a/src/m365/outlook/commands/message/message-list.spec.ts +++ b/src/m365/outlook/commands/message/message-list.spec.ts @@ -11,7 +11,7 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './message-list.js'; +import command, { options } from './message-list.js'; import { settingsNames } from '../../../../settingsNames.js'; import { accessToken } from '../../../../utils/accessToken.js'; import { formatting } from '../../../../utils/formatting.js'; @@ -21,6 +21,7 @@ describe(commands.MESSAGE_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const folderId = 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA='; const folderName = 'Inbox'; const startTime = '2023-12-16'; @@ -205,6 +206,7 @@ describe(commands.MESSAGE_LIST, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -479,73 +481,73 @@ describe(commands.MESSAGE_LIST, () => { new CommandError('An error has occurred')); }); - it('passes validation if both start and endTime are valid ISO datetimes', async () => { - const actual = await command.validate({ options: { startTime: startTime, endTime: endTime } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if both start and endTime are valid ISO datetimes', () => { + const actual = commandOptionsSchema.safeParse({ startTime: startTime, endTime: endTime }); + assert.strictEqual(actual.success, true); }); - it('fails validation if startTime is not a valid ISO datetime', async () => { - const actual = await command.validate({ options: { startTime: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines schema', () => { + assert.notStrictEqual(command.schema, undefined); }); - it('fails validation if endTime is not a valid ISO datetime', async () => { - const actual = await command.validate({ options: { endTime: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines refined schema', () => { + assert.notStrictEqual(command.getRefinedSchema(command.schema as any), undefined); }); - it('fails validation if endTime is in the future', async () => { + it('fails validation if startTime is not a valid ISO datetime', () => { + const actual = commandOptionsSchema.safeParse({ startTime: 'invalid' }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if endTime is not a valid ISO datetime', () => { + const actual = commandOptionsSchema.safeParse({ endTime: 'invalid' }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if endTime is in the future', () => { const endTime = new Date(); endTime.setHours(endTime.getHours() + 1); - const actual = await command.validate({ options: { endTime: endTime.toISOString() } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ endTime: endTime.toISOString() }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if startTime is in the future', async () => { + it('fails validation if startTime is in the future', () => { const startTime = new Date(); startTime.setHours(startTime.getHours() + 1); - const actual = await command.validate({ options: { startTime: startTime.toISOString() } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ startTime: startTime.toISOString() }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if endTime is before startTime', async () => { + it('fails validation if endTime is before startTime', () => { const startTime = new Date(); const endTime = new Date(startTime); endTime.setTime(endTime.getTime() - 1); - const actual = await command.validate({ options: { startTime: startTime.toISOString(), endTime: endTime.toISOString() } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ startTime: startTime.toISOString(), endTime: endTime.toISOString() }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if both folderId and folderName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { folderId: folderId, folderName: folderName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both folderId and folderName are specified', () => { + const actual = commandOptionsSchema.safeParse({ folderId: folderId, folderName: folderName }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { userId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ userId: 'invalid' }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if userName is not a valid user principal name', async () => { - const actual = await command.validate({ options: { userName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userName is not a valid user principal name', () => { + const actual = commandOptionsSchema.safeParse({ userName: 'invalid' }); + assert.notStrictEqual(actual.success, true); }); - it('passes validation if userId is a valid GUID', async () => { - const actual = await command.validate({ options: { userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if userId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ userId: userId }); + assert.strictEqual(actual.success, true); }); - it('passes validation if userName is a valid user principal name', async () => { - const actual = await command.validate({ options: { userName: userName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if userName is a valid user principal name', () => { + const actual = commandOptionsSchema.safeParse({ userName: userName }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/outlook/commands/message/message-list.ts b/src/m365/outlook/commands/message/message-list.ts index 51017e1ae8f..74429c5021d 100644 --- a/src/m365/outlook/commands/message/message-list.ts +++ b/src/m365/outlook/commands/message/message-list.ts @@ -1,6 +1,6 @@ import { Message } from '@microsoft/microsoft-graph-types'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; @@ -11,20 +11,44 @@ import { cli } from '../../../../cli/cli.js'; import { validation } from '../../../../utils/validation.js'; import { accessToken } from '../../../../utils/accessToken.js'; import auth from '../../../../Auth.js'; +import { z } from 'zod'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + folderId: z.string().optional(), + folderName: z.string().optional(), + startTime: z.string() + .refine(startTime => validation.isValidISODateTime(startTime), { + error: e => `'${e.input}' is not a valid ISO date string for option startTime.` + }) + .refine(startTime => new Date(startTime) <= new Date(), { + error: 'startTime value cannot be in the future.' + }) + .optional(), + endTime: z.string() + .refine(endTime => validation.isValidISODateTime(endTime), { + error: e => `'${e.input}' is not a valid ISO date string for option endTime.` + }) + .refine(endTime => new Date(endTime) <= new Date(), { + error: 'endTime value cannot be in the future.' + }) + .optional(), + userId: z.string() + .refine(userId => validation.isValidGuid(userId), { + error: e => `${e.input} is not a valid GUID for option userId.` + }).optional(), + userName: z.string() + .refine(userName => validation.isValidUserPrincipalName(userName), { + error: e => `${e.input} is not a valid UPN for option userName.` + }).optional() +}); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - folderId?: string; - folderName?: string; - startTime?: string; - endTime?: string; - userId?: string; - userName?: string; -} - class OutlookMessageListCommand extends GraphCommand { public get name(): string { return commands.MESSAGE_LIST; @@ -34,106 +58,29 @@ class OutlookMessageListCommand extends GraphCommand { return 'Gets all mail messages from the specified folder'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initTypes(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - folderId: typeof args.options.folderId !== 'undefined', - folderName: typeof args.options.folderName !== 'undefined', - startTime: typeof args.options.startTime !== 'undefined', - endTime: typeof args.options.endTime !== 'undefined', - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '--folderName [folderName]', - autocomplete: Outlook.wellKnownFolderNames - }, - { - option: '--folderId [folderId]' - }, - { - option: '--startTime [startTime]' - }, - { - option: '--endTime [endTime]' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.startTime) { - if (!validation.isValidISODateTime(args.options.startTime)) { - return `'${args.options.startTime}' is not a valid ISO date string for option startTime.`; - } - if (new Date(args.options.startTime) > new Date()) { - return 'startTime value cannot be in the future.'; - } - } - - if (args.options.endTime) { - if (!validation.isValidISODateTime(args.options.endTime)) { - return `'${args.options.endTime}' is not a valid ISO date string for option endTime.`; - } - if (new Date(args.options.endTime) > new Date()) { - return 'endTime value cannot be in the future.'; - } - } - - if (args.options.startTime && args.options.endTime && new Date(args.options.startTime) >= new Date(args.options.endTime)) { - return 'startTime must be before endTime.'; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => !(options.folderId && options.folderName), { + error: 'Specify either folderId or folderName, but not both', + params: { + customCode: 'optionSet', + options: ['folderId', 'folderName'] } - - if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `${args.options.userId} is not a valid GUID for option userId.`; - } - - if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { - return `${args.options.userName} is not a valid UPN for option userName.`; + }) + .refine(options => !(options.userId && options.userName), { + error: 'Specify either userId or userName, but not both', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] } - - return true; - } - ); - } - - #initTypes(): void { - this.types.string.push('folderName', 'folderId', 'startTime', 'endTime', 'userId', 'userName'); - } - - #initOptionSets(): void { - this.optionSets.push( - { - options: ['folderId', 'folderName'], - runsWhen: (args) => args.options.folderId || args.options.folderName - }, - { - options: ['userId', 'userName'], - runsWhen: (args) => args.options.userId || args.options.userName - } - ); + }) + .refine(options => !(options.startTime && options.endTime && new Date(options.startTime) >= new Date(options.endTime)), { + error: 'startTime must be before endTime.' + }); } public defaultProperties(): string[] | undefined { diff --git a/src/m365/outlook/commands/message/message-move.spec.ts b/src/m365/outlook/commands/message/message-move.spec.ts index 38cbbcfb6e8..ec534e67d18 100644 --- a/src/m365/outlook/commands/message/message-move.spec.ts +++ b/src/m365/outlook/commands/message/message-move.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 './message-move.js'; +import command, { options } from './message-move.js'; import { settingsNames } from '../../../../settingsNames.js'; import { accessToken } from '../../../../utils/accessToken.js'; @@ -19,6 +19,7 @@ describe(commands.MESSAGE_MOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -28,6 +29,7 @@ describe(commands.MESSAGE_MOVE, () => { sinon.stub(accessToken, 'assertAccessTokenType').returns(); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -373,80 +375,56 @@ describe(commands.MESSAGE_MOVE, () => { new CommandError('An error has occurred')); }); - it('passes validation if all required options specified', async () => { - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if all required options specified', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if neither sourceFolderId nor sourceFolderName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', targetFolderId: 'archive' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines schema', () => { + assert.notStrictEqual(command.schema, undefined); }); - it('fails validation if both sourceFolderId and sourceFolderName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', sourceFolderName: 'Inbox', targetFolderId: 'archive' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines refined schema', () => { + assert.notStrictEqual(command.getRefinedSchema(command.schema as any), undefined); }); - it('fails validation if neither targetFolderId nor targetFolderName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither sourceFolderId nor sourceFolderName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', targetFolderId: 'archive' }); + assert.notStrictEqual(actual.success, true); }); - it('fails validation if both targetFolderId and targetFolderName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } + it('fails validation if both sourceFolderId and sourceFolderName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', sourceFolderName: 'Inbox', targetFolderId: 'archive' }); + assert.notStrictEqual(actual.success, true); + }); - return defaultValue; - }); + it('fails validation if neither targetFolderId nor targetFolderName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox' }); + assert.notStrictEqual(actual.success, true); + }); - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive', targetFolderName: 'archive' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both targetFolderId and targetFolderName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive', targetFolderName: 'archive' }); + assert.notStrictEqual(actual.success, true); }); - it('passes validation if sourceFolderId specified using a well-know-name', async () => { - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if sourceFolderId specified using a well-know-name', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if targetFolderId specified using a well-know-name', async () => { - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if targetFolderId specified using a well-know-name', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'inbox', targetFolderId: 'archive' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if sourceFolderId specified using an ID', async () => { - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=', targetFolderId: 'archive' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if sourceFolderId specified using an ID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=', targetFolderId: 'archive' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if targetFolderId specified using an ID', async () => { - const actual = await command.validate({ options: { id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=', targetFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if targetFolderId specified using an ID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAALvuv07AAA', sourceFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=', targetFolderId: 'AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OAAuAAAAAAAiQ8W967B7TKBjgx9rVEURAQAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAA=' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/outlook/commands/message/message-move.ts b/src/m365/outlook/commands/message/message-move.ts index d6cf2487c23..5efdd464ca9 100644 --- a/src/m365/outlook/commands/message/message-move.ts +++ b/src/m365/outlook/commands/message/message-move.ts @@ -1,24 +1,28 @@ import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import commands from '../../commands.js'; import { Outlook } from '../../Outlook.js'; import { cli } from '../../../../cli/cli.js'; import DelegatedGraphCommand from '../../../base/GraphDelegatedCommand.js'; +import { z } from 'zod'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string(), + sourceFolderId: z.string().optional(), + sourceFolderName: z.string().optional(), + targetFolderId: z.string().optional(), + targetFolderName: z.string().optional() +}); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - sourceFolderId?: string; - sourceFolderName?: string; - targetFolderId?: string; - targetFolderName?: string; -} - class OutlookMessageMoveCommand extends DelegatedGraphCommand { public get name(): string { return commands.MESSAGE_MOVE; @@ -28,54 +32,40 @@ class OutlookMessageMoveCommand extends DelegatedGraphCommand { return 'Moves message to the specified folder'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initOptionSets(); + public get schema(): z.ZodType | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - sourceFolderId: typeof args.options.sourceFolderId !== 'undefined', - sourceFolderName: typeof args.options.sourceFolderName !== 'undefined', - targetFolderId: typeof args.options.targetFolderId !== 'undefined', - targetFolderName: typeof args.options.targetFolderName !== 'undefined' + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => options.sourceFolderId || options.sourceFolderName, { + error: 'Specify either sourceFolderId or sourceFolderName', + params: { + customCode: 'optionSet', + options: ['sourceFolderId', 'sourceFolderName'] + } + }) + .refine(options => !(options.sourceFolderId && options.sourceFolderName), { + error: 'Specify either sourceFolderId or sourceFolderName, but not both', + params: { + customCode: 'optionSet', + options: ['sourceFolderId', 'sourceFolderName'] + } + }) + .refine(options => options.targetFolderId || options.targetFolderName, { + error: 'Specify either targetFolderId or targetFolderName', + params: { + customCode: 'optionSet', + options: ['targetFolderId', 'targetFolderName'] + } + }) + .refine(options => !(options.targetFolderId && options.targetFolderName), { + error: 'Specify either targetFolderId or targetFolderName, but not both', + params: { + customCode: 'optionSet', + options: ['targetFolderId', 'targetFolderName'] + } }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '--id ' - }, - { - option: '--sourceFolderName [sourceFolderName]', - autocomplete: Outlook.wellKnownFolderNames - }, - { - option: '--sourceFolderId [sourceFolderId]', - autocomplete: Outlook.wellKnownFolderNames - }, - { - option: '--targetFolderName [targetFolderName]', - autocomplete: Outlook.wellKnownFolderNames - }, - { - option: '--targetFolderId [targetFolderId]', - autocomplete: Outlook.wellKnownFolderNames - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['sourceFolderId', 'sourceFolderName'] }, - { options: ['targetFolderId', 'targetFolderName'] } - ); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/outlook/commands/message/message-remove.spec.ts b/src/m365/outlook/commands/message/message-remove.spec.ts index 5a4e8655393..469d70ddb7d 100644 --- a/src/m365/outlook/commands/message/message-remove.spec.ts +++ b/src/m365/outlook/commands/message/message-remove.spec.ts @@ -11,7 +11,7 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { cli } from '../../../../cli/cli.js'; import { accessToken } from '../../../../utils/accessToken.js'; -import command from './message-remove.js'; +import command, { options } from './message-remove.js'; import { formatting } from '../../../../utils/formatting.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; @@ -24,6 +24,7 @@ describe(commands.MESSAGE_REMOVE, () => { let logger: Logger; let promptIssued: boolean; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -36,6 +37,7 @@ describe(commands.MESSAGE_REMOVE, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -81,24 +83,28 @@ describe(commands.MESSAGE_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation when userId is a valid GUID', async () => { - const actual = await command.validate({ options: { id: messageId, userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when userId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userId: userId }); + assert.strictEqual(actual.success, true); }); - it('passes validation when userName is a valid UPN', async () => { - const actual = await command.validate({ options: { id: messageId, userName: userPrincipalName } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when userName is a valid UPN', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userName: userPrincipalName }); + assert.strictEqual(actual.success, true); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: messageId, userId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('defines schema', () => { + assert.notStrictEqual(command.schema, undefined); }); - it('fails validation if userName is not a valid UPN', async () => { - const actual = await command.validate({ options: { id: messageId, userName: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userId: 'invalid' }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userName is not a valid UPN', () => { + const actual = commandOptionsSchema.safeParse({ id: messageId, userName: 'invalid' }); + assert.notStrictEqual(actual.success, true); }); it('removes specific message using delegated permissions without prompting for confirmation', async () => { diff --git a/src/m365/outlook/commands/message/message-remove.ts b/src/m365/outlook/commands/message/message-remove.ts index 71c47ef2ac6..e5b1224f646 100644 --- a/src/m365/outlook/commands/message/message-remove.ts +++ b/src/m365/outlook/commands/message/message-remove.ts @@ -1,6 +1,6 @@ import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { accessToken } from '../../../../utils/accessToken.js'; import GraphCommand from '../../../base/GraphCommand.js'; @@ -8,18 +8,28 @@ import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; import { validation } from '../../../../utils/validation.js'; import { formatting } from '../../../../utils/formatting.js'; +import { z } from 'zod'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().alias('i'), + userId: z.string() + .refine(userId => validation.isValidGuid(userId), { + error: e => `Value '${e.input}' is not a valid GUID for option 'userId'.` + }).optional(), + userName: z.string() + .refine(userName => validation.isValidUserPrincipalName(userName), { + error: e => `Value '${e.input}' is not a valid user principal name for option 'userName'.` + }).optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - userId?: string; - userName?: string; - force?: boolean -} - class OutlookMessageRemoveCommand extends GraphCommand { public get name(): string { return commands.MESSAGE_REMOVE; @@ -29,61 +39,8 @@ class OutlookMessageRemoveCommand extends GraphCommand { return 'Permanently removes a specific message from a mailbox'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initTypes(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined', - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `Value '${args.options.userId}' is not a valid GUID for option 'userId'.`; - } - - if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { - return `Value '${args.options.userName}' is not a valid user principal name for option 'userName'.`; - } - - return true; - } - ); - } - - #initTypes(): void { - this.types.string.push('id', 'userId', 'userName'); - this.types.boolean.push('force'); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise {