diff --git a/.gitignore b/.gitignore index 4146a43b816..add596b6b82 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,5 @@ docs/.env.production.local docs/npm-debug.log* docs/yarn-debug.log* -docs/yarn-error.log* \ No newline at end of file +docs/yarn-error.log* +.impeccable/ diff --git a/src/m365/purview/commands/retentionevent/retentionevent-add.spec.ts b/src/m365/purview/commands/retentionevent/retentionevent-add.spec.ts index 89ef741317c..bfb6261c041 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-add.spec.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-add.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 './retentionevent-add.js'; +import command, { options } from './retentionevent-add.js'; describe(commands.RETENTIONEVENT_ADD, () => { const validDisplayName = "Event display name"; @@ -86,6 +86,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -98,6 +99,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -138,19 +140,24 @@ describe(commands.RETENTIONEVENT_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if date is not a valid ISO date string', async () => { - const actual = await command.validate({ options: { displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: "Not a valid date", assetIds: validAssetIds } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if date is not a valid ISO date string', () => { + const actual = commandOptionsSchema.safeParse({ displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: "Not a valid date", assetIds: validAssetIds }); + assert.strictEqual(actual.success, false); }); - it('fails validation if assetId or keywords is not provided', async () => { - const actual = await command.validate({ options: { displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if assetId or keywords is not provided', () => { + const actual = commandOptionsSchema.safeParse({ displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate }); + assert.strictEqual(actual.success, false); }); - it('passes validation if a correct ISO date string is entered', async () => { - const actual = await command.validate({ options: { displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate, assetIds: validAssetIds } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if a correct ISO date string is entered', () => { + const actual = commandOptionsSchema.safeParse({ displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate, assetIds: validAssetIds }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ displayName: validDisplayName, eventTypeId: validTypeId, assetIds: validAssetIds, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('adds retention event with minimal required parameters and assetIds', async () => { @@ -162,7 +169,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { displayName: validDisplayName, eventTypeId: validTypeId, assetIds: validAssetIds } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: validDisplayName, eventTypeId: validTypeId, assetIds: validAssetIds }) }); assert(loggerLogSpy.calledWith(EventResponse)); }); @@ -175,7 +182,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { displayName: validDisplayName, eventTypeId: validTypeId, keywords: validKeyswords } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: validDisplayName, eventTypeId: validTypeId, keywords: validKeyswords }) }); assert(loggerLogSpy.calledWith(EventResponse)); }); @@ -188,7 +195,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate, assetIds: validAssetIds, keywords: validKeyswords } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, displayName: validDisplayName, eventTypeId: validTypeId, description: validDescription, triggerDateTime: validDate, assetIds: validAssetIds, keywords: validKeyswords }) }); assert(loggerLogSpy.calledWith(EventResponse)); }); @@ -209,7 +216,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true, displayName: validDisplayName, eventTypeName: validTypeName, assetIds: validAssetIds } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, displayName: validDisplayName, eventTypeName: validTypeName, assetIds: validAssetIds }) }); assert(loggerLogSpy.calledWith(EventResponse)); }); @@ -223,7 +230,7 @@ describe(commands.RETENTIONEVENT_ADD, () => { }); await assert.rejects(command.action(logger, { - options: { displayName: validDisplayName, eventTypeName: validTypeName, assetIds: validAssetIds } + options: commandOptionsSchema.parse({ displayName: validDisplayName, eventTypeName: validTypeName, assetIds: validAssetIds }) }), new CommandError(`The specified event type '${validTypeName}' does not exist.`)); }); @@ -236,9 +243,9 @@ describe(commands.RETENTIONEVENT_ADD, () => { sinon.stub(request, 'post').callsFake(async () => { throw error; }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ displayName: validDisplayName, eventTypeId: validTypeId, assetIds: validAssetIds - } + }) }), new CommandError(error.error.message)); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentionevent/retentionevent-add.ts b/src/m365/purview/commands/retentionevent/retentionevent-add.ts index 9d8e0a7ea05..e32072e3c70 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-add.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-add.ts @@ -1,25 +1,32 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import commands from '../../commands.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import { odata } from '../../../../utils/odata.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + displayName: z.string().alias('n'), + eventTypeId: z.string().optional().alias('i'), + eventTypeName: z.string().optional().alias('e'), + description: z.string().optional().alias('d'), + triggerDateTime: z.string().optional() + .refine(val => val === undefined || validation.isValidISODateTime(val), { + message: 'The triggerDateTime is not a valid ISO date string' + }), + assetIds: z.string().optional().alias('a'), + keywords: z.string().optional().alias('k') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - displayName: string; - eventTypeId?: string; - eventTypeName?: string; - description?: string; - triggerDateTime?: string; - assetIds?: string; - keywords?: string; -} - class PurviewRetentionEventAddCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENT_ADD; @@ -29,74 +36,26 @@ class PurviewRetentionEventAddCommand extends GraphCommand { return 'Create a retention event'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --displayName ' - }, - { - option: '-i, --eventTypeId [eventTypeId]' - }, - { - option: '-e, --eventTypeName [eventTypeName]' - }, - { - option: '-d, --description [description]' - }, - { - option: '--triggerDateTime [triggerDateTime]' - }, - { - option: '-a, --assetIds [assetIds]' - }, - { - option: '-k, --keywords [keywords]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.triggerDateTime && !validation.isValidISODateTime(args.options.triggerDateTime)) { - return 'The triggerDateTime is not a valid ISO date string'; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.eventTypeId, opts.eventTypeName].filter(x => x !== undefined).length === 1, { + error: `Specify either 'eventTypeId' or 'eventTypeName', but not both.`, + params: { + customCode: 'optionSet', + options: ['eventTypeId', 'eventTypeName'] } - - if (!args.options.assetIds && !args.options.keywords) { - return 'Specify assetIds and/or keywords, but at least one.'; + }) + .refine(opts => opts.assetIds !== undefined || opts.keywords !== undefined, { + error: 'Specify assetIds and/or keywords, but at least one.', + params: { + customCode: 'optionSet', + options: ['assetIds', 'keywords'] } - - return true; - } - ); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - description: typeof args.options.description !== 'undefined', - triggerDateTime: typeof args.options.triggerDateTime !== 'undefined', - eventTypeId: typeof args.options.eventTypeId !== 'undefined', - eventTypeName: typeof args.options.eventTypeName !== 'undefined', - assetIds: typeof args.options.assetIds !== 'undefined', - keywords: typeof args.options.keywords !== 'undefined' - }); - }); - } - - #initOptionSets(): void { - this.optionSets.push( - { options: ['eventTypeId', 'eventTypeName'] } - ); + }) as any; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/purview/commands/retentionevent/retentionevent-get.spec.ts b/src/m365/purview/commands/retentionevent/retentionevent-get.spec.ts index 0be5ccf6bcc..f867490fcbe 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-get.spec.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-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 './retentionevent-get.js'; +import command, { options } from './retentionevent-get.js'; describe(commands.RETENTIONEVENT_GET, () => { const retentionEventId = 'c37d695e-d581-4ae9-82a0-9364eba4291e'; @@ -63,6 +63,7 @@ describe(commands.RETENTIONEVENT_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -75,6 +76,7 @@ describe(commands.RETENTIONEVENT_GET, () => { accessToken: 'abc' }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -114,14 +116,19 @@ describe(commands.RETENTIONEVENT_GET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if a correct id is entered', async () => { - const actual = await command.validate({ options: { id: retentionEventId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if a correct id is entered', () => { + const actual = commandOptionsSchema.safeParse({ id: retentionEventId }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ id: retentionEventId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('retrieves retention event by specified id', async () => { @@ -133,7 +140,7 @@ describe(commands.RETENTIONEVENT_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: retentionEventId, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: retentionEventId, verbose: true }) }); assert(loggerLogSpy.calledWith(retentionEventGetResponse)); }); @@ -148,9 +155,9 @@ describe(commands.RETENTIONEVENT_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: retentionEventId - } + }) }), new CommandError(errorMessage)); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentionevent/retentionevent-get.ts b/src/m365/purview/commands/retentionevent/retentionevent-get.ts index 427ed22189c..638dfcb496e 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-get.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-get.ts @@ -1,18 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; -} - class PurviewRetentionEventGetCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENT_GET; @@ -22,31 +28,8 @@ class PurviewRetentionEventGetCommand extends GraphCommand { return 'Retrieve the specified retention event'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { - return `'${args.options.id}' is not a valid GUID.`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/purview/commands/retentionevent/retentionevent-remove.spec.ts b/src/m365/purview/commands/retentionevent/retentionevent-remove.spec.ts index 4ed3630fd8a..a4f176849ef 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-remove.spec.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-remove.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 './retentionevent-remove.js'; +import command, { options } from './retentionevent-remove.js'; describe(commands.RETENTIONEVENT_REMOVE, () => { const validId = 'c37d695e-d581-4ae9-82a0-9364eba4291e'; @@ -20,6 +20,7 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { let logger: Logger; let promptIssued: boolean = false; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -32,6 +33,7 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { expiresOn: new Date() }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -76,29 +78,33 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ - options: { - id: 'invalid' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + id: 'invalid' + }); + assert.strictEqual(actual.success, false); }); - it('validates for a correct input with id', async () => { - const actual = await command.validate({ - options: { - id: validId - } - }, commandInfo); - assert.strictEqual(actual, true); + it('validates for a correct input with id', () => { + const actual = commandOptionsSchema.safeParse({ + id: validId + }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ + id: validId, + unknownOption: 'value' + }); + assert.strictEqual(actual.success, false); }); it('prompts before removing the specified retention event when force option not passed', async () => { await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId - } + }) }); @@ -108,9 +114,9 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { it('aborts removing the specified retention event when force option not passed and prompt not confirmed', async () => { const deleteSpy = sinon.spy(request, 'delete'); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId - } + }) }); assert(deleteSpy.notCalled); }); @@ -128,9 +134,9 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { sinon.stub(cli, 'promptForConfirmation').resolves(true); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId - } + }) }); }); @@ -144,10 +150,10 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { }); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId, force: true - } + }) }); }); @@ -162,10 +168,10 @@ describe(commands.RETENTIONEVENT_REMOVE, () => { sinon.stub(request, 'delete').rejects(error); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId, force: true - } + }) }), new CommandError(error.error.message)); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentionevent/retentionevent-remove.ts b/src/m365/purview/commands/retentionevent/retentionevent-remove.ts index efa4b74b296..6b7b3dbb5b0 100644 --- a/src/m365/purview/commands/retentionevent/retentionevent-remove.ts +++ b/src/m365/purview/commands/retentionevent/retentionevent-remove.ts @@ -1,20 +1,26 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - force?: boolean; -} - class PurviewRetentionEventRemoveCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENT_REMOVE; @@ -24,43 +30,8 @@ class PurviewRetentionEventRemoveCommand extends GraphCommand { return 'Delete a retention event'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { - return `'${args.options.id}' is not a valid GUID.`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -76,7 +47,7 @@ class PurviewRetentionEventRemoveCommand extends GraphCommand { } } - private async removeRetentionEvent(options: GlobalOptions): Promise { + private async removeRetentionEvent(options: Options): Promise { try { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/security/triggers/retentionEvents/${options.id}`, diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.spec.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.spec.ts index b0a06e01e97..ff71bfc09e3 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.spec.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.spec.ts @@ -2,6 +2,8 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import { CommandError } from '../../../../Command.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; @@ -9,7 +11,7 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './retentioneventtype-add.js'; +import command, { options } from './retentioneventtype-add.js'; describe(commands.RETENTIONEVENTTYPE_ADD, () => { const displayName = 'Contract Expiry'; @@ -40,6 +42,8 @@ describe(commands.RETENTIONEVENTTYPE_ADD, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -51,6 +55,8 @@ describe(commands.RETENTIONEVENTTYPE_ADD, () => { accessToken: 'abc', expiresOn: new Date() }; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -99,16 +105,21 @@ describe(commands.RETENTIONEVENTTYPE_ADD, () => { return 'Invalid Request'; }); - await command.action(logger, { options: { displayName: displayName } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: displayName }) }); assert(loggerLogSpy.calledWith(requestResponse)); }); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ displayName: displayName, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); + }); + it('handles random API error', async () => { sinon.stub(request, 'post').callsFake(async () => { throw 'An error has occurred.'; }); - await assert.rejects(command.action(logger, { options: { displayName: displayName } }), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ displayName: displayName }) }), new CommandError('An error has occurred.')); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.ts index 90c5e5bf658..a4edac3dfeb 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-add.ts @@ -1,18 +1,22 @@ +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 GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + displayName: z.string().alias('n'), + description: z.string().optional().alias('d') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - displayName: string; - description?: string; -} - class PurviewRetentionEventTypeAddCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENTTYPE_ADD; @@ -22,30 +26,8 @@ class PurviewRetentionEventTypeAddCommand extends GraphCommand { return 'Create a retention event type'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - description: typeof args.options.description !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --displayName ' - }, - { - option: '-d, --description [description]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.spec.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.spec.ts index 6787ec0aca9..4438c6da6ee 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.spec.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-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 './retentioneventtype-get.js'; +import command, { options } from './retentioneventtype-get.js'; describe(commands.RETENTIONEVENTTYPE_GET, () => { const retentionEventTypeId = 'c37d695e-d581-4ae9-82a0-9364eba4291e'; @@ -39,6 +39,7 @@ describe(commands.RETENTIONEVENTTYPE_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -51,6 +52,7 @@ describe(commands.RETENTIONEVENTTYPE_GET, () => { expiresOn: new Date() }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -90,14 +92,19 @@ describe(commands.RETENTIONEVENTTYPE_GET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if a correct id is entered', async () => { - const actual = await command.validate({ options: { id: retentionEventTypeId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if a correct id is entered', () => { + const actual = commandOptionsSchema.safeParse({ id: retentionEventTypeId }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ id: retentionEventTypeId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('retrieves retention event type by specified id', async () => { @@ -109,7 +116,7 @@ describe(commands.RETENTIONEVENTTYPE_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: retentionEventTypeId, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: retentionEventTypeId, verbose: true }) }); assert(loggerLogSpy.calledWith(retentionEventTypeGetResponse)); }); @@ -124,9 +131,9 @@ describe(commands.RETENTIONEVENTTYPE_GET, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: retentionEventTypeId - } + }) }), new CommandError(errorMessage)); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.ts index 35a226422c5..f5a0f65d9a9 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-get.ts @@ -1,18 +1,24 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; -} - class PurviewRetentionEventTypeGetCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENTTYPE_GET; @@ -22,31 +28,8 @@ class PurviewRetentionEventTypeGetCommand extends GraphCommand { return 'Retrieve the specified retention event type'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { - return `'${args.options.id}' is not a valid GUID.`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.spec.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.spec.ts index cf58cf05546..7fd17598426 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.spec.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.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 './retentioneventtype-remove.js'; +import command, { options } from './retentioneventtype-remove.js'; describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { const validId = 'e554d69c-0992-4f9b-8a66-fca3c4d9c531'; @@ -20,6 +20,7 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { let logger: Logger; let promptIssued: boolean = false; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -32,6 +33,7 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { expiresOn: new Date() }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -76,18 +78,23 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('validates for a correct input with id', async () => { - const actual = await command.validate({ options: { id: validId } }, commandInfo); - assert.strictEqual(actual, true); + it('validates for a correct input with id', () => { + const actual = commandOptionsSchema.safeParse({ id: validId }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ id: validId, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('prompts before removing the specified retention event type when force option not passed', async () => { - await command.action(logger, { options: { id: validId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: validId }) }); assert(promptIssued); @@ -95,7 +102,7 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { it('aborts removing the specified retention event type when force option not passed and prompt not confirmed', async () => { const deleteSpy = sinon.spy(request, 'delete'); - await command.action(logger, { options: { id: validId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: validId }) }); assert(deleteSpy.notCalled); }); @@ -111,7 +118,7 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { id: validId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: validId }) }); }); it('correctly deletes retention event type by id when prompt confirmed', async () => { @@ -123,7 +130,7 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { throw 'Invalid Request'; }); - await command.action(logger, { options: { id: validId, force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: validId, force: true }) }); }); it('handles error when retention event type does not exist', async () => { @@ -142,10 +149,10 @@ describe(commands.RETENTIONEVENTTYPE_REMOVE, () => { }); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ id: validId, force: true - } + }) }), new CommandError(`There is no rule matching identity 'ca0e1f8d-4e42-4a81-be85-022502d70c4f'.`)); }); }); diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.ts index 0ce08ced983..aca6f190b79 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-remove.ts @@ -1,20 +1,26 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - force?: boolean; -} - class PurviewRetentionEventTypeRemoveCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENTTYPE_REMOVE; @@ -24,43 +30,8 @@ class PurviewRetentionEventTypeRemoveCommand extends GraphCommand { return 'Delete a retention event type'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { - return `'${args.options.id}' is not a valid GUID.`; - } - - return true; - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.spec.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.spec.ts index d69989b428c..b625ae0d29c 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.spec.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.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 './retentioneventtype-set.js'; +import command, { options } from './retentioneventtype-set.js'; describe(commands.RETENTIONEVENTTYPE_SET, () => { const validId = 'e554d69c-0992-4f9b-8a66-fca3c4d9c531'; @@ -20,6 +20,7 @@ describe(commands.RETENTIONEVENTTYPE_SET, () => { 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.RETENTIONEVENTTYPE_SET, () => { expiresOn: new Date() }; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -69,19 +71,24 @@ describe(commands.RETENTIONEVENTTYPE_SET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'invalid', description: description }); + assert.strictEqual(actual.success, false); }); - it('fails validation with valid id but no other option specified', async () => { - const actual = await command.validate({ options: { id: validId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with valid id but no other option specified', () => { + const actual = commandOptionsSchema.safeParse({ id: validId }); + assert.strictEqual(actual.success, false); }); - it('passes validation with valid id and a single option specified', async () => { - const actual = await command.validate({ options: { id: validId, description: description } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation with valid id and a single option specified', () => { + const actual = commandOptionsSchema.safeParse({ id: validId, description: description }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ id: validId, description: description, unknownOption: 'value' }); + assert.strictEqual(actual.success, false); }); it('correctly sets description of a specific retention event type by id', async () => { @@ -97,7 +104,7 @@ describe(commands.RETENTIONEVENTTYPE_SET, () => { throw 'Invalid Request'; }); - await command.action(logger, { options: { id: validId, description: description, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: validId, description: description, verbose: true }) }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, requestBody); }); @@ -117,9 +124,10 @@ describe(commands.RETENTIONEVENTTYPE_SET, () => { }); await assert.rejects(command.action(logger, { - options: { - id: validId - } + options: commandOptionsSchema.parse({ + id: validId, + description: description + }) }), new CommandError(`There is no rule matching identity 'ca0e1f8d-4e42-4a81-be85-022502d70c4f'.`)); }); }); \ No newline at end of file diff --git a/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.ts b/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.ts index 9fa293adaa9..8ebd620c221 100644 --- a/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.ts +++ b/src/m365/purview/commands/retentioneventtype/retentioneventtype-set.ts @@ -1,19 +1,25 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().refine(val => validation.isValidGuid(val), { + message: 'The value must be a valid GUID.' + }).alias('i'), + description: z.string().optional().alias('d') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; - description?: string; -} - class PurviewRetentionEventTypeSetCommand extends GraphCommand { public get name(): string { return commands.RETENTIONEVENTTYPE_SET; @@ -23,47 +29,19 @@ class PurviewRetentionEventTypeSetCommand extends GraphCommand { return 'Update a retention event type'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - description: typeof args.options.description !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '-d, --description [description]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { - return `'${args.options.id}' is not a valid GUID.`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => opts.description !== undefined, { + error: 'Specify at least one option to update.', + params: { + customCode: 'optionSet', + options: ['description'] } - - if (!args.options.description) { - return 'Specify at least one option to update.'; - } - - return true; - } - ); + }) as any; } public async commandAction(logger: Logger, args: CommandArgs): Promise {