From 728a04574d66e3a16813b48645200df7164cd464 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 3 May 2024 11:26:16 +0200 Subject: [PATCH] New command: m365 spo folder sharinglink set. Closes #5964 --- .../cmd/spo/folder/folder-sharinglink-set.mdx | 119 ++++++++++ docs/src/config/sidebars.ts | 5 + src/m365/spo/commands.ts | 1 + .../folder/folder-sharinglink-set.spec.ts | 210 ++++++++++++++++++ .../commands/folder/folder-sharinglink-set.ts | 149 +++++++++++++ src/utils/driveUtil.ts | 63 ++++++ src/utils/spo.ts | 32 +++ 7 files changed, 579 insertions(+) create mode 100644 docs/docs/cmd/spo/folder/folder-sharinglink-set.mdx create mode 100644 src/m365/spo/commands/folder/folder-sharinglink-set.spec.ts create mode 100644 src/m365/spo/commands/folder/folder-sharinglink-set.ts create mode 100644 src/utils/driveUtil.ts diff --git a/docs/docs/cmd/spo/folder/folder-sharinglink-set.mdx b/docs/docs/cmd/spo/folder/folder-sharinglink-set.mdx new file mode 100644 index 0000000000..c2fdc09b33 --- /dev/null +++ b/docs/docs/cmd/spo/folder/folder-sharinglink-set.mdx @@ -0,0 +1,119 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spo folder sharinglink set + +Updates a sharing link of a folder + +## Usage + +```sh +m365 spo folder sharinglink set [options] +``` + +## Options + +```md definition-list +`-u, --webUrl ` +: The URL of the site where the folder is located. + +`--folderUrl [folderUrl]` +: The server- or site-relative decoded URL of the folder. Specify either `folderUrl` or `folderId` but not both. + +`--folderId [folderId]` +: The unique ID (GUID) of the folder. Specify either `folderUrl` or `folderId` but not both. + +`--id ` +: The sharing link ID. + +`--role [role]` +: Role to set. Possible options are: `read` and `write`. This option only work for permissions that don't have a sharing link. + +`--expirationDateTime ` +: The date and time to set the expiration. This should be defined as a valid ISO 8601 string. This option only works for anonymous links. +``` + + + +## Examples + +Updates the expiration datetime of an anonymous sharing link with a specific id. + +```sh +m365 spo folder sharinglink set --webUrl https://contoso.sharepoint.com/sites/demo --folderId daebb04b-a773-4baa-b1d1-3625418e3234 --id 7c9f97c9-1bda-433c-9364-bb83e81771ee --expirationDateTime '2022-11-30T00:00:00Z' +``` + +Updates the roles of a sharing permission. + +```sh +m365 spo folder sharinglink set --webUrl https://contoso.sharepoint.com/sites/demo --folderUrl /sites/demo/shared%20documents/Folder --id Q29udG9zbyBEcm9uZSBMYW5kaW5nIE93bmVycw --role read +``` + +## Response + + + + + ```json + { + "id": "bd1481e9-958b-4c1a-a33c-fb021f4ed444", + "roles": [ + "write" + ], + "shareId": "u!aHR0cHM6Ly83NTY2YXZhLnNoYXJlcG9pbnQuY29tLzpmOi9nL0VwLVpGUHF2YkVsQnNnWXJhRjJBNG1jQmZWM1A3cU00eGZVVXJRZHdnSXllNGc", + "expirationDateTime": "2024-05-05T16:57:00Z", + "hasPassword": false, + "grantedToIdentitiesV2": [], + "grantedToIdentities": [], + "link": { + "scope": "anonymous", + "type": "edit", + "webUrl": "https://contoso.sharepoint.com/:f:/g/Ep-ZFPqvbElBsgYraF2A4mcBfV3P7qM4xfUUrQdwgIye4g", + "preventsDownload": false + } + } + ``` + + + + + ```text + expirationDateTime : 2024-05-05T16:57:00Z + grantedToIdentities : [] + grantedToIdentitiesV2: [] + hasPassword : false + id : bd1481e9-958b-4c1a-a33c-fb021f4ed444 + link : {"scope":"anonymous","type":"edit","webUrl":"https://contoso.sharepoint.com/:f:/g/Ep-ZFPqvbElBsgYraF2A4mcBfV3P7qM4xfUUrQdwgIye4g","preventsDownload":false} + roles : ["write"] + shareId : u!aHR0cHM6Ly83NTY2YXZhLnNoYXJlcG9pbnQuY29tLzpmOi9nL0VwLVpGUHF2YkVsQnNnWXJhRjJBNG1jQmZWM1A3cU00eGZVVXJRZHdnSXllNGc + ``` + + + + + ```csv + id,shareId,expirationDateTime,hasPassword + bd1481e9-958b-4c1a-a33c-fb021f4ed444,u!aHR0cHM6Ly83NTY2YXZhLnNoYXJlcG9pbnQuY29tLzpmOi9nL0VwLVpGUHF2YkVsQnNnWXJhRjJBNG1jQmZWM1A3cU00eGZVVXJRZHdnSXllNGc,2024-05-05T16:57:00Z, + ``` + + + + + ```md + # spo folder sharinglink set --webUrl "https://contoso.sharepoint.com" --folderUrl "/shared documents/f1" --id "bd1481e9-958b-4c1a-a33c-fb021f4ed444" --expirationDateTime "2024-05-05T16:57:00.000Z" + + Date: 03/05/2024 + + ## bd1481e9-958b-4c1a-a33c-fb021f4ed444 + + Property | Value + ---------|------- + id | bd1481e9-958b-4c1a-a33c-fb021f4ed444 + shareId | u!aHR0cHM6Ly83NTY2YXZhLnNoYXJlcG9pbnQuY29tLzpmOi9nL0VwLVpGUHF2YkVsQnNnWXJhRjJBNG1jQmZWM1A3cU00eGZVVXJRZHdnSXllNGc + expirationDateTime | 2024-05-05T16:57:00Z + hasPassword | false + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 65682bcc32..7e9c5e002d 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -2506,6 +2506,11 @@ const sidebars: SidebarsConfig = { type: 'doc', label: 'folder roleinheritance reset', id: 'cmd/spo/folder/folder-roleinheritance-reset' + }, + { + type: 'doc', + label: 'folder sharinglink set', + id: 'cmd/spo/folder/folder-sharinglink-set' } ] }, diff --git a/src/m365/spo/commands.ts b/src/m365/spo/commands.ts index 3756aa1e96..2179e0c36b 100644 --- a/src/m365/spo/commands.ts +++ b/src/m365/spo/commands.ts @@ -100,6 +100,7 @@ export default { FOLDER_ROLEASSIGNMENT_ADD: `${prefix} folder roleassignment add`, FOLDER_ROLEINHERITANCE_BREAK: `${prefix} folder roleinheritance break`, FOLDER_ROLEINHERITANCE_RESET: `${prefix} folder roleinheritance reset`, + FOLDER_SHARINGLINK_SET: `${prefix} folder sharinglink set`, GET: `${prefix} get`, GROUP_ADD: `${prefix} group add`, GROUP_GET: `${prefix} group get`, diff --git a/src/m365/spo/commands/folder/folder-sharinglink-set.spec.ts b/src/m365/spo/commands/folder/folder-sharinglink-set.spec.ts new file mode 100644 index 0000000000..2323c8f5fa --- /dev/null +++ b/src/m365/spo/commands/folder/folder-sharinglink-set.spec.ts @@ -0,0 +1,210 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandError } from '../../../../Command.js'; +import { telemetry } from '../../../../telemetry.js'; +import { Logger } from '../../../../cli/Logger.js'; +import request from '../../../../request.js'; +import { formatting } from '../../../../utils/formatting.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import commands from '../../commands.js'; +import command from './folder-sharinglink-set.js'; + +describe(commands.FOLDER_SHARINGLINK_SET, () => { + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + const webUrl = 'https://contoso.sharepoint.com/sites/project-x'; + const folderId = 'f09c4efe-b8c0-4e89-a166-03418661b89b'; + const folderUrl = '/sites/project-x/shared documents/folder1'; + const siteId = '0f9b8f4f-0e8e-4630-bb0a-501442db9b64'; + const driveId = '013TMHP6UOOSLON57HT5GLKEU7R5UGWZVK'; + const itemId = 'b!T4-bD44OMEa7ClAUQtubZID9tc40pGJKpguycvELod_Gx-lo4ZQiRJ7vylonTufG'; + const id = 'ef1cddaa-b74a-4aae-8a7a-5c16b4da67f2'; + + const defaultGetStub = (): sinon.SinonStub => { + return sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `${webUrl}/_api/web/GetFolderById('${folderId}')?$select=ServerRelativeUrl`) { + return { ServerRelativeUrl: folderUrl }; + } + else if (opts.url === `${webUrl}/_api/web/GetFolderByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter('/sites/project-x/shared documents')}')?$select=ServerRelativeUrl`) { + return { ServerRelativeUrl: '/sites/project-x/shared documents' }; + } + else if (opts.url === `${webUrl}/_api/web/GetFolderById('invalid')?$select=ServerRelativeUrl`) { + throw { error: { 'odata.error': { message: { value: 'Folder Not Found.' } } } }; + } + else if (opts.url === `https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/project-x?$select=id`) { + return { id: siteId }; + } + else if (opts.url === `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=webUrl,id`) { + return getDriveResponse; + } + else if (opts.url === `https://graph.microsoft.com/v1.0/drives/${driveId}/root:/folder1?$select=id` || + opts.url === `https://graph.microsoft.com/v1.0/drives/${driveId}/root?$select=id` + ) { + return { id: itemId }; + } + + throw 'Invalid request'; + }); + }; + + const getDriveResponse: any = { + value: [ + { + "id": driveId, + "webUrl": `${webUrl}/Shared%20Documents` + } + ] + }; + + const graphResponse = { + "id": "2a021f54-90a2-4016-b3b3-5f34d2e7d932", + "roles": [ + "read" + ], + "hasPassword": false, + "grantedToIdentitiesV2": [], + "grantedToIdentities": [], + "link": { + "scope": "anonymous", + "type": "view", + "webUrl": "https://contoso.sharepoint.com/:b:/s/pnpcoresdktestgroup/EY50lub3559MtRKfj2hrZqoBWnHOpGIcgi4gzw9XiWYJ-A", + "preventsDownload": false + } + }; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + request.patch + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.FOLDER_SHARINGLINK_SET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { + const actual = await command.validate({ options: { webUrl: 'foo', folderId: folderId, id: id } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the folderId option is not a valid GUID', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, folderId: 'invalid', id: id } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the expirationDateTime option is not a valid date', async () => { + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', folderId: folderId, expirationDateTime: 'invalid date', id: id } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if invalid role specified', async () => { + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', folderId: folderId, role: 'invalid role', id: id } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if options are valid', async () => { + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', folderId: folderId, role: 'read', id: id } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('updates a sharing link to a folder specified by the id', async () => { + defaultGetStub(); + + sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/permissions/${id}`) { + return graphResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: webUrl, folderId: folderId, role: 'read', id: id, verbose: true } } as any); + assert(loggerLogSpy.calledWith(graphResponse)); + }); + + it('updates a sharing link to a folder specified by the URL', async () => { + defaultGetStub(); + + sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/permissions/${id}`) { + return graphResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: webUrl, folderUrl: '/sites/project-x/shared documents/', expirationDateTime: '2024-05-05T16:57:00.000Z', id: id, verbose: true } } as any); + assert(loggerLogSpy.calledWith(graphResponse)); + }); + + it('throws error when folder not found by id', async () => { + defaultGetStub(); + + await assert.rejects(command.action(logger, { options: { webUrl: webUrl, folderId: 'invalid', role: "read" } } as any), + new CommandError(`Folder Not Found.`)); + }); + + it('throws error when drive not found by url', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `${webUrl}/_api/web/GetFolderByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(folderUrl)}')?$select=ServerRelativeUrl`) { + return { ServerRelativeUrl: folderUrl }; + } + else if (opts.url === `https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/project-x?$select=id`) { + return { id: siteId }; + } + else if (opts.url === `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=webUrl,id`) { + return { + value: [] + }; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { webUrl: webUrl, folderUrl: folderUrl, type: 'read' } } as any), + new CommandError(`Drive 'https://contoso.sharepoint.com/sites/project-x/shared%20documents/folder1' not found`)); + }); +}); \ No newline at end of file diff --git a/src/m365/spo/commands/folder/folder-sharinglink-set.ts b/src/m365/spo/commands/folder/folder-sharinglink-set.ts new file mode 100644 index 0000000000..3b960b6bd7 --- /dev/null +++ b/src/m365/spo/commands/folder/folder-sharinglink-set.ts @@ -0,0 +1,149 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { spo } from '../../../../utils/spo.js'; +import { urlUtil } from '../../../../utils/urlUtil.js'; +import { driveUtil } from '../../../../utils/driveUtil.js'; +import { validation } from '../../../../utils/validation.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import { Drive } from '@microsoft/microsoft-graph-types'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + webUrl: string; + folderUrl?: string; + folderId?: string; + id: string; + role?: string; + expirationDateTime?: string; +} + +class SpoFolderSharingLinkSetCommand extends SpoCommand { + private static readonly roles: string[] = ['read', 'write']; + + public get name(): string { + return commands.FOLDER_SHARINGLINK_SET; + } + + public get description(): string { + return 'Updates a specific sharing link to a folder'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + webUrl: typeof args.options.webUrl !== 'undefined', + folderUrl: typeof args.options.folderUrl !== 'undefined', + folderId: typeof args.options.folderId !== 'undefined', + id: typeof args.options.id !== 'undefined', + role: typeof args.options.role !== 'undefined', + expirationDateTime: typeof args.options.expirationDateTime !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { option: '-u, --webUrl ' }, + { option: '--folderUrl [folderUrl]' }, + { option: '--folderId [folderId]' }, + { option: '-i, --id ' }, + { + option: '--role [role]', + autocomplete: SpoFolderSharingLinkSetCommand.roles + }, + { option: '--expirationDateTime [expirationDateTime]' } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + const isValidSharePointUrl: boolean | string = validation.isValidSharePointUrl(args.options.webUrl); + if (isValidSharePointUrl !== true) { + return isValidSharePointUrl; + } + + if (args.options.folderId && !validation.isValidGuid(args.options.folderId)) { + return `${args.options.folderId} is not a valid GUID`; + } + + if (args.options.role && + SpoFolderSharingLinkSetCommand.roles.indexOf(args.options.role) < 0) { + return `'${args.options.role}' is not a valid role. Allowed roles are ${SpoFolderSharingLinkSetCommand.roles.join(', ')}`; + } + + if (args.options.expirationDateTime && !validation.isValidISODateTime(args.options.expirationDateTime)) { + return `${args.options.expirationDateTime} is not a valid ISO date string.`; + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['folderId', 'folderUrl'] }); + } + + #initTypes(): void { + this.types.string.push('webUrl', 'folderId', 'folderUrl', 'id', 'expirationDateTime', 'role'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Updating sharing link to a folder ${args.options.folderId || args.options.folderUrl}...`); + } + + try { + const relFolderUrl: string = await spo.getFolderServerRelativeUrl(args.options.webUrl, args.options.folderUrl, args.options.folderId); + const absoluteFolderUrl: string = urlUtil.getAbsoluteUrl(args.options.webUrl, relFolderUrl); + const folderUrl: URL = new URL(absoluteFolderUrl); + + const siteId: string = await spo.getSiteId(args.options.webUrl); + const drive: Drive = await driveUtil.getDriveByUrl(siteId, folderUrl); + const itemId: string = await driveUtil.getDriveItemId(drive, folderUrl); + + const requestOptions: CliRequestOptions = { + url: `https://graph.microsoft.com/v1.0/drives/${drive.id}/items/${itemId}/permissions/${args.options.id}`, + headers: { + accept: 'application/json;odata.metadata=none', + 'content-type': 'application/json' + }, + responseType: 'json', + data: {} + }; + + if (args.options.expirationDateTime) { + requestOptions.data.expirationDateTime = args.options.expirationDateTime; + } + + if (args.options.role) { + requestOptions.data.roles = [args.options.role]; + } + + const sharingLink = await request.patch(requestOptions); + + await logger.log(sharingLink); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new SpoFolderSharingLinkSetCommand(); \ No newline at end of file diff --git a/src/utils/driveUtil.ts b/src/utils/driveUtil.ts new file mode 100644 index 0000000000..ff63bf207d --- /dev/null +++ b/src/utils/driveUtil.ts @@ -0,0 +1,63 @@ + +import { Drive, DriveItem } from '@microsoft/microsoft-graph-types'; +import request, { CliRequestOptions } from "../request.js"; + +export const driveUtil = { + /** + * Retrieves the Drive associated with the specified site and URL. + * @param siteId Site ID + * @param url Drive URL + * @returns The Drive associated with the drive URL. + */ + async getDriveByUrl(siteId: string, url: URL): Promise { + const requestOptions: CliRequestOptions = { + url: `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=webUrl,id`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const drives = await request.get<{ value: Drive[] }>(requestOptions); + + const lowerCaseFolderUrl: string = url.href.toLowerCase(); + + const drive: Drive | undefined = drives.value + .sort((a, b) => (b.webUrl as string).localeCompare(a.webUrl as string)) + .find((d: Drive) => { + const driveUrl: string = (d.webUrl as string).toLowerCase(); + + return lowerCaseFolderUrl.startsWith(driveUrl) && + (driveUrl.length === lowerCaseFolderUrl.length || + lowerCaseFolderUrl[driveUrl.length] === '/'); + }); + + if (!drive) { + throw `Drive '${url.href}' not found`; + } + + return drive; + }, + + /** + * Retrieves the ID of a drive item (file, folder, etc.) associated with the given drive and item URL. + * @param drive The Drive object containing the item + * @param itemUrl Item URL + * @returns Drive item ID + */ + async getDriveItemId(drive: Drive, itemUrl: URL): Promise { + const relativeItemUrl: string = itemUrl.href.replace(new RegExp(`${drive.webUrl}`, 'i'), '').replace(/\/+$/, ''); + + const requestOptions: CliRequestOptions = { + url: `https://graph.microsoft.com/v1.0/drives/${drive.id}/root${relativeItemUrl ? `:${relativeItemUrl}` : ''}?$select=id`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const driveItem = await request.get(requestOptions); + + return driveItem?.id as string; + } +}; \ No newline at end of file diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 5a82d74265..188d67aedf 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -1674,6 +1674,38 @@ export const spo = { return site.id as string; }, + /** + * Retrieves the server-relative URL of a folder. + * @param webUrl Web URL + * @param folderUrl Folder URL + * @param folderId Folder ID + * @returns The server-relative URL of the folder + */ + async getFolderServerRelativeUrl(webUrl: string, folderUrl: string | undefined, folderId: string | undefined): Promise { + let requestUrl: string = `${webUrl}/_api/web/`; + + if (folderUrl) { + const folderServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, folderUrl); + requestUrl += `GetFolderByServerRelativePath(decodedUrl='${formatting.encodeQueryParameter(folderServerRelativeUrl)}')`; + } + else { + requestUrl += `GetFolderById('${folderId}')`; + } + + requestUrl += '?$select=ServerRelativeUrl'; + + const requestOptions: CliRequestOptions = { + url: requestUrl, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + const res = await request.get<{ ServerRelativeUrl: string }>(requestOptions); + return res.ServerRelativeUrl; + }, + /** * Retrieves the ObjectIdentity from a SharePoint site * @param webUrl web url