diff --git a/src/m365/file/commands/convert/convert-pdf.spec.ts b/src/m365/file/commands/convert/convert-pdf.spec.ts index 400eb5841a4..6bd2092289f 100644 --- a/src/m365/file/commands/convert/convert-pdf.spec.ts +++ b/src/m365/file/commands/convert/convert-pdf.spec.ts @@ -12,6 +12,7 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { z } from 'zod'; import commands from '../../commands.js'; import command from './convert-pdf.js'; @@ -19,6 +20,7 @@ describe(commands.CONVERT_PDF, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; let unlinkSyncStub: sinon.SinonStub; const mockPdfFile = 'pdf'; let pdfConvertResponseStream: PassThrough; @@ -32,6 +34,7 @@ describe(commands.CONVERT_PDF, () => { auth.connection.active = true; unlinkSyncStub = sinon.stub(fs, 'unlinkSync').returns(); commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -1749,25 +1752,25 @@ describe(commands.CONVERT_PDF, () => { it(`fails validation if the specified local source file doesn't exist`, async () => { sinon.stub(fs, 'existsSync').callsFake(() => false); - const actual = await command.validate({ options: { sourceFile: 'file.docx', targetFile: 'file.pdf' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ sourceFile: 'file.docx', targetFile: 'file.pdf' }); + assert.strictEqual(actual.success, false); }); it(`fails validation if another file exists at the path specified in the target file`, async () => { sinon.stub(fs, 'existsSync').callsFake(() => true); - const actual = await command.validate({ options: { sourceFile: 'file.docx', targetFile: 'file.pdf' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ sourceFile: 'file.docx', targetFile: 'file.pdf' }); + assert.strictEqual(actual.success, false); }); it(`passes validation if the source file is a URL`, async () => { sinon.stub(fs, 'existsSync').callsFake(() => false); - const actual = await command.validate({ options: { sourceFile: 'https://contoso.sharepoint.com/Shared Documents/file.docx', targetFile: 'file.pdf' } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ sourceFile: 'https://contoso.sharepoint.com/Shared Documents/file.docx', targetFile: 'file.pdf' }); + assert.strictEqual(actual.success, true); }); it(`passes validation if the target file is a URL`, async () => { sinon.stub(fs, 'existsSync').callsFake(() => true); - const actual = await command.validate({ options: { sourceFile: 'file.docx', targetFile: 'https://contoso.sharepoint.com/Shared Documents/file.pdf' } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ sourceFile: 'file.docx', targetFile: 'https://contoso.sharepoint.com/Shared Documents/file.pdf' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/file/commands/convert/convert-pdf.ts b/src/m365/file/commands/convert/convert-pdf.ts index 3827eb1c603..36e254b7a20 100644 --- a/src/m365/file/commands/convert/convert-pdf.ts +++ b/src/m365/file/commands/convert/convert-pdf.ts @@ -3,24 +3,27 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { v4 } from 'uuid'; +import { z } from 'zod'; import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; -import { CommandError } from '../../../../Command.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { CommandError, 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'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + sourceFile: z.string().alias('s'), + targetFile: z.string().alias('t') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - sourceFile: string; - targetFile: string; -} - class FileConvertPdfCommand extends GraphCommand { // Graph's drive item URL of the source file private sourceFileGraphUrl?: string; @@ -33,42 +36,18 @@ class FileConvertPdfCommand extends GraphCommand { return 'Converts the specified file to PDF using Microsoft Graph'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); + public get schema(): z.ZodType | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-s, --sourceFile ' - }, - { - option: '-t, --targetFile ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!args.options.sourceFile.toLowerCase().startsWith('https://') && - !fs.existsSync(args.options.sourceFile)) { - // assume local path - return `Specified source file ${args.options.sourceFile} doesn't exist`; - } - - if (!args.options.targetFile.toLowerCase().startsWith('https://') && - fs.existsSync(args.options.targetFile)) { - // assume local path - return `Another file found at ${args.options.targetFile}`; - } - - return true; - } - ); + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => options.sourceFile.toLowerCase().startsWith('https://') || fs.existsSync(options.sourceFile), { + error: e => `Specified source file ${(e.input as Options).sourceFile} doesn't exist` + }) + .refine(options => options.targetFile.toLowerCase().startsWith('https://') || !fs.existsSync(options.targetFile), { + error: e => `Another file found at ${(e.input as Options).targetFile}` + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -467,4 +446,4 @@ class FileConvertPdfCommand extends GraphCommand { } } -export default new FileConvertPdfCommand(); \ No newline at end of file +export default new FileConvertPdfCommand(); diff --git a/src/m365/file/commands/file-add.spec.ts b/src/m365/file/commands/file-add.spec.ts index 5b5ecc7d76a..606e60feaeb 100644 --- a/src/m365/file/commands/file-add.spec.ts +++ b/src/m365/file/commands/file-add.spec.ts @@ -11,6 +11,7 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; +import { z } from 'zod'; import commands from '../commands.js'; import command from './file-add.js'; @@ -18,6 +19,7 @@ describe(commands.ADD, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -26,6 +28,7 @@ describe(commands.ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -943,25 +946,29 @@ describe(commands.ADD, () => { it(`fails validation if the specified local source file doesn't exist`, async () => { sinon.stub(fs, 'existsSync').returns(false); - const actual = await command.validate({ options: { filePath: 'file.pdf', folderUrl: 'https://contoso.sharepoint.com/Shared Documents' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ filePath: 'file.pdf', folderUrl: 'https://contoso.sharepoint.com/Shared Documents' }); + assert.strictEqual(actual.success, false); + }); + + it(`fails validation if the specified folderUrl is invalid`, async () => { + sinon.stub(fs, 'existsSync').returns(true); + const actual = commandOptionsSchema.safeParse({ filePath: 'file.pdf', folderUrl: '/' }); + assert.strictEqual(actual.success, false); }); it(`fails validation if the specified siteUrl is invalid`, async () => { sinon.stub(fs, 'existsSync').returns(true); - const actual = await command.validate({ - options: { - filePath: 'file.pdf', - folderUrl: 'https://contoso.sharepoint.com/Shared Documents', - siteUrl: '/' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + filePath: 'file.pdf', + folderUrl: 'https://contoso.sharepoint.com/Shared Documents', + siteUrl: '/' + }); + assert.strictEqual(actual.success, false); }); it(`passes validation if the target file is a URL`, async () => { sinon.stub(fs, 'existsSync').returns(true); - const actual = await command.validate({ options: { filePath: 'file.pdf', folderUrl: 'https://contoso.sharepoint.com/Shared Documents' } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ filePath: 'file.pdf', folderUrl: 'https://contoso.sharepoint.com/Shared Documents' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/file/commands/file-add.ts b/src/m365/file/commands/file-add.ts index afd98fddd80..e97c4a1b401 100644 --- a/src/m365/file/commands/file-add.ts +++ b/src/m365/file/commands/file-add.ts @@ -1,22 +1,30 @@ import fs from 'fs'; import path from 'path'; +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { validation } from '../../../utils/validation.js'; import GraphCommand from '../../base/GraphCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + folderUrl: z.string() + .refine(url => validation.isValidSharePointUrl(url) === true, { + error: e => `'${e.input}' is not a valid SharePoint Online site URL.` + }) + .alias('u'), + filePath: z.string().alias('p'), + siteUrl: z.string().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - folderUrl: string; - filePath: string; - siteUrl?: string; -} - class FileAddCommand extends GraphCommand { public get name(): string { return commands.ADD; @@ -26,47 +34,24 @@ class FileAddCommand extends GraphCommand { return 'Uploads file to the specified site'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - siteUrl: typeof args.options.siteUrl !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { option: '-u, --folderUrl ' }, - { option: '-p, --filePath ' }, - { option: '--siteUrl [siteUrl]' } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!fs.existsSync(args.options.filePath)) { - return `Specified source file ${args.options.sourceFile} doesn't exist`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => fs.existsSync(options.filePath), { + error: e => `Specified source file ${(e.input as Options).filePath} doesn't exist` + }) + .refine(options => { + if (options.siteUrl) { + return validation.isValidSharePointUrl(options.siteUrl) === true; } - if (args.options.siteUrl) { - const isValidSiteUrl = validation.isValidSharePointUrl(args.options.siteUrl); - if (isValidSiteUrl !== true) { - return isValidSiteUrl; - } - } - - return validation.isValidSharePointUrl(args.options.folderUrl); - } - ); + return true; + }, { + error: e => `'${(e.input as Options).siteUrl}' is not a valid SharePoint Online site URL.` + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -279,4 +264,4 @@ class FileAddCommand extends GraphCommand { } } -export default new FileAddCommand(); \ No newline at end of file +export default new FileAddCommand(); diff --git a/src/m365/file/commands/file-copy.spec.ts b/src/m365/file/commands/file-copy.spec.ts index a967d8cccfc..ec41c15d395 100644 --- a/src/m365/file/commands/file-copy.spec.ts +++ b/src/m365/file/commands/file-copy.spec.ts @@ -11,6 +11,7 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; +import { z } from 'zod'; import commands from '../commands.js'; import command from './file-copy.js'; @@ -18,6 +19,7 @@ describe(commands.COPY, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; const defaultPostStub = (): sinon.SinonStub => { return sinon.stub(request, 'post').callsFake(async (opts) => { @@ -123,6 +125,7 @@ describe(commands.COPY, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -165,39 +168,33 @@ describe(commands.COPY, () => { }); it('fails validation if nameConflictBehavior is not a valid option', async () => { - const actual = await command.validate({ - options: { - webUrl: 'https://contoso.sharepoint.com', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents', - nameConflictBehavior: 'invalid' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents', + nameConflictBehavior: 'invalid' + }); + assert.strictEqual(actual.success, false); }); it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ - options: { - webUrl: 'foo', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'foo', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents' + }); + assert.strictEqual(actual.success, false); }); it('passes validation with valid options', async () => { - const actual = await command.validate({ - options: { - webUrl: 'https://contoso.sharepoint.com', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents', - newName: 'file1', - nameConflictBehavior: 'rename' - } - }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents', + newName: 'file1', + nameConflictBehavior: 'rename' + }); + assert.strictEqual(actual.success, true); }); it('copies file from source to target', async () => { diff --git a/src/m365/file/commands/file-copy.ts b/src/m365/file/commands/file-copy.ts index 6c911b35115..3408c6b8bc4 100644 --- a/src/m365/file/commands/file-copy.ts +++ b/src/m365/file/commands/file-copy.ts @@ -1,6 +1,7 @@ import { Drive, DriveItem } from '@microsoft/microsoft-graph-types'; +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { urlUtil } from '../../../utils/urlUtil.js'; import { spo } from '../../../utils/spo.js'; @@ -8,21 +9,28 @@ import { validation } from '../../../utils/validation.js'; import GraphCommand from '../../base/GraphCommand.js'; import commands from '../commands.js'; +const nameConflictBehaviorOptions = ['fail', 'replace', 'rename'] as const; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string() + .refine(url => validation.isValidSharePointUrl(url) === true, { + error: e => `'${e.input}' is not a valid SharePoint Online site URL.` + }) + .alias('u'), + sourceUrl: z.string().alias('s'), + targetUrl: z.string().alias('t'), + newName: z.string().optional(), + nameConflictBehavior: z.enum(nameConflictBehaviorOptions).optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - sourceUrl: string; - targetUrl: string; - newName?: string; - nameConflictBehavior?: string; -} - class FileCopyCommand extends GraphCommand { - private readonly nameConflictBehaviorOptions = ['fail', 'replace', 'rename']; - public get name(): string { return commands.COPY; } @@ -31,46 +39,8 @@ class FileCopyCommand extends GraphCommand { return 'Copies a file to another location using the Microsoft Graph'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - webUrl: typeof args.options.webUrl !== 'undefined', - sourceUrl: typeof args.options.sourceUrl !== 'undefined', - targetUrl: typeof args.options.targetUrl !== 'undefined', - newName: typeof args.options.newName !== 'undefined', - nameConflictBehavior: typeof args.options.nameConflictBehavior !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { option: '-u, --webUrl ' }, - { option: '-s, --sourceUrl ' }, - { option: '-t, --targetUrl ' }, - { option: '--newName [newName]' }, - { option: '--nameConflictBehavior [nameConflictBehavior]', autocomplete: this.nameConflictBehaviorOptions } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.nameConflictBehavior && this.nameConflictBehaviorOptions.indexOf(args.options.nameConflictBehavior) === -1) { - return `${args.options.nameConflictBehavior} is not a valid nameConflictBehavior value. Allowed values: ${this.nameConflictBehaviorOptions.join(', ')}.`; - } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -106,7 +76,7 @@ class FileCopyCommand extends GraphCommand { const sourceFileExtension = sourceFileName.includes('.') ? sourceFileName.substring(sourceFileName.lastIndexOf('.')) : ''; const newNameExtension = newName.includes('.') ? newName.substring(newName.lastIndexOf('.')) : ''; - requestOptions.data.name = newNameExtension ? `${newName.replace(newNameExtension, "")}${sourceFileExtension}` : `${newName}${sourceFileExtension}`; + requestOptions.data.name = newNameExtension ? `${newName.replace(newNameExtension, '')}${sourceFileExtension}` : `${newName}${sourceFileExtension}`; } await request.post(requestOptions); @@ -201,4 +171,4 @@ class FileCopyCommand extends GraphCommand { } } -export default new FileCopyCommand(); \ No newline at end of file +export default new FileCopyCommand(); diff --git a/src/m365/file/commands/file-list.spec.ts b/src/m365/file/commands/file-list.spec.ts index eb4b877b79c..30c3449a58a 100644 --- a/src/m365/file/commands/file-list.spec.ts +++ b/src/m365/file/commands/file-list.spec.ts @@ -10,6 +10,7 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; +import { z } from 'zod'; import commands from '../commands.js'; import command from './file-list.js'; @@ -18,6 +19,7 @@ describe(commands.LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -26,6 +28,7 @@ describe(commands.LIST, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -9778,22 +9781,18 @@ describe(commands.LIST, () => { }); it(`fails validation if the specified webUrl is invalid`, async () => { - const actual = await command.validate({ - options: { - folderUrl: '/Shared Documents', - webUrl: '/' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + folderUrl: '/Shared Documents', + webUrl: '/' + }); + assert.strictEqual(actual.success, false); }); it(`passes validation if the target file is a URL`, async () => { - const actual = await command.validate({ - options: { - folderUrl: 'Shared Documents', - webUrl: 'https://contoso.sharepoint.com/Shared Documents' - } - }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + folderUrl: 'Shared Documents', + webUrl: 'https://contoso.sharepoint.com/Shared Documents' + }); + assert.strictEqual(actual.success, true); }); }); \ No newline at end of file diff --git a/src/m365/file/commands/file-list.ts b/src/m365/file/commands/file-list.ts index 93cbdb9aeec..6b985882052 100644 --- a/src/m365/file/commands/file-list.ts +++ b/src/m365/file/commands/file-list.ts @@ -1,6 +1,7 @@ import { Drive, DriveItem, Site } from '@microsoft/microsoft-graph-types'; +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import request, { CliRequestOptions } from '../../../request.js'; import { formatting } from '../../../utils/formatting.js'; import { odata } from '../../../utils/odata.js'; @@ -8,16 +9,23 @@ import { validation } from '../../../utils/validation.js'; import GraphCommand from '../../base/GraphCommand.js'; import commands from '../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string() + .refine(url => validation.isValidSharePointUrl(url) === true, { + error: e => `'${e.input}' is not a valid SharePoint Online site URL.` + }) + .alias('u'), + folderUrl: z.string(), + recursive: z.boolean().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - folderUrl: string; - recursive?: boolean; -} - class FileListCommand extends GraphCommand { foldersToGetFilesFrom: string[] = []; @@ -33,34 +41,8 @@ class FileListCommand extends GraphCommand { return ['name', 'lastModifiedByUser']; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - recursive: !!args.options.recursive - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { option: '-u, --webUrl ' }, - { option: '--folderUrl ' }, - { option: '--recursive' } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => validation.isValidSharePointUrl(args.options.webUrl) - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -225,4 +207,4 @@ class FileListCommand extends GraphCommand { } } -export default new FileListCommand(); \ No newline at end of file +export default new FileListCommand(); diff --git a/src/m365/file/commands/file-move.spec.ts b/src/m365/file/commands/file-move.spec.ts index dc93308f238..07be4dd8c11 100644 --- a/src/m365/file/commands/file-move.spec.ts +++ b/src/m365/file/commands/file-move.spec.ts @@ -11,6 +11,7 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; +import { z } from 'zod'; import commands from '../commands.js'; import command from './file-move.js'; @@ -18,6 +19,7 @@ describe(commands.MOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; const defaultPostStub = (): sinon.SinonStub => { return sinon.stub(request, 'post').callsFake(async (opts) => { @@ -176,6 +178,7 @@ describe(commands.MOVE, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -219,39 +222,33 @@ describe(commands.MOVE, () => { }); it('fails validation if nameConflictBehavior is not a valid option', async () => { - const actual = await command.validate({ - options: { - webUrl: 'https://contoso.sharepoint.com', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents', - nameConflictBehavior: 'invalid' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents', + nameConflictBehavior: 'invalid' + }); + assert.strictEqual(actual.success, false); }); it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ - options: { - webUrl: 'foo', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'foo', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents' + }); + assert.strictEqual(actual.success, false); }); it('passes validation with valid options', async () => { - const actual = await command.validate({ - options: { - webUrl: 'https://contoso.sharepoint.com', - sourceUrl: '/Shared Documents/file.pdf', - targetUrl: '/teams/finance/Shared Documents', - newName: 'file1', - nameConflictBehavior: 'rename' - } - }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + sourceUrl: '/Shared Documents/file.pdf', + targetUrl: '/teams/finance/Shared Documents', + newName: 'file1', + nameConflictBehavior: 'rename' + }); + assert.strictEqual(actual.success, true); }); it('moves a file by renaming when the same name already exists.', async () => { diff --git a/src/m365/file/commands/file-move.ts b/src/m365/file/commands/file-move.ts index e254c14b670..90468ce0ea6 100644 --- a/src/m365/file/commands/file-move.ts +++ b/src/m365/file/commands/file-move.ts @@ -1,6 +1,7 @@ import { Drive } from '@microsoft/microsoft-graph-types'; +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../Command.js'; import GraphCommand from '../../base/GraphCommand.js'; import { setTimeout } from 'timers/promises'; import commands from '../commands.js'; @@ -10,21 +11,29 @@ import { urlUtil } from '../../../utils/urlUtil.js'; import { drive } from '../../../utils/drive.js'; import { validation } from '../../../utils/validation.js'; +const nameConflictBehaviorOptions = ['fail', 'replace', 'rename'] as const; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + webUrl: z.string() + .refine(url => validation.isValidSharePointUrl(url) === true, { + error: e => `'${e.input}' is not a valid SharePoint Online site URL.` + }) + .alias('u'), + sourceUrl: z.string().alias('s'), + targetUrl: z.string().alias('t'), + newName: z.string().optional(), + nameConflictBehavior: z.enum(nameConflictBehaviorOptions).optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - webUrl: string; - sourceUrl: string; - targetUrl: string; - newName?: string; - nameConflictBehavior?: string; -} - class FileMoveCommand extends GraphCommand { private pollingInterval: number = 10_000; - private readonly nameConflictBehaviorOptions = ['fail', 'replace', 'rename']; public get name(): string { return commands.MOVE; @@ -34,46 +43,8 @@ class FileMoveCommand extends GraphCommand { return 'Moves a file to another location using the Microsoft Graph'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - webUrl: typeof args.options.webUrl !== 'undefined', - sourceUrl: typeof args.options.sourceUrl !== 'undefined', - targetUrl: typeof args.options.targetUrl !== 'undefined', - newName: typeof args.options.newName !== 'undefined', - nameConflictBehavior: typeof args.options.nameConflictBehavior !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { option: '-u, --webUrl ' }, - { option: '-s, --sourceUrl ' }, - { option: '-t, --targetUrl ' }, - { option: '--newName [newName]' }, - { option: '--nameConflictBehavior [nameConflictBehavior]', autocomplete: this.nameConflictBehaviorOptions } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.nameConflictBehavior && this.nameConflictBehaviorOptions.indexOf(args.options.nameConflictBehavior) === -1) { - return `${args.options.nameConflictBehavior} is not a valid nameConflictBehavior value. Allowed values: ${this.nameConflictBehaviorOptions.join(', ')}.`; - } - - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -143,7 +114,7 @@ class FileMoveCommand extends GraphCommand { const sourceFileName = sourcePath.substring(sourcePath.lastIndexOf('/') + 1); const sourceFileExtension = sourceFileName.includes('.') ? sourceFileName.substring(sourceFileName.lastIndexOf('.')) : ''; const newNameExtension = newName.includes('.') ? newName.substring(newName.lastIndexOf('.')) : ''; - requestOptions.data.name = newNameExtension ? `${newName.replace(newNameExtension, "")}${sourceFileExtension}` : `${newName}${sourceFileExtension}`; + requestOptions.data.name = newNameExtension ? `${newName.replace(newNameExtension, '')}${sourceFileExtension}` : `${newName}${sourceFileExtension}`; } return requestOptions; @@ -175,4 +146,4 @@ class FileMoveCommand extends GraphCommand { } } -export default new FileMoveCommand(); \ No newline at end of file +export default new FileMoveCommand();