diff --git a/src/System Application/App/MicrosoftGraph/app.json b/src/System Application/App/MicrosoftGraph/app.json index 396158fd77..62f5bd4374 100644 --- a/src/System Application/App/MicrosoftGraph/app.json +++ b/src/System Application/App/MicrosoftGraph/app.json @@ -52,6 +52,11 @@ "id": "2746dab0-7900-449d-b154-20751e116a67", "name": "Microsoft Graph Test", "publisher": "Microsoft" + }, + { + "id": "1a7bfa64-c856-49ed-86b0-bb05eb5b2de4", + "name": "SharePoint", + "publisher": "Microsoft" } ], "screenshots": [], diff --git a/src/System Application/App/MicrosoftGraph/src/GraphRequestHeader.Enum.al b/src/System Application/App/MicrosoftGraph/src/GraphRequestHeader.Enum.al index a1a420bd9a..2b58f69ce0 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphRequestHeader.Enum.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphRequestHeader.Enum.al @@ -43,4 +43,12 @@ enum 9353 "Graph Request Header" { Caption = 'ConsistencyLevel', Locked = true; } + + /// + /// Range Request Header + /// + value(40; Range) + { + Caption = 'Range', Locked = true; + } } \ No newline at end of file diff --git a/src/System Application/App/SharePoint/README.md b/src/System Application/App/SharePoint/README.md index efa10f21cc..ee0103bdbd 100644 --- a/src/System Application/App/SharePoint/README.md +++ b/src/System Application/App/SharePoint/README.md @@ -1,16 +1,166 @@ -Provides functions to interact with SharePoint REST API +Provides functions to interact with SharePoint. -Use this module to do the following: -> Navigate Lists and Folders. +Two clients are available: +- **SharePoint Client** - Legacy REST API v1 +- **SharePoint Graph Client** - Modern Microsoft Graph API -> Upload and Download files. +--- -> Create folders and list items. +# SharePoint Graph Client +Modern implementation using Microsoft Graph API. Provides simpler authentication, cleaner interfaces, and better performance. -# Authorization +## Authorization + +Use Graph Authorization from the Graph module: + +```al +var + GraphAuth: Codeunit "Graph Authorization"; + GraphAuthorization: Interface "Graph Authorization"; +begin + GraphAuthorization := GraphAuth.CreateAuthorizationWithClientCredentials( + '', '', '', + 'https://graph.microsoft.com/.default'); +``` + +## Initialize Client + +```al +var + SPGraphClient: Codeunit "SharePoint Graph Client"; +begin + SPGraphClient.Initialize('https://contoso.sharepoint.com/sites/MySite/', GraphAuthorization); +``` + +## Working with Lists + +```al +var + GraphList: Record "SharePoint Graph List" temporary; + Response: Codeunit "SharePoint Graph Response"; +begin + // Get all lists + Response := SPGraphClient.GetLists(GraphList); + + // Create a new list + Response := SPGraphClient.CreateList('My List', 'Description', GraphList); +``` + +## Working with List Items + +```al +var + GraphListItem: Record "SharePoint Graph List Item" temporary; +begin + // Get items from a list + Response := SPGraphClient.GetListItems('', GraphListItem); + + // Create a new item + Response := SPGraphClient.CreateListItem('', 'Item Title', GraphListItem); +``` + +## Working with Drives and Files + +```al +var + GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; + TempBlob: Codeunit "Temp Blob"; + FileInStream: InStream; + Response: Codeunit "SharePoint Graph Response"; +begin + // Get root folder items + Response := SPGraphClient.GetRootItems(GraphDriveItem); + if not Response.IsSuccessful() then + Error(Response.GetError()); + + // Filter to files only and download first one + GraphDriveItem.SetRange(IsFolder, false); + if GraphDriveItem.FindFirst() then + Response := SPGraphClient.DownloadFile(GraphDriveItem.Id, TempBlob); + + // Get items by path + Response := SPGraphClient.GetItemsByPath('Documents/Folder1', GraphDriveItem); + + // Upload a file (empty path = root folder) + Response := SPGraphClient.UploadFile('', 'file.pdf', FileInStream, GraphDriveItem); + + // Create a folder + Response := SPGraphClient.CreateFolder('Documents', 'NewFolder', GraphDriveItem); +``` + +## Large File Operations + +For files over 4MB, use chunked upload/download: + +```al +begin + // Upload large file (uses resumable upload session) + Response := SPGraphClient.UploadLargeFile('Documents', 'largefile.zip', FileInStream, GraphDriveItem); + + // Download large file (uses 100MB chunks) + Response := SPGraphClient.DownloadLargeFile('', TempBlob); +``` + +## Item Management + +```al +var + Exists: Boolean; + Response: Codeunit "SharePoint Graph Response"; +begin + // Check if item exists + Response := SPGraphClient.ItemExistsByPath('Documents/file.pdf', Exists); + + // Delete item + Response := SPGraphClient.DeleteItemByPath('Documents/file.pdf'); + + // Copy item (asynchronous operation) + Response := SPGraphClient.CopyItemByPath('Documents/file.pdf', 'Archive', 'file_copy.pdf'); + + // Move/rename item + Response := SPGraphClient.MoveItemByPath('Documents/file.pdf', 'Archive', ''); +``` + +## OData Query Parameters + +```al +var + OptionalParams: Codeunit "Graph Optional Parameters"; +begin + // Filter items + SPGraphClient.SetODataFilter(OptionalParams, 'name eq ''document.docx'''); + + // Select specific fields + SPGraphClient.SetODataSelect(OptionalParams, 'id,name,size'); + + // Order results + SPGraphClient.SetODataOrderBy(OptionalParams, 'name asc'); + + Response := SPGraphClient.GetRootItems(GraphDriveItem, OptionalParams); +``` + +## Error Handling + +All methods return `SharePoint Graph Response` codeunit: + +```al +var + Response: Codeunit "SharePoint Graph Response"; +begin + Response := SPGraphClient.GetLists(GraphList); + if not Response.IsSuccessful() then + Error(Response.GetError()); +``` + +--- + +# SharePoint Client (Legacy REST API) + +Legacy implementation using SharePoint REST API v1. + +## Authorization -## User Credentials Use "SharePoint Authorization module". ## Example diff --git a/src/System Application/App/SharePoint/app.json b/src/System Application/App/SharePoint/app.json index bf6fa474da..8d1a4a11cf 100644 --- a/src/System Application/App/SharePoint/app.json +++ b/src/System Application/App/SharePoint/app.json @@ -40,6 +40,18 @@ "name": "BLOB Storage", "publisher": "Microsoft", "version": "28.0.0.0" + }, + { + "id": "6d72c93d-164a-494c-8d65-24d7f41d7b61", + "name": "Microsoft Graph", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, + { + "id": "812b339d-a9db-4a6e-84e4-fe35cbef0c44", + "name": "Rest Client", + "publisher": "Microsoft", + "version": "28.0.0.0" } ], "screenshots": [], @@ -54,6 +66,11 @@ "contextSensitiveHelpUrl": "https://docs.microsoft.com/dynamics365/business-central/", "target": "OnPrem", "internalsVisibleTo": [ + { + "id": "977e6b76-d7c1-41fa-b38b-21399cd140a7", + "name": "SharePoint Test", + "publisher": "Microsoft" + }, { "id": "ff0caa38-65a2-49c5-a7e2-6a0475cfc60e", "name": "SharePoint Test Library", diff --git a/src/System Application/App/SharePoint/permissions/SharePointApiObjects.PermissionSet.al b/src/System Application/App/SharePoint/permissions/SharePointApiObjects.PermissionSet.al index 4bf88968c0..e057d3eb5d 100644 --- a/src/System Application/App/SharePoint/permissions/SharePointApiObjects.PermissionSet.al +++ b/src/System Application/App/SharePoint/permissions/SharePointApiObjects.PermissionSet.al @@ -10,5 +10,6 @@ permissionset 9100 "SharePoint API - Objects" Access = Internal; Assignable = false; - Permissions = codeunit "SharePoint Client" = X; + Permissions = codeunit "SharePoint Client" = X, + codeunit "SharePoint Graph Client" = X; } diff --git a/src/System Application/App/SharePoint/src/graph/SharePointGraphClient.Codeunit.al b/src/System Application/App/SharePoint/src/graph/SharePointGraphClient.Codeunit.al new file mode 100644 index 0000000000..69404ee38b --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/SharePointGraphClient.Codeunit.al @@ -0,0 +1,752 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Graph.Authorization; +using System.RestClient; +using System.Utilities; + +/// +/// Provides functionality for interacting with SharePoint through Microsoft Graph API. +/// +codeunit 9119 "SharePoint Graph Client" +{ + Access = Public; + + var + SharePointGraphClientImpl: Codeunit "SharePoint Graph Client Impl."; + + #region Initialization + + /// + /// Initializes SharePoint Graph client. + /// + /// SharePoint site URL. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphClientImpl.Initialize(NewSharePointUrl, GraphAuthorization); + end; + + /// + /// Initializes SharePoint Graph client with a specific API version. + /// + /// SharePoint site URL. + /// The Graph API version to use. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; ApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphClientImpl.Initialize(NewSharePointUrl, ApiVersion, GraphAuthorization); + end; + + /// + /// Initializes SharePoint Graph client with a custom base URL. + /// + /// SharePoint site URL. + /// The custom base URL for Graph API. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; BaseUrl: Text; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphClientImpl.Initialize(NewSharePointUrl, BaseUrl, GraphAuthorization); + end; + + /// + /// Initializes SharePoint Graph client with an HTTP client handler. + /// + /// SharePoint site URL. + /// The Graph API version to use. + /// The Graph API authorization to use. + /// HTTP client handler for intercepting requests. + procedure Initialize(NewSharePointUrl: Text; ApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization"; HttpClientHandler: Interface "Http Client Handler") + begin + SharePointGraphClientImpl.Initialize(NewSharePointUrl, ApiVersion, GraphAuthorization, HttpClientHandler); + end; + + #endregion + + #region Lists + + /// + /// Gets all lists from the SharePoint site. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetLists(var GraphLists: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetLists(GraphLists)); + end; + + /// + /// Gets all lists from the SharePoint site. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters + /// An operation response object containing the result of the operation. + procedure GetLists(var GraphLists: Record "SharePoint Graph List" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetLists(GraphLists, GraphOptionalParameters)); + end; + + /// + /// Gets a SharePoint list by ID. + /// + /// ID of the list to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetList(ListId: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetList(ListId, GraphList)); + end; + + /// + /// Gets a SharePoint list by ID. + /// + /// ID of the list to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetList(ListId: Text; var GraphList: Record "SharePoint Graph List" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetList(ListId, GraphList, GraphOptionalParameters)); + end; + + /// + /// Creates a new SharePoint list. + /// + /// Display name for the list. + /// Description for the list. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateList(DisplayName: Text; Description: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateList(DisplayName, Description, GraphList)); + end; + + /// + /// Creates a new SharePoint list. + /// + /// Display name for the list. + /// Template for the list (genericList, documentLibrary, etc.) + /// Description for the list. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateList(DisplayName: Text; ListTemplate: Text; Description: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateList(DisplayName, ListTemplate, Description, GraphList)); + end; + + #endregion + + #region List Items + + /// + /// Gets items from a SharePoint list. + /// + /// ID of the list. + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetListItems(ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetListItems(ListId, GraphListItems)); + end; + + /// + /// Gets items from a SharePoint list. + /// + /// ID of the list. + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetListItems(ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetListItems(ListId, GraphListItems, GraphOptionalParameters)); + end; + + /// + /// Creates a new item in a SharePoint list. + /// + /// ID of the list. + /// JSON object containing the fields for the new item. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateListItem(ListId: Text; FieldsJsonObject: JsonObject; var GraphListItem: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateListItem(ListId, FieldsJsonObject, GraphListItem)); + end; + + /// + /// Creates a new item in a SharePoint list with a simple title. + /// + /// ID of the list. + /// Title for the new item. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateListItem(ListId: Text; Title: Text; var GraphListItem: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateListItem(ListId, Title, GraphListItem)); + end; + + #endregion + + #region Drive and Items + + /// + /// Gets the default document library (drive) for the site. + /// + /// ID of the default drive. + /// An operation response object containing the result of the operation. + procedure GetDefaultDrive(var DriveId: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDefaultDrive(DriveId)); + end; + + /// + /// Gets all drives (document libraries) available on the site with detailed information. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetDrives(var GraphDrives: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDrives(GraphDrives)); + end; + + /// + /// Gets all drives (document libraries) available on the site with detailed information. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDrives(var GraphDrives: Record "SharePoint Graph Drive" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDrives(GraphDrives, GraphOptionalParameters)); + end; + + /// + /// Gets a drive (document library) by ID with detailed information. + /// + /// ID of the drive to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDrive(DriveId: Text; var GraphDrive: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDrive(DriveId, GraphDrive)); + end; + + /// + /// Gets a drive (document library) by ID with detailed information. + /// + /// ID of the drive to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDrive(DriveId: Text; var GraphDrive: Record "SharePoint Graph Drive" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDrive(DriveId, GraphDrive, GraphOptionalParameters)); + end; + + /// + /// Gets the default document library (drive) for the site with detailed information. + /// + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDefaultDrive(var GraphDrive: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDefaultDrive(GraphDrive)); + end; + + /// + /// Gets items in the root folder of the default drive. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetRootItems(var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetRootItems(GraphDriveItems)); + end; + + /// + /// Gets items in the root folder of the default drive. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetRootItems(var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetRootItems(GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets children of a folder by the folder's ID. + /// + /// ID of the folder. + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetFolderItems(FolderId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetFolderItems(FolderId, GraphDriveItems)); + end; + + /// + /// Gets children of a folder by the folder's ID. + /// + /// ID of the folder. + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetFolderItems(FolderId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetFolderItems(FolderId, GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets items from a path in the default drive. + /// + /// Path to the folder (e.g., 'Documents/Folder1'). + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetItemsByPath(FolderPath: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetItemsByPath(FolderPath, GraphDriveItems)); + end; + + /// + /// Gets items from a path in the default drive. + /// + /// Path to the folder (e.g., 'Documents/Folder1'). + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetItemsByPath(FolderPath: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetItemsByPath(FolderPath, GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets a file or folder by ID. + /// + /// ID of the item to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDriveItem(ItemId: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDriveItem(ItemId, GraphDriveItem)); + end; + + /// + /// Gets a file or folder by ID. + /// + /// ID of the item to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDriveItem(ItemId: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDriveItem(ItemId, GraphDriveItem, GraphOptionalParameters)); + end; + + /// + /// Gets a file or folder by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDriveItemByPath(ItemPath: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDriveItemByPath(ItemPath, GraphDriveItem)); + end; + + /// + /// Gets a file or folder by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDriveItemByPath(ItemPath: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.GetDriveItemByPath(ItemPath, GraphDriveItem, GraphOptionalParameters)); + end; + + /// + /// Creates a new folder. + /// + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateFolder(FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateFolder('', FolderPath, FolderName, GraphDriveItem)); + end; + + /// + /// Creates a new folder with specified conflict behavior. + /// + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// How to handle conflicts if a folder with the same name exists + /// An operation response object containing the result of the operation. + procedure CreateFolder(FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateFolder('', FolderPath, FolderName, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Creates a new folder in a specific drive (document library). + /// + /// ID of the drive (document library). + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateFolder(DriveId: Text; FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateFolder(DriveId, FolderPath, FolderName, GraphDriveItem)); + end; + + /// + /// Creates a new folder in a specific drive (document library) with specified conflict behavior. + /// + /// ID of the drive (document library). + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// How to handle conflicts if a folder with the same name exists + /// An operation response object containing the result of the operation. + procedure CreateFolder(DriveId: Text; FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CreateFolder(DriveId, FolderPath, FolderName, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Uploads a file to a folder on the default drive. + /// + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadFile(FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadFile('', FolderPath, FileName, FileInStream, GraphDriveItem)); + end; + + /// + /// Uploads a file to a folder on the default drive with specified conflict behavior. + /// + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadFile(FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadFile('', FolderPath, FileName, FileInStream, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Uploads a file to a folder in a specific drive (document library). + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem)); + end; + + /// + /// Uploads a file to a folder in a specific drive (document library) with specified conflict behavior. + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Downloads a file. + /// + /// ID of the file to download. + /// TempBlob to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadFile(ItemId: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DownloadFile(ItemId, TempBlob)); + end; + + /// + /// Downloads a file by path. + /// + /// Path to the file (e.g., 'Documents/file.docx'). + /// TempBlob to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadFileByPath(FilePath: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DownloadFileByPath(FilePath, TempBlob)); + end; + + /// + /// Downloads a large file using chunked download for files larger than Business Central's 150MB HTTP response limit. + /// + /// ID of the file to download. + /// TempBlob to receive the file content. + /// An operation response object containing the result of the operation. + /// Uses 100MB chunks to stay under the 150MB limit. Any chunk failure will fail the entire download. + procedure DownloadLargeFile(ItemId: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DownloadLargeFile(ItemId, TempBlob)); + end; + + /// + /// Downloads a large file by path using chunked download for files larger than Business Central's 150MB HTTP response limit. + /// + /// Path to the file (e.g., 'Documents/file.docx'). + /// Blob to receive the file content. + /// An operation response object containing the result of the operation. + /// Uses 100MB chunks to stay under the 150MB limit. Any chunk failure will fail the entire download. + procedure DownloadLargeFileByPath(FilePath: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DownloadLargeFileByPath(FilePath, TempBlob)); + end; + + /// + /// Uploads a large file to a folder on the default drive using chunked upload for improved performance and reliability. + /// + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadLargeFile('', FolderPath, FileName, FileInStream, GraphDriveItem)); + end; + + /// + /// Uploads a large file to a folder on the default drive using chunked upload with specified conflict behavior. + /// + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadLargeFile('', FolderPath, FileName, FileInStream, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Uploads a large file to a folder in a specific drive (document library) using chunked upload. + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadLargeFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem)); + end; + + /// + /// Uploads a large file to a folder in a specific drive (document library) using chunked upload with specified conflict behavior. + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.UploadLargeFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem, ConflictBehavior)); + end; + + /// + /// Deletes a drive item (file or folder) by ID. + /// + /// ID of the item to delete. + /// An operation response object containing the result of the operation. + /// Returns success even if the item doesn't exist (404 is treated as success). + procedure DeleteItem(ItemId: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DeleteItem(ItemId)); + end; + + /// + /// Deletes a drive item (file or folder) by path. + /// + /// Path to the item (e.g., 'Documents/file.docx' or 'Documents/folder'). + /// An operation response object containing the result of the operation. + /// Returns success even if the item doesn't exist (404 is treated as success). + procedure DeleteItemByPath(ItemPath: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.DeleteItemByPath(ItemPath)); + end; + + /// + /// Checks if a drive item (file or folder) exists by ID. + /// + /// ID of the item to check. + /// True if the item exists, false if it doesn't exist (404). + /// An operation response object containing the result of the operation. + procedure ItemExists(ItemId: Text; var Exists: Boolean): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.ItemExists(ItemId, Exists)); + end; + + /// + /// Checks if a drive item (file or folder) exists by path. + /// + /// Path to the item (e.g., 'Documents/file.docx' or 'Documents/folder'). + /// True if the item exists, false if it doesn't exist (404). + /// An operation response object containing the result of the operation. + procedure ItemExistsByPath(ItemPath: Text; var Exists: Boolean): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.ItemExistsByPath(ItemPath, Exists)); + end; + + /// + /// Copies a drive item (file or folder) to a new location by ID. + /// + /// ID of the item to copy. + /// ID of the target folder. + /// New name for the copied item (optional - leave empty to keep original name). + /// An operation response object containing the result of the operation. + /// This is an asynchronous operation. The copy happens in the background. + procedure CopyItem(ItemId: Text; TargetFolderId: Text; NewName: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CopyItem(ItemId, TargetFolderId, NewName)); + end; + + /// + /// Copies a drive item (file or folder) to a new location by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Path to the target folder (e.g., 'Documents/Archive'). + /// New name for the copied item (optional - leave empty to keep original name). + /// An operation response object containing the result of the operation. + /// This is an asynchronous operation. The copy happens in the background. + procedure CopyItemByPath(ItemPath: Text; TargetFolderPath: Text; NewName: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.CopyItemByPath(ItemPath, TargetFolderPath, NewName)); + end; + + /// + /// Moves a drive item (file or folder) to a new location by ID. + /// + /// ID of the item to move. + /// ID of the target folder (leave empty to only rename). + /// New name for the moved item (leave empty to keep original name). + /// An operation response object containing the result of the operation. + /// At least one of TargetFolderId or NewName must be provided. + procedure MoveItem(ItemId: Text; TargetFolderId: Text; NewName: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.MoveItem(ItemId, TargetFolderId, NewName)); + end; + + /// + /// Moves a drive item (file or folder) to a new location by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Path to the target folder (leave empty to only rename). + /// New name for the moved item (leave empty to keep original name). + /// An operation response object containing the result of the operation. + /// At least one of TargetFolderPath or NewName must be provided. + procedure MoveItemByPath(ItemPath: Text; TargetFolderPath: Text; NewName: Text): Codeunit "SharePoint Graph Response" + begin + exit(SharePointGraphClientImpl.MoveItemByPath(ItemPath, TargetFolderPath, NewName)); + end; + + #endregion + + /// + /// Creates an OData query to filter items in SharePoint + /// + /// The optional parameters to configure + /// The OData filter expression + /// Use this for $filter OData queries + procedure SetODataFilter(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; Filter: Text) + begin + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::Filter, Filter); + end; + + /// + /// Creates an OData query to select specific fields from items in SharePoint + /// + /// The optional parameters to configure + /// The fields to select (comma-separated) + /// Use this for $select OData queries + procedure SetODataSelect(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; Select: Text) + begin + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::Select, Select); + end; + + /// + /// Creates an OData query to expand related entities in SharePoint + /// + /// The optional parameters to configure + /// The entities to expand (comma-separated) + /// Use this for $expand OData queries + procedure SetODataExpand(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; Expand: Text) + begin + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::Expand, Expand); + end; + + /// + /// Creates an OData query to order results in SharePoint + /// + /// The optional parameters to configure + /// The fields to order by (e.g. "displayName asc") + /// Use this for $orderby OData queries + procedure SetODataOrderBy(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; OrderBy: Text) + begin + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::OrderBy, OrderBy); + end; + + /// + /// Returns detailed information on last API call. + /// + /// Codeunit holding http response status, reason phrase, headers and possible error information for the last API call + procedure GetDiagnostics(): Interface "HTTP Diagnostics" + begin + exit(SharePointGraphClientImpl.GetDiagnostics()); + end; + + /// + /// Sets the site ID directly for testing purposes. + /// + /// The site ID to set. + internal procedure SetSiteIdForTesting(SiteId: Text) + begin + SharePointGraphClientImpl.SetSiteIdForTesting(SiteId); + end; + + /// + /// Sets the default drive ID directly for testing purposes. + /// + /// The default drive ID to set. + internal procedure SetDefaultDriveIdForTesting(DefaultDriveId: Text) + begin + SharePointGraphClientImpl.SetDefaultDriveIdForTesting(DefaultDriveId); + end; +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/SharePointGraphClientImpl.Codeunit.al b/src/System Application/App/SharePoint/src/graph/SharePointGraphClientImpl.Codeunit.al new file mode 100644 index 0000000000..9be756bb7f --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/SharePointGraphClientImpl.Codeunit.al @@ -0,0 +1,1941 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Graph.Authorization; +using System.RestClient; +using System.Utilities; + +/// +/// Provides functionality for interacting with SharePoint through Microsoft Graph API. +/// This implementation uses native Graph API concepts and models. +/// +codeunit 9120 "SharePoint Graph Client Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + SharePointGraphRequestHelper: Codeunit "SharePoint Graph Req. Helper"; + SharePointGraphParser: Codeunit "Sharepoint Graph Parser"; + SharePointGraphUriBuilder: Codeunit "Sharepoint Graph Uri Builder"; + SiteId: Text; + SharePointUrl: Text; + DefaultDriveId: Text; + IsInitialized: Boolean; + NotInitializedErr: Label 'SharePoint Graph Client is not initialized. Call Initialize first.'; + InvalidSharePointUrlErr: Label 'Invalid SharePoint URL ''%1''.', Comment = '%1 = URL string'; + RetrieveSiteInfoErr: Label 'Failed to retrieve SharePoint site information from Graph API. %1', Comment = '%1 = Error message'; + ContentRangeHeaderLbl: Label 'bytes %1-%2/%3', Locked = true, Comment = '%1 = Start Bytes, %2 = End Bytes, %3 = Total Bytes'; + FailedToRetrieveListsErr: Label 'Failed to retrieve lists: %1', Comment = '%1 = Error message'; + FailedToParseListsErr: Label 'Failed to parse lists collection from response'; + FailedToRetrieveListErr: Label 'Failed to retrieve list: %1', Comment = '%1 = Error message'; + FailedToParseListErr: Label 'Failed to parse list details from response'; + InvalidListIdErr: Label 'List ID cannot be empty'; + InvalidDisplayNameErr: Label 'Display name cannot be empty'; + FailedToCreateListErr: Label 'Failed to create list: %1', Comment = '%1 = Error message'; + FailedToParseCreatedListErr: Label 'Failed to parse created list details from response'; + FailedToRetrieveListItemsErr: Label 'Failed to retrieve list items: %1', Comment = '%1 = Error message'; + FailedToParseListItemsErr: Label 'Failed to parse list items collection from response'; + FailedToCreateListItemErr: Label 'Failed to create list item: %1', Comment = '%1 = Error message'; + FailedToParseCreatedListItemErr: Label 'Failed to parse created list item details from response'; + NoDefaultDriveIdErr: Label 'Default drive ID is not available. Please check the SharePoint site.'; + FailedToRetrieveDefaultDriveErr: Label 'Failed to retrieve default drive: %1', Comment = '%1 = Error message'; + FailedToRetrieveDrivesErr: Label 'Failed to retrieve drives: %1', Comment = '%1 = Error message'; + FailedToParseDrivesErr: Label 'Failed to parse drives collection from response'; + FailedToRetrieveDriveErr: Label 'Failed to retrieve drive: %1', Comment = '%1 = Error message'; + FailedToParseDriveErr: Label 'Failed to parse drive details from response'; + InvalidDriveIdErr: Label 'Drive ID cannot be empty'; + FailedToRetrieveRootItemsErr: Label 'Failed to retrieve root items: %1', Comment = '%1 = Error message'; + FailedToParseRootItemsErr: Label 'Failed to parse root items collection from response'; + InvalidFolderIdErr: Label 'Folder ID cannot be empty'; + FailedToRetrieveFolderItemsErr: Label 'Failed to retrieve folder items: %1', Comment = '%1 = Error message'; + FailedToParseFolderItemsErr: Label 'Failed to parse folder items collection from response'; + FailedToRetrieveItemsByPathErr: Label 'Failed to retrieve items by path: %1', Comment = '%1 = Error message'; + FailedToParseItemsByPathErr: Label 'Failed to parse items collection from response'; + InvalidItemIdErr: Label 'Item ID cannot be empty'; + FailedToRetrieveDriveItemErr: Label 'Failed to retrieve drive item: %1', Comment = '%1 = Error message'; + FailedToParseDriveItemErr: Label 'Failed to parse drive item details from response'; + InvalidItemPathErr: Label 'Item path cannot be empty'; + FailedToRetrieveDriveItemByPathErr: Label 'Failed to retrieve drive item by path: %1', Comment = '%1 = Error message'; + FailedToParseDriveItemByPathErr: Label 'Failed to parse drive item details from response'; + InvalidFolderNameErr: Label 'Folder name cannot be empty'; + FailedToCreateFolderErr: Label 'Failed to create folder: %1', Comment = '%1 = Error message'; + FailedToParseCreatedFolderErr: Label 'Failed to parse created folder details from response'; + InvalidFileNameErr: Label 'File name cannot be empty'; + FailedToUploadFileErr: Label 'Failed to upload file: %1', Comment = '%1 = Error message'; + FailedToParseUploadedFileErr: Label 'Failed to parse uploaded file details from response'; + InvalidFileSizeErr: Label 'File size must be greater than 0'; + FailedToCreateUploadSessionErr: Label 'Failed to create upload session: %1', Comment = '%1 = Error message'; + FailedToUploadChunkErr: Label 'Failed to upload file chunk: %1', Comment = '%1 = Error message'; + NoUploadResponseErr: Label 'No response received from chunked upload'; + FailedToParseChunkedUploadErr: Label 'Failed to parse chunked upload response'; + FailedToDownloadFileErr: Label 'Failed to download file: %1', Comment = '%1 = Error message'; + InvalidFilePathErr: Label 'File path cannot be empty'; + FailedToDownloadFileByPathErr: Label 'Failed to download file by path: %1', Comment = '%1 = Error message'; + FailedToDownloadChunkErr: Label 'Failed to download file chunk %1-%2: %3', Comment = '%1 = Start byte, %2 = End byte, %3 = Error message'; + FailedToGetFileSizeErr: Label 'Failed to get file size for chunked download: %1', Comment = '%1 = Error message'; + FailedToDeleteItemErr: Label 'Failed to delete item: %1', Comment = '%1 = Error message'; + FailedToDeleteItemByPathErr: Label 'Failed to delete item by path: %1', Comment = '%1 = Error message'; + InvalidTargetPathErr: Label 'Target path cannot be empty'; + FailedToCopyItemErr: Label 'Failed to copy item: %1', Comment = '%1 = Error message'; + FailedToMoveItemErr: Label 'Failed to move item: %1', Comment = '%1 = Error message'; + + #region Initialization + + /// + /// Initializes SharePoint Graph client. + /// + /// SharePoint site URL. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphRequestHelper.Initialize(GraphAuthorization); + InitializeCommon(NewSharePointUrl); + end; + + /// + /// Initializes SharePoint Graph client with a specific API version. + /// + /// SharePoint site URL. + /// The Graph API version to use. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; ApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphRequestHelper.Initialize(ApiVersion, GraphAuthorization); + InitializeCommon(NewSharePointUrl); + end; + + /// + /// Initializes SharePoint Graph client with a custom base URL. + /// + /// SharePoint site URL. + /// The custom base URL for Graph API. + /// The Graph API authorization to use. + procedure Initialize(NewSharePointUrl: Text; BaseUrl: Text; GraphAuthorization: Interface "Graph Authorization") + begin + SharePointGraphRequestHelper.Initialize(BaseUrl, GraphAuthorization); + InitializeCommon(NewSharePointUrl); + end; + + /// + /// Initializes SharePoint Graph client with an HTTP client handler for testing. + /// + /// SharePoint site URL. + /// The Graph API version to use. + /// The Graph API authorization to use. + /// HTTP client handler for intercepting requests. + procedure Initialize(NewSharePointUrl: Text; ApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization"; HttpClientHandler: Interface "Http Client Handler") + begin + SharePointGraphRequestHelper.Initialize(ApiVersion, GraphAuthorization, HttpClientHandler); + InitializeCommon(NewSharePointUrl); + end; + + /// + /// Common initialization logic shared by all Initialize overloads. + /// + /// SharePoint site URL. + local procedure InitializeCommon(NewSharePointUrl: Text) + begin + // If we have a new URL, clear the cached IDs so they'll be re-acquired + if SharePointUrl <> NewSharePointUrl then begin + SharePointUrl := NewSharePointUrl; + SiteId := ''; + DefaultDriveId := ''; + end; + IsInitialized := true; + end; + + local procedure GetSiteIdFromUrl(Url: Text) + var + JsonResponse: JsonObject; + JsonToken: JsonToken; + HostName: Text; + RelativePath: Text; + Endpoint: Text; + begin + // Extract hostname and relative path from the URL + HostName := ExtractHostName(Url); + RelativePath := ExtractRelativePath(Url); + + if (HostName = '') or (RelativePath = '') then + Error(InvalidSharePointUrlErr, Url); + + // Build the Graph endpoint to get site information + Endpoint := SharePointGraphUriBuilder.GetSiteByHostAndPathEndpoint(HostName, RelativePath); + + if not SharePointGraphRequestHelper.Get(Endpoint, JsonResponse) then + Error(RetrieveSiteInfoErr, SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase()); + + if JsonResponse.Get('id', JsonToken) then + SiteId := JsonToken.AsValue().AsText(); + end; + + local procedure GetDefaultDriveId() + var + JsonResponse: JsonObject; + JsonToken: JsonToken; + begin + if SiteId = '' then + exit; + + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveEndpoint(), JsonResponse) then + exit; + + if JsonResponse.Get('id', JsonToken) then + DefaultDriveId := JsonToken.AsValue().AsText(); + end; + + local procedure ExtractHostName(Url: Text): Text + var + UriBuilder: Codeunit "Uri Builder"; + begin + UriBuilder.Init(Url); + exit(UriBuilder.GetHost()); // Returns contoso.sharepoint.com + end; + + local procedure ExtractRelativePath(Url: Text): Text + var + UriBuilder: Codeunit "Uri Builder"; + Path: Text; + begin + UriBuilder.Init(Url); + + // Azure AD / Graph requires the path to start with '/' + Path := UriBuilder.GetPath(); + + // Guarantee at least '/' + if Path = '' then + Path := '/'; + + exit(Path); + end; + + local procedure EnsureInitialized() + begin + if not IsInitialized then + Error(NotInitializedErr); + end; + + #endregion + + #region Lists + + /// + /// Gets all lists from the SharePoint site. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetLists(var GraphLists: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetLists(GraphLists, GraphOptionalParameters)); + end; + + /// + /// Gets all lists from the SharePoint site. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters + /// An operation response object containing the result of the operation. + procedure GetLists(var GraphLists: Record "SharePoint Graph List" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetListsEndpoint(), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveListsErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseListCollection(JsonArray, GraphLists) then begin + SharePointGraphResponse.SetError(FailedToParseListsErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets a SharePoint list by ID. + /// + /// ID of the list to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetList(ListId: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetList(ListId, GraphList, GraphOptionalParameters)); + end; + + /// + /// Gets a SharePoint list by ID. + /// + /// ID of the list to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetList(ListId: Text; var GraphList: Record "SharePoint Graph List" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ListId = '' then begin + SharePointGraphResponse.SetError(InvalidListIdErr); + exit(SharePointGraphResponse); + end; + + // Make the API request + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetListEndpoint(ListId), JsonResponse, GraphOptionalParameters) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveListErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphList.Init(); + if not SharePointGraphParser.ParseListItem(JsonResponse, GraphList) then begin + SharePointGraphResponse.SetError(FailedToParseListErr); + exit(SharePointGraphResponse); + end; + GraphList.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Creates a new SharePoint list. + /// + /// Display name for the list. + /// Description for the list. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateList(DisplayName: Text; Description: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + begin + exit(CreateList(DisplayName, 'genericList', Description, GraphList)); + end; + + /// + /// Creates a new SharePoint list. + /// + /// Display name for the list. + /// Template for the list (genericList, documentLibrary, etc.) + /// Description for the list. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateList(DisplayName: Text; ListTemplate: Text; Description: Text; var GraphList: Record "SharePoint Graph List" temporary): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + RequestJsonObj: JsonObject; + ListJsonObj: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if DisplayName = '' then begin + SharePointGraphResponse.SetError(InvalidDisplayNameErr); + exit(SharePointGraphResponse); + end; + + // Create the request body with list properties + RequestJsonObj.Add('displayName', DisplayName); + RequestJsonObj.Add('description', Description); + + // Add list template + ListJsonObj.Add('template', ListTemplate); + RequestJsonObj.Add('list', ListJsonObj); + + // Post the request to create the list + if not SharePointGraphRequestHelper.Post(SharePointGraphUriBuilder.GetListsEndpoint(), RequestJsonObj, JsonResponse) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToCreateListErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphList.Init(); + if not SharePointGraphParser.ParseListItem(JsonResponse, GraphList) then begin + SharePointGraphResponse.SetError(FailedToParseCreatedListErr); + exit(SharePointGraphResponse); + end; + GraphList.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + #endregion + + #region List Items + + /// + /// Gets items from a SharePoint list. + /// + /// ID of the list. + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetListItems(ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetListItems(ListId, GraphListItems, GraphOptionalParameters)); + end; + + /// + /// Gets items from a SharePoint list. + /// + /// ID of the list. + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetListItems(ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ListId = '' then begin + SharePointGraphResponse.SetError(InvalidListIdErr); + exit(SharePointGraphResponse); + end; + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetListItemsEndpoint(ListId), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveListItemsErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseListItemCollection(JsonArray, ListId, GraphListItems) then begin + SharePointGraphResponse.SetError(FailedToParseListItemsErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Creates a new item in a SharePoint list. + /// + /// ID of the list. + /// JSON object containing the fields for the new item. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateListItem(ListId: Text; FieldsJsonObject: JsonObject; var GraphListItem: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + RequestJsonObj: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ListId = '' then begin + SharePointGraphResponse.SetError(InvalidListIdErr); + exit(SharePointGraphResponse); + end; + + // Create the request body with fields + RequestJsonObj.Add('fields', FieldsJsonObject); + + // Post the request to create the item + if not SharePointGraphRequestHelper.Post(SharePointGraphUriBuilder.GetCreateListItemEndpoint(ListId), RequestJsonObj, JsonResponse) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToCreateListItemErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphListItem.Init(); + GraphListItem.ListId := CopyStr(ListId, 1, MaxStrLen(GraphListItem.ListId)); + if not SharePointGraphParser.ParseListItemDetail(JsonResponse, GraphListItem) then begin + SharePointGraphResponse.SetError(FailedToParseCreatedListItemErr); + exit(SharePointGraphResponse); + end; + GraphListItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Creates a new item in a SharePoint list with a simple title. + /// + /// ID of the list. + /// Title for the new item. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateListItem(ListId: Text; Title: Text; var GraphListItem: Record "SharePoint Graph List Item" temporary): Codeunit "SharePoint Graph Response" + var + FieldsJsonObject: JsonObject; + begin + FieldsJsonObject.Add('Title', Title); + exit(CreateListItem(ListId, FieldsJsonObject, GraphListItem)); + end; + + #endregion + + #region Drive and Items + + /// + /// Gets the default document library (drive) for the site. + /// + /// ID of the default drive. + /// An operation response object containing the result of the operation. + procedure GetDefaultDrive(var DriveId: Text): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Get default drive ID if not already cached + if DefaultDriveId = '' then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDefaultDriveErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + DriveId := DefaultDriveId; + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets all drives (document libraries) available on the site with detailed information. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetDrives(var GraphDrives: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetDrives(GraphDrives, GraphOptionalParameters)); + end; + + /// + /// Gets all drives (document libraries) available on the site with detailed information. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDrives(var GraphDrives: Record "SharePoint Graph Drive" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetDrivesEndpoint(), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDrivesErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseDriveCollection(JsonArray, GraphDrives) then begin + SharePointGraphResponse.SetError(FailedToParseDrivesErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets a drive (document library) by ID with detailed information. + /// + /// ID of the drive to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDrive(DriveId: Text; var GraphDrive: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetDrive(DriveId, GraphDrive, GraphOptionalParameters)); + end; + + /// + /// Gets a drive (document library) by ID with detailed information. + /// + /// ID of the drive to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDrive(DriveId: Text; var GraphDrive: Record "SharePoint Graph Drive" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + DriveEndpoint: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if DriveId = '' then begin + SharePointGraphResponse.SetError(InvalidDriveIdErr); + exit(SharePointGraphResponse); + end; + + // Construct drive endpoint for specific drive ID + DriveEndpoint := SharePointGraphUriBuilder.GetSiteEndpoint() + '/drives/' + DriveId; + + // Make the API request + if not SharePointGraphRequestHelper.Get(DriveEndpoint, JsonResponse, GraphOptionalParameters) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDriveErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDrive.Init(); + if not SharePointGraphParser.ParseDriveDetail(JsonResponse, GraphDrive) then begin + SharePointGraphResponse.SetError(FailedToParseDriveErr); + exit(SharePointGraphResponse); + end; + GraphDrive.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets the default document library (drive) for the site with detailed information. + /// + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDefaultDrive(var GraphDrive: Record "SharePoint Graph Drive" temporary): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + JsonResponse: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Make the API request + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveEndpoint(), JsonResponse, GraphOptionalParameters) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDefaultDriveErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDrive.Init(); + if not SharePointGraphParser.ParseDriveDetail(JsonResponse, GraphDrive) then begin + SharePointGraphResponse.SetError(FailedToParseDriveErr); + exit(SharePointGraphResponse); + end; + GraphDrive.Insert(); + + // Update DefaultDriveId while we're at it + DefaultDriveId := CopyStr(GraphDrive.Id, 1, MaxStrLen(DefaultDriveId)); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets items in the root folder of the default drive. + /// + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetRootItems(var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetRootItems(GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets items in the root folder of the default drive. + /// + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetRootItems(var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Ensure we have Default Drive ID + EnsureDefaultDriveId(); + if DefaultDriveId = '' then begin + SharePointGraphResponse.SetError(NoDefaultDriveIdErr); + exit(SharePointGraphResponse); + end; + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetDriveRootChildrenEndpoint(), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveRootItemsErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseDriveItemCollection(JsonArray, DefaultDriveId, GraphDriveItems) then begin + SharePointGraphResponse.SetError(FailedToParseRootItemsErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets children of a folder by the folder's ID. + /// + /// ID of the folder. + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetFolderItems(FolderId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetFolderItems(FolderId, GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets children of a folder by the folder's ID. + /// + /// ID of the folder. + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetFolderItems(FolderId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FolderId = '' then begin + SharePointGraphResponse.SetError(InvalidFolderIdErr); + exit(SharePointGraphResponse); + end; + + // Ensure we have Default Drive ID + EnsureDefaultDriveId(); + if DefaultDriveId = '' then begin + SharePointGraphResponse.SetError(NoDefaultDriveIdErr); + exit(SharePointGraphResponse); + end; + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetDriveItemChildrenByIdEndpoint(FolderId), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveFolderItemsErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseDriveItemCollection(JsonArray, DefaultDriveId, GraphDriveItems) then begin + SharePointGraphResponse.SetError(FailedToParseFolderItemsErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets items from a path in the default drive. + /// + /// Path to the folder (e.g., 'Documents/Folder1'). + /// Collection of the result (temporary record). + /// An operation response object containing the result of the operation. + procedure GetItemsByPath(FolderPath: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetItemsByPath(FolderPath, GraphDriveItems, GraphOptionalParameters)); + end; + + /// + /// Gets items from a path in the default drive. + /// + /// Path to the folder (e.g., 'Documents/Folder1'). + /// Collection of the result (temporary record). + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetItemsByPath(FolderPath: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonArray: JsonArray; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Handle empty path as root + if FolderPath = '' then + exit(GetRootItems(GraphDriveItems, GraphOptionalParameters)); + + // Remove leading slash if present + FolderPath := FolderPath.TrimStart('/'); + + // Use Graph pagination to get all pages automatically + if not SharePointGraphRequestHelper.GetAllPages(SharePointGraphUriBuilder.GetDriveItemChildrenByPathEndpoint(FolderPath), GraphOptionalParameters, JsonArray) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveItemsByPathErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Parse the combined results from all pages + if not SharePointGraphParser.ParseDriveItemCollection(JsonArray, DefaultDriveId, GraphDriveItems) then begin + SharePointGraphResponse.SetError(FailedToParseItemsByPathErr); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Uploads a file to a folder in a specific drive (document library) with specified conflict behavior. + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + JsonResponse: JsonObject; + EffectiveDriveId: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FileName = '' then begin + SharePointGraphResponse.SetError(InvalidFileNameErr); + exit(SharePointGraphResponse); + end; + + // Use default drive ID if none specified + if DriveId = '' then begin + EnsureDefaultDriveId(); + EffectiveDriveId := DefaultDriveId; + end else + EffectiveDriveId := DriveId; + + // Remove leading slash if present + FolderPath := FolderPath.TrimStart('/'); + + // Configure conflict behavior + SharePointGraphRequestHelper.ConfigureConflictBehavior(GraphOptionalParameters, ConflictBehavior); + + // Put the file content in the specific drive + if not SharePointGraphRequestHelper.UploadFile(SharePointGraphUriBuilder.GetSpecificDriveUploadEndpoint(EffectiveDriveId, FolderPath, FileName), FileInStream, GraphOptionalParameters, JsonResponse) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToUploadFileErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDriveItem.Init(); + GraphDriveItem.DriveId := CopyStr(EffectiveDriveId, 1, MaxStrLen(GraphDriveItem.DriveId)); + if not SharePointGraphParser.ParseDriveItemDetail(JsonResponse, GraphDriveItem) then begin + SharePointGraphResponse.SetError(FailedToParseUploadedFileErr); + exit(SharePointGraphResponse); + end; + GraphDriveItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Uploads a file to a folder in a specific drive (document library). + /// + /// ID of the drive (document library). + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(UploadFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem, Enum::"Graph ConflictBehavior"::Replace)); + end; + + /// + /// Uploads a large file to a folder on SharePoint using chunked upload for better performance and reliability. + /// + /// ID of the drive (document library), or empty for default drive. + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphConflictBehavior: Enum "Graph ConflictBehavior"; + begin + exit(UploadLargeFile(DriveId, FolderPath, FileName, FileInStream, GraphDriveItem, GraphConflictBehavior::Replace)); + end; + + /// + /// Uploads a large file to a folder on SharePoint using chunked upload with specified conflict behavior. + /// + /// ID of the drive (document library), or empty for default drive. + /// Path to the folder (e.g., 'Documents'). + /// Name of the file to upload. + /// Content of the file. + /// Record to store the result. + /// How to handle conflicts if a file with the same name exists + /// An operation response object containing the result of the operation. + procedure UploadLargeFile(DriveId: Text; FolderPath: Text; FileName: Text; FileInStream: InStream; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + TempBlob: Codeunit "Temp Blob"; + ChunkInStream: InStream; + ChunkOutStream: OutStream; + JsonResponse: JsonObject; + CompleteResponseJson: JsonObject; + Endpoint: Text; + UploadUrl: Text; + ContentRange: Text; + EffectiveDriveId: Text; + FileSize: Integer; + ChunkSize: Integer; + BytesInChunk: Integer; + TotalBytesRead: Integer; + MinChunkSize: Integer; + ChunkMultiple: Integer; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FileName = '' then begin + SharePointGraphResponse.SetError(InvalidFileNameErr); + exit(SharePointGraphResponse); + end; + + // Use default drive ID if none specified + if DriveId = '' then begin + EnsureDefaultDriveId(); + if DefaultDriveId = '' then begin + SharePointGraphResponse.SetError(NoDefaultDriveIdErr); + exit(SharePointGraphResponse); + end; + EffectiveDriveId := DefaultDriveId; + end else + EffectiveDriveId := DriveId; + + // Remove leading slash if present + FolderPath := FolderPath.TrimStart('/'); + + // Configure conflict behavior + SharePointGraphRequestHelper.ConfigureConflictBehavior(GraphOptionalParameters, ConflictBehavior); + + // Prepare the upload session endpoint + if EffectiveDriveId = DefaultDriveId then + Endpoint := SharePointGraphUriBuilder.GetDriveItemByPathEndpoint(FolderPath + '/' + FileName) + else + Endpoint := SharePointGraphUriBuilder.GetSpecificDriveUploadEndpoint(EffectiveDriveId, FolderPath, FileName); + + FileSize := FileInStream.Length(); + if FileSize <= 0 then begin + SharePointGraphResponse.SetError(InvalidFileSizeErr); + exit(SharePointGraphResponse); + end; + + // Create upload session + if not SharePointGraphRequestHelper.CreateUploadSession(Endpoint, FileName, FileSize, GraphOptionalParameters, ConflictBehavior, UploadUrl) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToCreateUploadSessionErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Microsoft requires chunks to be multiples of 320 KiB (327,680 bytes) + ChunkMultiple := 320 * 1024; // 320 KiB + MinChunkSize := ChunkMultiple; // Minimum allowed size + + // Use 4 MB chunks as recommended by Microsoft for optimum performance + ChunkSize := 4 * 1024 * 1024; // 4 MB + + // Ensure chunk size is a multiple of 320 KiB + ChunkSize := Round(ChunkSize / ChunkMultiple, 1, '<') * ChunkMultiple; + + // For small files, use at least the minimum size but ensure it doesn't exceed file size + if FileSize < ChunkSize then + ChunkSize := MinChunkSize; + + // Reset the stream position to beginning + FileInStream.ResetPosition(); + TotalBytesRead := 0; + + // Read and upload chunks until the entire file is uploaded + while TotalBytesRead < FileSize do begin + // Clear temp blob for new chunk + Clear(TempBlob); + TempBlob.CreateOutStream(ChunkOutStream); + + // Determine bytes to copy in this chunk + BytesInChunk := ChunkSize; + if (FileSize - TotalBytesRead) < ChunkSize then + // For the last chunk, ensure it's still a multiple of 320 KiB unless it's the final remainder + if (FileSize - TotalBytesRead) > MinChunkSize then + BytesInChunk := Round((FileSize - TotalBytesRead) / ChunkMultiple, 1, '<') * ChunkMultiple + else + BytesInChunk := FileSize - TotalBytesRead; + + // Copy directly from source stream into the chunk stream + CopyStream(ChunkOutStream, FileInStream, BytesInChunk); + + // Prepare content range header - must follow format "bytes startPosition-endPosition/totalSize" + ContentRange := StrSubstNo(ContentRangeHeaderLbl, + TotalBytesRead, + TotalBytesRead + BytesInChunk - 1, + FileSize); + + // Get the input stream for the chunk + TempBlob.CreateInStream(ChunkInStream); + + // Upload the chunk - use the exact URL returned from the upload session without modification + if not SharePointGraphRequestHelper.UploadChunk(UploadUrl, ChunkInStream, ContentRange, JsonResponse) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToUploadChunkErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Check if upload is complete (last chunk response will contain the item details) + if JsonResponse.Contains('id') then + CompleteResponseJson := JsonResponse; + + // Update total bytes read + TotalBytesRead += BytesInChunk; + end; + + if not CompleteResponseJson.Contains('id') then begin + SharePointGraphResponse.SetError(NoUploadResponseErr); + exit(SharePointGraphResponse); + end; + + GraphDriveItem.Init(); + GraphDriveItem.DriveId := CopyStr(EffectiveDriveId, 1, MaxStrLen(GraphDriveItem.DriveId)); + if not SharePointGraphParser.ParseDriveItemDetail(CompleteResponseJson, GraphDriveItem) then begin + SharePointGraphResponse.SetError(FailedToParseChunkedUploadErr); + exit(SharePointGraphResponse); + end; + GraphDriveItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Creates a new folder in a specific drive (document library) with specified conflict behavior. + /// + /// ID of the drive (document library). + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// How to handle conflicts if a folder with the same name exists + /// An operation response object containing the result of the operation. + procedure CreateFolder(DriveId: Text; FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; ConflictBehavior: Enum "Graph ConflictBehavior"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + JsonResponse: JsonObject; + RequestJsonObj: JsonObject; + FolderJsonObj: JsonObject; + Endpoint: Text; + EffectiveDriveId: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FolderName = '' then begin + SharePointGraphResponse.SetError(InvalidFolderNameErr); + exit(SharePointGraphResponse); + end; + + // Use default drive ID if none specified + if DriveId = '' then begin + EnsureDefaultDriveId(); + EffectiveDriveId := DefaultDriveId; + end else + EffectiveDriveId := DriveId; + + // Remove leading slash if present + FolderPath := FolderPath.TrimStart('/'); + + // Create the request body with folder properties + RequestJsonObj.Add('name', FolderName); + RequestJsonObj.Add('folder', FolderJsonObj); + + // Configure conflict behavior + SharePointGraphRequestHelper.ConfigureConflictBehavior(GraphOptionalParameters, ConflictBehavior); + + // Set endpoint for creating folder in specific drive + if FolderPath = '' then + Endpoint := SharePointGraphUriBuilder.GetSpecificDriveRootChildrenEndpoint(EffectiveDriveId) + else + Endpoint := SharePointGraphUriBuilder.GetSpecificDriveItemChildrenByPathEndpoint(EffectiveDriveId, FolderPath); + + // Post the request to create the folder + if not SharePointGraphRequestHelper.Post(Endpoint, RequestJsonObj, GraphOptionalParameters, JsonResponse) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToCreateFolderErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDriveItem.Init(); + GraphDriveItem.DriveId := CopyStr(EffectiveDriveId, 1, MaxStrLen(GraphDriveItem.DriveId)); + if not SharePointGraphParser.ParseDriveItemDetail(JsonResponse, GraphDriveItem) then begin + SharePointGraphResponse.SetError(FailedToParseCreatedFolderErr); + exit(SharePointGraphResponse); + end; + GraphDriveItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Creates a new folder in a specific drive (document library). + /// + /// ID of the drive (document library). + /// Path where to create the folder (e.g., 'Documents'). + /// Name of the new folder. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure CreateFolder(DriveId: Text; FolderPath: Text; FolderName: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + begin + exit(CreateFolder(DriveId, FolderPath, FolderName, GraphDriveItem, Enum::"Graph ConflictBehavior"::Fail)); + end; + + /// + /// Downloads a file. + /// + /// ID of the file to download. + /// TempBlob to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadFile(ItemId: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + // Make the API request + if not SharePointGraphRequestHelper.DownloadFile(SharePointGraphUriBuilder.GetDriveItemContentByIdEndpoint(ItemId), TempBlob) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToDownloadFileErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Downloads a file by path. + /// + /// Path to the file (e.g., 'Documents/file.docx'). + /// TempBlob to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadFileByPath(FilePath: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FilePath = '' then begin + SharePointGraphResponse.SetError(InvalidFilePathErr); + exit(SharePointGraphResponse); + end; + + // Remove leading slash if present + FilePath := FilePath.TrimStart('/'); + + // Make the API request + if not SharePointGraphRequestHelper.DownloadFile(SharePointGraphUriBuilder.GetDriveItemContentByPathEndpoint(FilePath), TempBlob) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToDownloadFileByPathErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Downloads a large file using chunked download to overcome Business Central's 150MB HTTP response limit. + /// + /// ID of the file to download. + /// OutStream to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadLargeFile(ItemId: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + ChunkTempBlob: Codeunit "Temp Blob"; + ChunkInStream: InStream; + FileOutStream: OutStream; + FileSize: BigInteger; + ChunkSize: BigInteger; + RangeStart: BigInteger; + RangeEnd: BigInteger; + Endpoint: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + // First, get the file size + if not GetDriveItem(ItemId, GraphDriveItem).IsSuccessful() then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToGetFileSizeErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + FileSize := GraphDriveItem.Size; + if FileSize <= 0 then begin + SharePointGraphResponse.SetError(InvalidFileSizeErr); + exit(SharePointGraphResponse); + end; + + // 100MB chunk size (104,857,600 bytes) - safely under 150MB Business Central limit + ChunkSize := 100 * 1024 * 1024; + RangeStart := 0; + Endpoint := SharePointGraphUriBuilder.GetDriveItemContentByIdEndpoint(ItemId); + + TempBlob.CreateOutStream(FileOutStream); + + // Download file in chunks + while RangeStart < FileSize do begin + Clear(ChunkTempBlob); + Clear(ChunkInStream); + + RangeEnd := RangeStart + ChunkSize - 1; + if RangeEnd >= FileSize then + RangeEnd := FileSize - 1; + + if not SharePointGraphRequestHelper.DownloadChunk(Endpoint, RangeStart, RangeEnd, ChunkTempBlob) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToDownloadChunkErr, + RangeStart, RangeEnd, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Write chunk to output Blob + ChunkTempBlob.CreateInStream(ChunkInStream); + CopyStream(FileOutStream, ChunkInStream); + + RangeStart := RangeEnd + 1; + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Downloads a large file by path using chunked download to overcome Business Central's 150MB HTTP response limit. + /// + /// Path to the file (e.g., 'Documents/file.docx'). + /// OutStream to receive the file content. + /// An operation response object containing the result of the operation. + procedure DownloadLargeFileByPath(FilePath: Text; var TempBlob: Codeunit "Temp Blob"): Codeunit "SharePoint Graph Response" + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + ChunkTempBlob: Codeunit "Temp Blob"; + ChunkInStream: InStream; + FileOutStream: OutStream; + FileSize: BigInteger; + ChunkSize: BigInteger; + RangeStart: BigInteger; + RangeEnd: BigInteger; + Endpoint: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if FilePath = '' then begin + SharePointGraphResponse.SetError(InvalidFilePathErr); + exit(SharePointGraphResponse); + end; + + // Remove leading slash if present + FilePath := FilePath.TrimStart('/'); + + // First, get the file size + if not GetDriveItemByPath(FilePath, GraphDriveItem).IsSuccessful() then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToGetFileSizeErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + FileSize := GraphDriveItem.Size; + if FileSize <= 0 then begin + SharePointGraphResponse.SetError(InvalidFileSizeErr); + exit(SharePointGraphResponse); + end; + + // 100MB chunk size (104,857,600 bytes) - safely under 150MB Business Central limit + ChunkSize := 100 * 1024 * 1024; + RangeStart := 0; + Endpoint := SharePointGraphUriBuilder.GetDriveItemContentByPathEndpoint(FilePath); + + TempBlob.CreateOutStream(FileOutStream); + + // Download file in chunks + while RangeStart < FileSize do begin + Clear(ChunkTempBlob); + + RangeEnd := RangeStart + ChunkSize - 1; + if RangeEnd >= FileSize then + RangeEnd := FileSize - 1; + + if not SharePointGraphRequestHelper.DownloadChunk(Endpoint, RangeStart, RangeEnd, ChunkTempBlob) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToDownloadChunkErr, + RangeStart, RangeEnd, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + // Write chunk to output Blob + ChunkTempBlob.CreateInStream(ChunkInStream); + CopyStream(FileOutStream, ChunkInStream); + + RangeStart := RangeEnd + 1; + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets a file or folder by ID. + /// + /// ID of the item to retrieve. + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDriveItem(ItemId: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetDriveItem(ItemId, GraphDriveItem, GraphOptionalParameters)); + end; + + /// + /// Gets a file or folder by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Record to store the result. + /// An operation response object containing the result of the operation. + procedure GetDriveItemByPath(ItemPath: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Codeunit "SharePoint Graph Response" + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(GetDriveItemByPath(ItemPath, GraphDriveItem, GraphOptionalParameters)); + end; + + /// + /// Gets a file or folder by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDriveItemByPath(ItemPath: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemPath = '' then begin + SharePointGraphResponse.SetError(InvalidItemPathErr); + exit(SharePointGraphResponse); + end; + + // Remove leading slash if present + ItemPath := ItemPath.TrimStart('/'); + + // Make the API request + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveItemByPathEndpoint(ItemPath), JsonResponse, GraphOptionalParameters) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDriveItemByPathErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDriveItem.Init(); + GraphDriveItem.DriveId := CopyStr(DefaultDriveId, 1, MaxStrLen(GraphDriveItem.DriveId)); + if not SharePointGraphParser.ParseDriveItemDetail(JsonResponse, GraphDriveItem) then begin + SharePointGraphResponse.SetError(FailedToParseDriveItemByPathErr); + exit(SharePointGraphResponse); + end; + GraphDriveItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Gets a file or folder by ID. + /// + /// ID of the item to retrieve. + /// Record to store the result. + /// A wrapper for optional header and query parameters. + /// An operation response object containing the result of the operation. + procedure GetDriveItem(ItemId: Text; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + // Make the API request + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveItemByIdEndpoint(ItemId), JsonResponse, GraphOptionalParameters) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDriveItemErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + GraphDriveItem.Init(); + GraphDriveItem.DriveId := CopyStr(DefaultDriveId, 1, MaxStrLen(GraphDriveItem.DriveId)); + if not SharePointGraphParser.ParseDriveItemDetail(JsonResponse, GraphDriveItem) then begin + SharePointGraphResponse.SetError(FailedToParseDriveItemErr); + exit(SharePointGraphResponse); + end; + GraphDriveItem.Insert(); + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Deletes a drive item (file or folder) by ID. + /// + /// ID of the item to delete. + /// An operation response object containing the result of the operation. + procedure DeleteItem(ItemId: Text): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + // Make the DELETE request + if not SharePointGraphRequestHelper.Delete(SharePointGraphUriBuilder.GetDriveItemByIdEndpoint(ItemId)) then begin + // 404 is success - item already doesn't exist + if SharePointGraphRequestHelper.GetDiagnostics().GetHttpStatusCode() = 404 then begin + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetError(StrSubstNo(FailedToDeleteItemErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Deletes a drive item (file or folder) by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// An operation response object containing the result of the operation. + procedure DeleteItemByPath(ItemPath: Text): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemPath = '' then begin + SharePointGraphResponse.SetError(InvalidFilePathErr); + exit(SharePointGraphResponse); + end; + + // Remove leading slash if present + ItemPath := ItemPath.TrimStart('/'); + + // Make the DELETE request + if not SharePointGraphRequestHelper.Delete(SharePointGraphUriBuilder.GetDriveItemByPathEndpoint(ItemPath)) then begin + // 404 is success - item already doesn't exist + if SharePointGraphRequestHelper.GetDiagnostics().GetHttpStatusCode() = 404 then begin + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetError(StrSubstNo(FailedToDeleteItemByPathErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Checks if a drive item (file or folder) exists by ID. + /// + /// ID of the item to check. + /// True if the item exists, false if it doesn't exist (404). + /// An operation response object containing the result of the operation. + procedure ItemExists(ItemId: Text; var Exists: Boolean): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + HttpStatusCode: Integer; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + Exists := false; + exit(SharePointGraphResponse); + end; + + // Try to get the item + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveItemByIdEndpoint(ItemId), JsonResponse) then begin + HttpStatusCode := SharePointGraphRequestHelper.GetDiagnostics().GetHttpStatusCode(); + + // 404 means item doesn't exist - this is a successful check, just with a negative result + if HttpStatusCode = 404 then begin + Exists := false; + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + // For other errors (401, 403, 429, 500, etc.), return error response + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDriveItemErr, SharePointGraphRequestHelper.GetDiagnostics().GetErrorMessage())); + Exists := false; + exit(SharePointGraphResponse); + end; + + // Item exists + Exists := true; + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Checks if a drive item (file or folder) exists by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// True if the item exists, false if it doesn't exist (404). + /// An operation response object containing the result of the operation. + procedure ItemExistsByPath(ItemPath: Text; var Exists: Boolean): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + JsonResponse: JsonObject; + HttpStatusCode: Integer; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemPath = '' then begin + SharePointGraphResponse.SetError(InvalidItemPathErr); + Exists := false; + exit(SharePointGraphResponse); + end; + + // Remove leading slash if present + ItemPath := ItemPath.TrimStart('/'); + + // Try to get the item + if not SharePointGraphRequestHelper.Get(SharePointGraphUriBuilder.GetDriveItemByPathEndpoint(ItemPath), JsonResponse) then begin + HttpStatusCode := SharePointGraphRequestHelper.GetDiagnostics().GetHttpStatusCode(); + + // 404 means item doesn't exist - this is a successful check, just with a negative result + if HttpStatusCode = 404 then begin + Exists := false; + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + // For other errors (401, 403, 429, 500, etc.), return error response + SharePointGraphResponse.SetError(StrSubstNo(FailedToRetrieveDriveItemByPathErr, SharePointGraphRequestHelper.GetDiagnostics().GetErrorMessage())); + Exists := false; + exit(SharePointGraphResponse); + end; + + // Item exists + Exists := true; + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Copies a drive item (file or folder) to a new location by ID. + /// + /// ID of the item to copy. + /// ID of the target folder. + /// New name for the copied item (optional). + /// An operation response object containing the result of the operation. + procedure CopyItem(ItemId: Text; TargetFolderId: Text; NewName: Text): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + RequestBody: JsonObject; + ParentReference: JsonObject; + ResponseJson: JsonObject; + CopyEndpoint: Text; + CopyItemEndpointLbl: Label '/sites/%1/drive/items/%2/copy', Locked = true; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + if TargetFolderId = '' then begin + SharePointGraphResponse.SetError(InvalidTargetPathErr); + exit(SharePointGraphResponse); + end; + + // Build the copy endpoint + CopyEndpoint := StrSubstNo(CopyItemEndpointLbl, SiteId, ItemId); + + // Build request body + ParentReference.Add('driveId', DefaultDriveId); + ParentReference.Add('id', TargetFolderId); + RequestBody.Add('parentReference', ParentReference); + + if NewName <> '' then + RequestBody.Add('name', NewName); + + // Make the POST request (copy is asynchronous - returns 202 Accepted) + if not SharePointGraphRequestHelper.Post(CopyEndpoint, RequestBody, ResponseJson) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToCopyItemErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Copies a drive item (file or folder) to a new location by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Path to the target folder (e.g., 'Documents/Archive'). + /// New name for the copied item (optional). + /// An operation response object containing the result of the operation. + procedure CopyItemByPath(ItemPath: Text; TargetFolderPath: Text; NewName: Text): Codeunit "SharePoint Graph Response" + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + TargetFolderItem: Record "SharePoint Graph Drive Item"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemPath = '' then begin + SharePointGraphResponse.SetError(InvalidFilePathErr); + exit(SharePointGraphResponse); + end; + + if TargetFolderPath = '' then begin + SharePointGraphResponse.SetError(InvalidTargetPathErr); + exit(SharePointGraphResponse); + end; + + // Get the target folder ID first + SharePointGraphResponse := GetDriveItemByPath(TargetFolderPath, TargetFolderItem); + if not SharePointGraphResponse.IsSuccessful() then + exit(SharePointGraphResponse); + + // Get the source item ID + SharePointGraphResponse := GetDriveItemByPath(ItemPath, GraphDriveItem); + if not SharePointGraphResponse.IsSuccessful() then + exit(SharePointGraphResponse); + + // Now call CopyItem with IDs + exit(CopyItem(GraphDriveItem.Id, TargetFolderItem.Id, NewName)); + end; + + /// + /// Moves a drive item (file or folder) to a new location by ID. + /// + /// ID of the item to move. + /// ID of the target folder (optional). + /// New name for the moved item (optional). + /// An operation response object containing the result of the operation. + procedure MoveItem(ItemId: Text; TargetFolderId: Text; NewName: Text): Codeunit "SharePoint Graph Response" + var + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + RequestBody: JsonObject; + ParentReference: JsonObject; + ResponseJson: JsonObject; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemId = '' then begin + SharePointGraphResponse.SetError(InvalidItemIdErr); + exit(SharePointGraphResponse); + end; + + // At least one of target folder or new name must be provided + if (TargetFolderId = '') and (NewName = '') then begin + SharePointGraphResponse.SetError(InvalidTargetPathErr); + exit(SharePointGraphResponse); + end; + + // Build request body for PATCH + // Move is done by updating the parentReference and/or name + if TargetFolderId <> '' then begin + ParentReference.Add('id', TargetFolderId); + RequestBody.Add('parentReference', ParentReference); + end; + + if NewName <> '' then + RequestBody.Add('name', NewName); + + // Make the PATCH request + if not SharePointGraphRequestHelper.Patch(SharePointGraphUriBuilder.GetDriveItemByIdEndpoint(ItemId), RequestBody, ResponseJson) then begin + SharePointGraphResponse.SetError(StrSubstNo(FailedToMoveItemErr, + SharePointGraphRequestHelper.GetDiagnostics().GetResponseReasonPhrase())); + exit(SharePointGraphResponse); + end; + + SharePointGraphResponse.SetSuccess(); + exit(SharePointGraphResponse); + end; + + /// + /// Moves a drive item (file or folder) to a new location by path. + /// + /// Path to the item (e.g., 'Documents/file.docx'). + /// Path to the target folder (optional). + /// New name for the moved item (optional). + /// An operation response object containing the result of the operation. + procedure MoveItemByPath(ItemPath: Text; TargetFolderPath: Text; NewName: Text): Codeunit "SharePoint Graph Response" + var + GraphDriveItem: Record "SharePoint Graph Drive Item"; + TargetFolderItem: Record "SharePoint Graph Drive Item"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + TargetFolderId: Text; + begin + EnsureInitialized(); + EnsureSiteId(); + EnsureDefaultDriveId(); + + SharePointGraphResponse.SetRequestHelper(SharePointGraphRequestHelper); + + // Validate input + if ItemPath = '' then begin + SharePointGraphResponse.SetError(InvalidFilePathErr); + exit(SharePointGraphResponse); + end; + + // At least one of target folder or new name must be provided + if (TargetFolderPath = '') and (NewName = '') then begin + SharePointGraphResponse.SetError(InvalidTargetPathErr); + exit(SharePointGraphResponse); + end; + + // Get the source item ID + SharePointGraphResponse := GetDriveItemByPath(ItemPath, GraphDriveItem); + if not SharePointGraphResponse.IsSuccessful() then + exit(SharePointGraphResponse); + + // Get the target folder ID if provided + if TargetFolderPath <> '' then begin + SharePointGraphResponse := GetDriveItemByPath(TargetFolderPath, TargetFolderItem); + if not SharePointGraphResponse.IsSuccessful() then + exit(SharePointGraphResponse); + TargetFolderId := TargetFolderItem.Id; + end; + + // Now call MoveItem with IDs + exit(MoveItem(GraphDriveItem.Id, TargetFolderId, NewName)); + end; + + #endregion + + /// + /// Returns detailed information on last API call. + /// + /// Codeunit holding http response status, reason phrase, headers and possible error information for the last API call + procedure GetDiagnostics(): Interface "HTTP Diagnostics" + begin + exit(SharePointGraphRequestHelper.GetDiagnostics()); + end; + + /// + /// Sets the site ID directly for testing purposes. + /// + /// The site ID to set. + internal procedure SetSiteIdForTesting(NewSiteId: Text) + begin + SiteId := NewSiteId; + SharePointGraphUriBuilder.Initialize(SiteId, SharePointGraphRequestHelper); + end; + + /// + /// Sets the default drive ID directly for testing purposes. + /// + /// The default drive ID to set. + internal procedure SetDefaultDriveIdForTesting(NewDefaultDriveId: Text) + begin + DefaultDriveId := NewDefaultDriveId; + end; + + // Add this method to lazily load the default drive ID + local procedure EnsureDefaultDriveId() + begin + if DefaultDriveId = '' then + GetDefaultDriveId(); + end; + + // Add method to lazily load the site ID + local procedure EnsureSiteId() + begin + if SiteId = '' then begin + GetSiteIdFromUrl(SharePointUrl); + SharePointGraphUriBuilder.Initialize(SiteId, SharePointGraphRequestHelper); + end; + end; +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphParser.Codeunit.al b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphParser.Codeunit.al new file mode 100644 index 0000000000..aec1e911f1 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphParser.Codeunit.al @@ -0,0 +1,425 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Provides functionality for parsing Microsoft Graph API responses for SharePoint. +/// +codeunit 9122 "SharePoint Graph Parser" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Extracts the next page link from a paginated response. + /// + /// The JSON response that might contain a next link. + /// The extracted next link if available. + /// True if a next link was found; otherwise false. + procedure ExtractNextLink(JsonResponse: JsonObject; var NextLink: Text): Boolean + var + JsonToken: JsonToken; + begin + if not JsonResponse.Get('@odata.nextLink', JsonToken) then + exit(false); + + NextLink := JsonToken.AsValue().AsText(); + exit(true); + end; + + /// + /// Parses a JSON response into a collection of SharePoint Graph List records. + /// + /// The JSON response to parse. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListCollection(JsonResponse: JsonObject; var GraphLists: Record "SharePoint Graph List" temporary): Boolean + var + JsonArray: JsonArray; + JsonToken: JsonToken; + begin + if not JsonResponse.Get('value', JsonToken) then + exit(false); + + if not JsonToken.IsArray() then + exit(false); + + JsonArray := JsonToken.AsArray(); + exit(ParseListCollection(JsonArray, GraphLists)); + end; + + /// + /// Parses a JSON array into a collection of SharePoint Graph List records. + /// + /// The JSON array to parse. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListCollection(JsonArray: JsonArray; var GraphLists: Record "SharePoint Graph List" temporary): Boolean + var + JsonToken: JsonToken; + JsonListObject: JsonObject; + begin + foreach JsonToken in JsonArray do begin + JsonListObject := JsonToken.AsObject(); + + GraphLists.Init(); + if ParseListItem(JsonListObject, GraphLists) then + GraphLists.Insert(); + end; + + exit(true); + end; + + /// + /// Parses a JSON object into a SharePoint Graph List record. + /// + /// The JSON object to parse. + /// The record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListItem(JsonListObject: JsonObject; var GraphList: Record "SharePoint Graph List" temporary): Boolean + var + JsonToken: JsonToken; + DtToken: JsonToken; + begin + if not JsonListObject.Get('id', JsonToken) then + exit(false); + + GraphList.Id := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphList.Id)); + + if JsonListObject.Get('displayName', JsonToken) then + GraphList.DisplayName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphList.DisplayName)); + + if JsonListObject.Get('name', JsonToken) then + GraphList.Name := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphList.Name)); + + if JsonListObject.Get('description', JsonToken) then + GraphList.Description := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphList.Description)); + + if JsonListObject.Get('webUrl', JsonToken) then + GraphList.WebUrl := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphList.WebUrl)); + + if JsonListObject.Get('list', JsonToken) then + if JsonToken.IsObject() then + if JsonToken.AsObject().Get('template', DtToken) then + GraphList.Template := CopyStr(DtToken.AsValue().AsText(), 1, MaxStrLen(GraphList.Template)); + + if JsonListObject.Get('drive', JsonToken) then + if JsonToken.IsObject() then + if JsonToken.AsObject().Get('id', DtToken) then + GraphList.DriveId := CopyStr(DtToken.AsValue().AsText(), 1, MaxStrLen(GraphList.DriveId)); + + if JsonListObject.Get('createdDateTime', JsonToken) then + GraphList.CreatedDateTime := JsonToken.AsValue().AsDateTime(); + + if JsonListObject.Get('lastModifiedDateTime', JsonToken) then + GraphList.LastModifiedDateTime := JsonToken.AsValue().AsDateTime(); + + exit(true); + end; + + /// + /// Parses a JSON response into a collection of SharePoint Graph List Item records. + /// + /// The JSON response to parse. + /// The ID of the list the items belong to. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListItemCollection(JsonResponse: JsonObject; ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary): Boolean + var + JsonArray: JsonArray; + JsonToken: JsonToken; + begin + if not JsonResponse.Get('value', JsonToken) then + exit(false); + + if not JsonToken.IsArray() then + exit(false); + + JsonArray := JsonToken.AsArray(); + exit(ParseListItemCollection(JsonArray, ListId, GraphListItems)); + end; + + /// + /// Parses a JSON array into a collection of SharePoint Graph List Item records. + /// + /// The JSON array to parse. + /// The ID of the list the items belong to. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListItemCollection(JsonArray: JsonArray; ListId: Text; var GraphListItems: Record "SharePoint Graph List Item" temporary): Boolean + var + JsonToken: JsonToken; + JsonItemObject: JsonObject; + begin + foreach JsonToken in JsonArray do begin + JsonItemObject := JsonToken.AsObject(); + + GraphListItems.Init(); + GraphListItems.ListId := CopyStr(ListId, 1, MaxStrLen(GraphListItems.Id)); + if ParseListItemDetail(JsonItemObject, GraphListItems) then + GraphListItems.Insert(); + end; + + exit(true); + end; + + /// + /// Parses a JSON object into a SharePoint Graph List Item record. + /// + /// The JSON object to parse. + /// The record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseListItemDetail(JsonItemObject: JsonObject; var GraphListItem: Record "SharePoint Graph List Item" temporary): Boolean + var + JsonToken: JsonToken; + FieldsJsonObject: JsonObject; + begin + if not JsonItemObject.Get('id', JsonToken) then + exit(false); + + GraphListItem.Id := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphListItem.Id)); + + if JsonItemObject.Get('contentType', JsonToken) then + if JsonToken.IsObject() then + if JsonToken.AsObject().Get('name', JsonToken) then + GraphListItem.ContentType := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphListItem.ContentType)); + + if JsonItemObject.Get('webUrl', JsonToken) then + GraphListItem.WebUrl := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphListItem.WebUrl)); + + if JsonItemObject.Get('createdDateTime', JsonToken) then + GraphListItem.CreatedDateTime := JsonToken.AsValue().AsDateTime(); + + if JsonItemObject.Get('lastModifiedDateTime', JsonToken) then + GraphListItem.LastModifiedDateTime := JsonToken.AsValue().AsDateTime(); + + // Extract fields from fields property + if JsonItemObject.Get('fields', JsonToken) then begin + FieldsJsonObject := JsonToken.AsObject(); + + // Extract title specifically + if FieldsJsonObject.Get('Title', JsonToken) then + GraphListItem.Title := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphListItem.Title)); + + // Store all fields as JSON + GraphListItem.SetFieldsJson(FieldsJsonObject); + end; + + exit(true); + end; + + /// + /// Parses a JSON response into a collection of SharePoint Graph Drive Item records. + /// + /// The JSON response to parse. + /// The ID of the drive the items belong to. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveItemCollection(JsonResponse: JsonObject; DriveId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Boolean + var + JsonArray: JsonArray; + JsonToken: JsonToken; + begin + if not JsonResponse.Get('value', JsonToken) then + exit(false); + + if not JsonToken.IsArray() then + exit(false); + + JsonArray := JsonToken.AsArray(); + exit(ParseDriveItemCollection(JsonArray, DriveId, GraphDriveItems)); + end; + + /// + /// Parses a JSON array into a collection of SharePoint Graph Drive Item records. + /// + /// The JSON array to parse. + /// The ID of the drive the items belong to. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveItemCollection(JsonArray: JsonArray; DriveId: Text; var GraphDriveItems: Record "SharePoint Graph Drive Item" temporary): Boolean + var + JsonToken: JsonToken; + JsonItemObject: JsonObject; + begin + foreach JsonToken in JsonArray do begin + JsonItemObject := JsonToken.AsObject(); + + GraphDriveItems.Init(); + GraphDriveItems.DriveId := CopyStr(DriveId, 1, MaxStrLen(GraphDriveItems.DriveId)); + if ParseDriveItemDetail(JsonItemObject, GraphDriveItems) then + GraphDriveItems.Insert(); + end; + + exit(true); + end; + + /// + /// Parses a JSON object into a SharePoint Graph Drive Item record. + /// + /// The JSON object to parse. + /// The record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveItemDetail(JsonItemObject: JsonObject; var GraphDriveItem: Record "SharePoint Graph Drive Item" temporary): Boolean + var + JsonToken: JsonToken; + FileJsonObj: JsonObject; + ParentRefJsonObj: JsonObject; + begin + if not JsonItemObject.Get('id', JsonToken) then + exit(false); + + GraphDriveItem.Id := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.Id)); + + if JsonItemObject.Get('name', JsonToken) then + GraphDriveItem.Name := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.Name)); + + // Check if item is a folder + GraphDriveItem.IsFolder := JsonItemObject.Contains('folder'); + + // Get file type if it's a file + if JsonItemObject.Get('file', JsonToken) and JsonToken.IsObject() then begin + FileJsonObj := JsonToken.AsObject(); + if FileJsonObj.Get('mimeType', JsonToken) then + GraphDriveItem.FileType := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.FileType)); + end; + + // Get parent reference + if JsonItemObject.Get('parentReference', JsonToken) and JsonToken.IsObject() then begin + ParentRefJsonObj := JsonToken.AsObject(); + if ParentRefJsonObj.Get('id', JsonToken) then + GraphDriveItem.ParentId := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.ParentId)); + + if ParentRefJsonObj.Get('path', JsonToken) then + GraphDriveItem.Path := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.Path)); + end; + + if JsonItemObject.Get('webUrl', JsonToken) then + GraphDriveItem.WebUrl := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.WebUrl)); + + if JsonItemObject.Get('@microsoft.graph.downloadUrl', JsonToken) then + GraphDriveItem.DownloadUrl := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDriveItem.DownloadUrl)); + + if JsonItemObject.Get('createdDateTime', JsonToken) then + GraphDriveItem.CreatedDateTime := JsonToken.AsValue().AsDateTime(); + + if JsonItemObject.Get('lastModifiedDateTime', JsonToken) then + GraphDriveItem.LastModifiedDateTime := JsonToken.AsValue().AsDateTime(); + + if JsonItemObject.Get('size', JsonToken) then + GraphDriveItem.Size := JsonToken.AsValue().AsBigInteger(); + + exit(true); + end; + + /// + /// Parses a JSON response into a collection of SharePoint Graph Drive records. + /// + /// The JSON response to parse. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveCollection(JsonResponse: JsonObject; var GraphDrives: Record "SharePoint Graph Drive" temporary): Boolean + var + JsonArray: JsonArray; + JsonToken: JsonToken; + begin + if not JsonResponse.Get('value', JsonToken) then + exit(false); + + if not JsonToken.IsArray() then + exit(false); + + JsonArray := JsonToken.AsArray(); + exit(ParseDriveCollection(JsonArray, GraphDrives)); + end; + + /// + /// Parses a JSON array into a collection of SharePoint Graph Drive records. + /// + /// The JSON array to parse. + /// The temporary record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveCollection(JsonArray: JsonArray; var GraphDrives: Record "SharePoint Graph Drive" temporary): Boolean + var + JsonToken: JsonToken; + JsonDriveObject: JsonObject; + begin + foreach JsonToken in JsonArray do begin + JsonDriveObject := JsonToken.AsObject(); + + GraphDrives.Init(); + if ParseDriveDetail(JsonDriveObject, GraphDrives) then + GraphDrives.Insert(); + end; + + exit(true); + end; + + /// + /// Parses a JSON object into a SharePoint Graph Drive record. + /// + /// The JSON object to parse. + /// The record to populate. + /// True if successfully parsed; otherwise false. + procedure ParseDriveDetail(JsonDriveObject: JsonObject; var GraphDrive: Record "SharePoint Graph Drive" temporary): Boolean + var + JsonToken: JsonToken; + OwnerJsonObj: JsonObject; + UserJsonObj: JsonObject; + QuotaJsonObj: JsonObject; + begin + if not JsonDriveObject.Get('id', JsonToken) then + exit(false); + + GraphDrive.Id := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.Id)); + + if JsonDriveObject.Get('name', JsonToken) then + GraphDrive.Name := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.Name)); + + if JsonDriveObject.Get('driveType', JsonToken) then + GraphDrive.DriveType := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.DriveType)); + + if JsonDriveObject.Get('description', JsonToken) then + GraphDrive.Description := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.Description)); + + if JsonDriveObject.Get('webUrl', JsonToken) then + GraphDrive.WebUrl := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.WebUrl)); + + if JsonDriveObject.Get('createdDateTime', JsonToken) then + GraphDrive.CreatedDateTime := JsonToken.AsValue().AsDateTime(); + + if JsonDriveObject.Get('lastModifiedDateTime', JsonToken) then + GraphDrive.LastModifiedDateTime := JsonToken.AsValue().AsDateTime(); + + // Get owner information + if JsonDriveObject.Get('owner', JsonToken) and JsonToken.IsObject() then begin + OwnerJsonObj := JsonToken.AsObject(); + if OwnerJsonObj.Get('user', JsonToken) and JsonToken.IsObject() then begin + UserJsonObj := JsonToken.AsObject(); + if UserJsonObj.Get('displayName', JsonToken) then + GraphDrive.OwnerName := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.OwnerName)); + if UserJsonObj.Get('email', JsonToken) then + GraphDrive.OwnerEmail := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.OwnerEmail)); + end; + end; + + // Get quota information + if JsonDriveObject.Get('quota', JsonToken) and JsonToken.IsObject() then begin + QuotaJsonObj := JsonToken.AsObject(); + if QuotaJsonObj.Get('total', JsonToken) then + GraphDrive.QuotaTotal := JsonToken.AsValue().AsBigInteger(); + if QuotaJsonObj.Get('used', JsonToken) then + GraphDrive.QuotaUsed := JsonToken.AsValue().AsBigInteger(); + if QuotaJsonObj.Get('remaining', JsonToken) then + GraphDrive.QuotaRemaining := JsonToken.AsValue().AsBigInteger(); + if QuotaJsonObj.Get('state', JsonToken) then + GraphDrive.QuotaState := CopyStr(JsonToken.AsValue().AsText(), 1, MaxStrLen(GraphDrive.QuotaState)); + end; + + exit(true); + end; +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphReqHelper.Codeunit.al b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphReqHelper.Codeunit.al new file mode 100644 index 0000000000..a665f83076 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphReqHelper.Codeunit.al @@ -0,0 +1,635 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Graph.Authorization; +using System.RestClient; +using System.Utilities; + +/// +/// Provides functionality for making requests to the Microsoft Graph API for SharePoint. +/// +codeunit 9123 "SharePoint Graph Req. Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + GraphClient: Codeunit "Graph Client"; + SharePointDiagnostics: Codeunit "SharePoint Diagnostics"; + ApiVersion: Enum "Graph API Version"; + CustomBaseUrl: Text; + MicrosoftGraphDefaultBaseUrlLbl: Label 'https://graph.microsoft.com/%1', Comment = '%1 = Graph API Version', Locked = true; + RangeHeaderLbl: Label 'bytes=%1-%2', Locked = true; + + /// + /// Initializes the Graph Request Helper with an authorization. + /// + /// The Graph API authorization to use. + procedure Initialize(GraphAuthorization: Interface "Graph Authorization") + begin + ApiVersion := Enum::"Graph API Version"::"v1.0"; + CustomBaseUrl := ''; + GraphClient.Initialize(ApiVersion, GraphAuthorization); + end; + + /// + /// Initializes the Graph Request Helper with a specific API version and authorization. + /// + /// The Graph API version to use. + /// The Graph API authorization to use. + procedure Initialize(NewApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization") + begin + ApiVersion := NewApiVersion; + CustomBaseUrl := ''; + GraphClient.Initialize(NewApiVersion, GraphAuthorization); + end; + + /// + /// Initializes the Graph Request Helper with a custom base URL and authorization. + /// + /// The custom base URL to use. + /// The Graph API authorization to use. + procedure Initialize(NewBaseUrl: Text; GraphAuthorization: Interface "Graph Authorization") + begin + ApiVersion := Enum::"Graph API Version"::"v1.0"; + CustomBaseUrl := NewBaseUrl; + GraphClient.Initialize(ApiVersion, GraphAuthorization); + end; + + /// + /// Initializes the Graph Request Helper with an HTTP client handler for testing. + /// + /// The Graph API version to use. + /// The Graph API authorization to use. + /// HTTP client handler for intercepting requests. + procedure Initialize(NewApiVersion: Enum "Graph API Version"; GraphAuthorization: Interface "Graph Authorization"; HttpClientHandler: Interface "Http Client Handler") + begin + ApiVersion := NewApiVersion; + CustomBaseUrl := ''; + GraphClient.Initialize(NewApiVersion, GraphAuthorization, HttpClientHandler); + end; + + /// + /// Gets the base URL for Graph API calls. + /// + /// The base URL for Graph API requests. + procedure GetGraphApiBaseUrl(): Text + begin + if CustomBaseUrl <> '' then + exit(CustomBaseUrl); + + exit(StrSubstNo(MicrosoftGraphDefaultBaseUrlLbl, ApiVersion)); + end; + + #region GET Requests + + /// + /// Makes a GET request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The JSON response. + /// Optional parameters for the request. + /// True if the request was successful; otherwise false. + procedure Get(Endpoint: Text; var ResponseJson: JsonObject; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + FinalEndpoint: Text; + begin + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Get(FinalEndpoint, GraphOptionalParameters, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + /// + /// Makes a GET request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Get(Endpoint: Text; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(Get(Endpoint, ResponseJson, GraphOptionalParameters)); + end; + + /// + /// Downloads a file from Microsoft Graph API. + /// + /// The endpoint to request. + /// The blob to write the file content to. + /// True if the request was successful; otherwise false. + procedure DownloadFile(Endpoint: Text; var TempBlob: Codeunit "Temp Blob"): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(DownloadFile(Endpoint, TempBlob, GraphOptionalParameters)); + end; + + /// + /// Downloads a file from Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The blob to write the file content to. + /// Optional parameters for the request. + /// True if the request was successful; otherwise false. + procedure DownloadFile(Endpoint: Text; var TempBlob: Codeunit "Temp Blob"; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + FinalEndpoint: Text; + begin + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Get(FinalEndpoint, GraphOptionalParameters, HttpResponseMessage); + exit(ProcessStreamResponse(HttpResponseMessage, TempBlob)); + end; + + /// + /// Downloads a chunk of a file using HTTP Range header. + /// + /// The endpoint to request. + /// Starting byte position (0-based, inclusive). + /// Ending byte position (0-based, inclusive). + /// The blob to receive the chunk content. + /// True if the chunk was downloaded successfully; otherwise false. + procedure DownloadChunk(Endpoint: Text; RangeStart: BigInteger; RangeEnd: BigInteger; var TempBlob: Codeunit "Temp Blob"): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + FinalEndpoint: Text; + RangeHeader: Text; + begin + // Set Range header: "bytes=0-104857599" + RangeHeader := StrSubstNo(RangeHeaderLbl, RangeStart, RangeEnd); + GraphOptionalParameters.SetRequestHeader(Enum::"Graph Request Header"::Range, RangeHeader); + + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Get(FinalEndpoint, GraphOptionalParameters, HttpResponseMessage); + + // Graph API should return 206 Partial Content for Range requests + // But we'll accept both 200 (full content) and 206 (partial content) + SharePointDiagnostics.SetParameters(HttpResponseMessage.GetIsSuccessStatusCode(), + HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase(), + TryGetRetryAfterHeaderValue(HttpResponseMessage), HttpResponseMessage.GetErrorMessage()); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + exit(false); + + exit(ProcessStreamResponse(HttpResponseMessage, TempBlob)); + end; + + #endregion + + #region POST Requests + + /// + /// Makes a POST request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The request body. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Post(Endpoint: Text; RequestBody: JsonObject; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(Post(Endpoint, RequestBody, GraphOptionalParameters, ResponseJson)); + end; + + /// + /// Makes a POST request to the Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The request body. + /// Optional parameters for the request. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Post(Endpoint: Text; RequestBody: JsonObject; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var ResponseJson: JsonObject): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + FinalEndpoint: Text; + begin + HttpContent.Create(RequestBody); + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Post(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + #endregion + + #region File Upload + + /// + /// Uploads a file to Microsoft Graph API. + /// + /// The endpoint to request. + /// The stream containing the file content. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure UploadFile(Endpoint: Text; FileInStream: InStream; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(UploadFile(Endpoint, FileInStream, GraphOptionalParameters, ResponseJson)); + end; + + /// + /// Uploads a file to Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The stream containing the file content. + /// Optional parameters for the request. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure UploadFile(Endpoint: Text; FileInStream: InStream; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var ResponseJson: JsonObject): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + FinalEndpoint: Text; + begin + HttpContent.Create(FileInStream); + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Put(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + #endregion + + #region Chunked File Upload + + /// + /// Creates an upload session for chunked file upload. + /// + /// The endpoint to create the upload session. + /// Name of the file to upload. + /// Size of the file in bytes. + /// Optional parameters for the request. + /// How to handle conflicts if a file with the same name exists. + /// The upload URL result for the session. + /// True if the upload session was created successfully; otherwise false. + procedure CreateUploadSession(Endpoint: Text; FileName: Text; FileSize: Integer; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; GraphConflictBehavior: Enum "Graph ConflictBehavior"; var UploadUrlResult: Text): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + RequestBodyJson: JsonObject; + ItemJson: JsonObject; + ResponseJson: JsonObject; + JsonToken: JsonToken; + FinalEndpoint: Text; + begin + // Create request body for upload session + ItemJson.Add('@microsoft.graph.conflictBehavior', Format(GraphConflictBehavior)); + ItemJson.Add('name', FileName); + // Can also add description or fileSystemInfo here if needed + RequestBodyJson.Add('item', ItemJson); + + HttpContent.Create(RequestBodyJson); + FinalEndpoint := PrepareEndpoint(Endpoint + ':/createUploadSession', GraphOptionalParameters); + GraphClient.Post(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + + if not ProcessJsonResponse(HttpResponseMessage, ResponseJson) then + exit(false); + + // Extract uploadUrl from the response + if ResponseJson.Get('uploadUrl', JsonToken) then + UploadUrlResult := JsonToken.AsValue().AsText() + else + exit(false); + + exit(true); + end; + + /// + /// Uploads a chunk of file content to an upload session. + /// + /// The upload URL for the session. + /// The content of the chunk. + /// The content range header value (e.g., "bytes 0-1023/5000"). + /// The JSON response. + /// True if the chunk was uploaded successfully; otherwise false. + procedure UploadChunk(UploadUrl: Text; var ChunkContent: InStream; ContentRange: Text; var ResponseJson: JsonObject): Boolean + var + RestClient: Codeunit "Rest Client"; + HttpContent: Codeunit "Http Content"; + HttpResponseMessage: Codeunit "Http Response Message"; + begin + // Important: For upload sessions, we don't use GraphClient + // because the upload URL is a complete URL and we shouldn't send the Authorization header + Clear(ResponseJson); + + // Initialize a fresh RestClient without passing any authorization + RestClient.Initialize(); + + // Create the HTTP content with our chunk + HttpContent.Create(ChunkContent); + HttpContent.SetHeader('Content-Range', ContentRange); + HttpContent.SetContentTypeHeader('application/octet-stream'); + + // Use direct PUT method on the upload URL + // The UploadUrl is already a complete URL from the upload session + HttpResponseMessage := RestClient.Put(UploadUrl, HttpContent); + + SharePointDiagnostics.SetParameters(HttpResponseMessage.GetIsSuccessStatusCode(), + HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase(), + TryGetRetryAfterHeaderValue(HttpResponseMessage), HttpResponseMessage.GetErrorMessage()); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + exit(false); + + ResponseJson := HttpResponseMessage.GetContent().AsJsonObject(); + + exit(true); + end; + + #endregion + + #region PUT Requests + + /// + /// Makes a PUT request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The request body. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Put(Endpoint: Text; RequestBody: JsonObject; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(Put(Endpoint, RequestBody, GraphOptionalParameters, ResponseJson)); + end; + + /// + /// Makes a PUT request to the Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The request body. + /// Optional parameters for the request. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Put(Endpoint: Text; RequestBody: JsonObject; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var ResponseJson: JsonObject): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + FinalEndpoint: Text; + begin + HttpContent.Create(RequestBody); + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Put(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + /// + /// Makes a PUT request with binary content and custom headers to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The binary content stream. + /// The content type of the binary data. + /// Dictionary of additional headers to include. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure PutContent(Endpoint: Text; Content: InStream; ContentType: Text; var AdditionalHeaders: Dictionary of [Text, Text]; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(PutContent(Endpoint, Content, ContentType, AdditionalHeaders, GraphOptionalParameters, false, ResponseJson)); + end; + + /// + /// Makes a PUT request with binary content and custom headers to the Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The binary content stream. + /// The content type of the binary data. + /// Dictionary of additional headers to include. + /// Optional parameters for the request. + /// If true, the endpoint is treated as a complete URL and not processed further. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure PutContent(Endpoint: Text; Content: InStream; ContentType: Text; var AdditionalHeaders: Dictionary of [Text, Text]; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; IsCompleteUrl: Boolean; var ResponseJson: JsonObject): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + FinalEndpoint: Text; + HeaderKey: Text; + begin + HttpContent.Create(Content); + HttpContent.SetContentTypeHeader(ContentType); + + // Add any additional headers + foreach HeaderKey in AdditionalHeaders.Keys() do + HttpContent.SetHeader(HeaderKey, AdditionalHeaders.Get(HeaderKey)); + + if IsCompleteUrl then + FinalEndpoint := Endpoint + else + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + + GraphClient.Put(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + #endregion + + #region PATCH Requests + + /// + /// Makes a PATCH request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// The request body. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Patch(Endpoint: Text; RequestBody: JsonObject; var ResponseJson: JsonObject): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(Patch(Endpoint, RequestBody, GraphOptionalParameters, ResponseJson)); + end; + + /// + /// Makes a PATCH request to the Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// The request body. + /// Optional parameters for the request. + /// The JSON response. + /// True if the request was successful; otherwise false. + procedure Patch(Endpoint: Text; RequestBody: JsonObject; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var ResponseJson: JsonObject): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + FinalEndpoint: Text; + begin + HttpContent.Create(RequestBody); + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Patch(FinalEndpoint, GraphOptionalParameters, HttpContent, HttpResponseMessage); + exit(ProcessJsonResponse(HttpResponseMessage, ResponseJson)); + end; + + #endregion + + #region DELETE Requests + + /// + /// Makes a DELETE request to the Microsoft Graph API. + /// + /// The endpoint to request. + /// True if the request was successful; otherwise false. + procedure Delete(Endpoint: Text): Boolean + var + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + begin + exit(Delete(Endpoint, GraphOptionalParameters)); + end; + + /// + /// Makes a DELETE request to the Microsoft Graph API with optional parameters. + /// + /// The endpoint to request. + /// Optional parameters for the request. + /// True if the request was successful; otherwise false. + procedure Delete(Endpoint: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + FinalEndpoint: Text; + begin + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + GraphClient.Delete(FinalEndpoint, GraphOptionalParameters, HttpResponseMessage); + exit(ProcessResponse(HttpResponseMessage)); + end; + + #endregion + + #region Pagination + + /// + /// Makes a GET request to the Microsoft Graph API with pagination support and returns all pages automatically. + /// + /// The endpoint to request. + /// Optional parameters for the request. + /// The JSON array containing all results from all pages. + /// True if the request was successful; otherwise false. + procedure GetAllPages(Endpoint: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var JsonArray: JsonArray): Boolean + var + HttpResponseMessage: Codeunit "Http Response Message"; + FinalEndpoint: Text; + begin + FinalEndpoint := PrepareEndpoint(Endpoint, GraphOptionalParameters); + + if not GraphClient.GetAllPages(FinalEndpoint, GraphOptionalParameters, HttpResponseMessage, JsonArray) then begin + SharePointDiagnostics.SetParameters(HttpResponseMessage.GetIsSuccessStatusCode(), + HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase(), + TryGetRetryAfterHeaderValue(HttpResponseMessage), HttpResponseMessage.GetErrorMessage()); + exit(false); + end; + + SharePointDiagnostics.SetParameters(HttpResponseMessage.GetIsSuccessStatusCode(), + HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase(), + TryGetRetryAfterHeaderValue(HttpResponseMessage), HttpResponseMessage.GetErrorMessage()); + + exit(true); + end; + + #endregion + + #region Helpers + + /// + /// Configures conflict behavior in optional parameters + /// + /// Optional parameters to configure + /// The desired conflict behavior + procedure ConfigureConflictBehavior(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; ConflictBehavior: Enum "Graph ConflictBehavior") + begin + GraphOptionalParameters.SetMicrosftGraphConflictBehavior(ConflictBehavior); + end; + + /// + /// Returns detailed information on last API call. + /// + /// Codeunit holding http response status, reason phrase, headers and possible error information for the last API call + procedure GetDiagnostics(): Interface "HTTP Diagnostics" + begin + exit(SharePointDiagnostics); + end; + + /// + /// Prepares the endpoint for a request by adding optional parameters. + /// + /// The base endpoint. + /// Optional parameters to add to the endpoint. + /// The final endpoint with optional parameters. + local procedure PrepareEndpoint(Endpoint: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Text + var + SharePointGraphUriBuilder: Codeunit "Sharepoint Graph Uri Builder"; + begin + exit(SharePointGraphUriBuilder.AddOptionalParametersToEndpoint(Endpoint, GraphOptionalParameters)); + end; + + /// + /// Common response processing - sets diagnostics and checks for success status + /// + /// The HTTP response message to process + /// True if the response is successful; otherwise false + local procedure ProcessResponse(HttpResponseMessage: Codeunit "Http Response Message"): Boolean + begin + SharePointDiagnostics.SetParameters(HttpResponseMessage.GetIsSuccessStatusCode(), + HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase(), + TryGetRetryAfterHeaderValue(HttpResponseMessage), HttpResponseMessage.GetErrorMessage()); + + exit(HttpResponseMessage.GetIsSuccessStatusCode()); + end; + + /// + /// Processes an HTTP response and extracts the JSON content. + /// + /// The HTTP response message. + /// The JSON response to populate. + /// True if the request was successful; otherwise false. + local procedure ProcessJsonResponse(HttpResponseMessage: Codeunit "Http Response Message"; var ResponseJson: JsonObject): Boolean + begin + if not ProcessResponse(HttpResponseMessage) then + exit(false); + + ResponseJson := HttpResponseMessage.GetContent().AsJsonObject(); + exit(true); + end; + + /// + /// Processes an HTTP response and extracts the stream content. + /// + /// The HTTP response message. + /// The blob to populate with the response content. + /// True if the request was successful; otherwise false. + local procedure ProcessStreamResponse(HttpResponseMessage: Codeunit "Http Response Message"; var TempBlob: Codeunit "Temp Blob"): Boolean + begin + if not ProcessResponse(HttpResponseMessage) then + exit(false); + + TempBlob := HttpResponseMessage.GetContent().AsBlob(); + + exit(true); + end; + + local procedure TryGetRetryAfterHeaderValue(HttpResponseMessage: Codeunit "Http Response Message") RetryAfterAsInteger: Integer + var + Values: array[1] of Text; + begin + if not HttpResponseMessage.GetHeaders().GetValues('Retry-After', Values) then + exit; + + //Since the HTTP Diagnostics interface expects an integer in Retry-After, we must convert the header to a number. + //At the same time, the HTTP specification states that there may be a date there. + if not Evaluate(RetryAfterAsInteger, Values[1]) then + exit(0); + end; + + #endregion +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphResponse.Codeunit.al b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphResponse.Codeunit.al new file mode 100644 index 0000000000..3ae90f61b2 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphResponse.Codeunit.al @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Holder object for SharePoint Graph API operation results. +/// +codeunit 9129 "SharePoint Graph Response" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + var + SharePointGraphRequestHelper: Codeunit "SharePoint Graph Req. Helper"; + IsSuccess: Boolean; + ErrorMessage: Text; + ErrorCallStack: Text; + + /// + /// Checks whether the operation was successful. + /// + /// True if the operation was successful; otherwise - false. + procedure IsSuccessful(): Boolean + begin + exit(IsSuccess); + end; + + /// + /// Gets the error message (if any) of the response. + /// + /// Text representation of the error that occurred during the operation. + procedure GetError(): Text + begin + exit(ErrorMessage); + end; + + /// + /// Gets the call stack at the time of the error. + /// + /// The call stack when the error occurred. + procedure GetErrorCallStack(): Text + begin + exit(ErrorCallStack); + end; + + /// + /// Gets the HTTP diagnostics for the last HTTP request (if any). + /// + /// HTTP diagnostics interface for detailed HTTP response information. + procedure GetHttpDiagnostics(): Interface "HTTP Diagnostics" + begin + exit(SharePointGraphRequestHelper.GetDiagnostics()); + end; + + /// + /// Sets the response as successful. + /// + internal procedure SetSuccess() + begin + IsSuccess := true; + ErrorMessage := ''; + ErrorCallStack := ''; + end; + + /// + /// Sets the response as failed with an error message. + /// + /// The error message to set. + internal procedure SetError(NewErrorMessage: Text) + begin + IsSuccess := false; + ErrorMessage := NewErrorMessage; + ErrorCallStack := SessionInformation.Callstack(); + end; + + /// + /// Sets the request helper for HTTP diagnostics access. + /// + /// The request helper instance. + internal procedure SetRequestHelper(var NewSharePointGraphRequestHelper: Codeunit "SharePoint Graph Req. Helper") + begin + SharePointGraphRequestHelper := NewSharePointGraphRequestHelper; + end; +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphUriBuilder.Codeunit.al b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphUriBuilder.Codeunit.al new file mode 100644 index 0000000000..cb1cc97f80 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/helpers/SharePointGraphUriBuilder.Codeunit.al @@ -0,0 +1,358 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Utilities; + +/// +/// Provides functionality to build URIs for the Microsoft Graph API for SharePoint. +/// +codeunit 9121 "SharePoint Graph Uri Builder" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + SharePointGraphReqHelper: Codeunit "SharePoint Graph Req. Helper"; + SiteId: Text; + SiteLbl: Label '/sites/%1', Locked = true; + ListsLbl: Label '/sites/%1/lists', Locked = true; + ListByIdLbl: Label '/sites/%1/lists/%2', Locked = true; + ListItemsLbl: Label '/sites/%1/lists/%2/items', Locked = true; + CreateListItemLbl: Label '/sites/%1/lists/%2/items', Locked = true; + SiteByHostAndPathLbl: Label '/sites/%1:%2', Locked = true; + DriveLbl: Label '/sites/%1/drive', Locked = true; + DrivesLbl: Label '/sites/%1/drives', Locked = true; + DriveRootLbl: Label '/sites/%1/drive/root', Locked = true; + DriveRootChildrenLbl: Label '/sites/%1/drive/root/children', Locked = true; + DriveRootItemByPathLbl: Label '/sites/%1/drive/root:/%2', Locked = true; + DriveItemByIdLbl: Label '/sites/%1/drive/items/%2', Locked = true; + DriveItemChildrenLbl: Label '/sites/%1/drive/items/%2/children', Locked = true; + DriveItemContentLbl: Label '/sites/%1/drive/items/%2/content', Locked = true; + DriveItemContentByPathLbl: Label '/sites/%1/drive/root:/%2:/content', Locked = true; + DriveItemChildrenByPathLbl: Label '/sites/%1/drive/root:/%2:/children', Locked = true; + SpecificDriveRootLbl: Label '/sites/%1/drives/%2/root', Locked = true; + SpecificDriveRootChildrenLbl: Label '/sites/%1/drives/%2/root/children', Locked = true; + SpecificDriveItemChildrenByPathLbl: Label '/sites/%1/drives/%2/root:/%3:/children', Locked = true; + SpecificDriveItemContentByPathLbl: Label '/sites/%1/drives/%2/root:/%3:/content', Locked = true; + + /// + /// Initializes the Graph URI Builder with a specific request helper. + /// + /// The SharePoint site ID. + /// The SharePoint Graph Request Helper to use. + procedure Initialize(NewSiteId: Text; NewRequestHelper: Codeunit "SharePoint Graph Req. Helper") + begin + SiteId := NewSiteId; + SharePointGraphReqHelper := NewRequestHelper; + end; + + /// + /// Gets the endpoint for getting a site by hostname and path. + /// + /// The hostname (e.g., contoso.sharepoint.com). + /// The relative path (e.g., /sites/Marketing). + /// The endpoint. + procedure GetSiteByHostAndPathEndpoint(HostName: Text; RelativePath: Text): Text + begin + exit(StrSubstNo(SiteByHostAndPathLbl, HostName, RelativePath)); + end; + + /// + /// Gets the endpoint for getting a site. + /// + /// The endpoint. + procedure GetSiteEndpoint(): Text + begin + exit(StrSubstNo(SiteLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting all lists. + /// + /// The endpoint. + procedure GetListsEndpoint(): Text + begin + exit(StrSubstNo(ListsLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting a list by ID. + /// + /// The list ID. + /// The endpoint. + procedure GetListEndpoint(ListId: Text): Text + begin + exit(StrSubstNo(ListByIdLbl, SiteId, ListId)); + end; + + /// + /// Gets the endpoint for getting items in a list. + /// + /// The list ID. + /// The endpoint. + procedure GetListItemsEndpoint(ListId: Text): Text + begin + exit(StrSubstNo(ListItemsLbl, SiteId, ListId)); + end; + + /// + /// Gets the endpoint for creating an item in a list. + /// + /// The list ID. + /// The endpoint. + procedure GetCreateListItemEndpoint(ListId: Text): Text + begin + exit(StrSubstNo(CreateListItemLbl, SiteId, ListId)); + end; + + /// + /// Gets the endpoint for getting the default drive. + /// + /// The endpoint. + procedure GetDriveEndpoint(): Text + begin + exit(StrSubstNo(DriveLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting all drives. + /// + /// The endpoint. + procedure GetDrivesEndpoint(): Text + begin + exit(StrSubstNo(DrivesLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting the root of the default drive. + /// + /// The endpoint. + procedure GetDriveRootEndpoint(): Text + begin + exit(StrSubstNo(DriveRootLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting the children of the root folder. + /// + /// The endpoint. + procedure GetDriveRootChildrenEndpoint(): Text + begin + exit(StrSubstNo(DriveRootChildrenLbl, SiteId)); + end; + + /// + /// Gets the endpoint for getting an item by path. + /// + /// The path to the item. + /// The endpoint. + procedure GetDriveItemByPathEndpoint(ItemPath: Text): Text + begin + exit(StrSubstNo(DriveRootItemByPathLbl, SiteId, ItemPath)); + end; + + /// + /// Gets the endpoint for getting an item by ID. + /// + /// The item ID. + /// The endpoint. + procedure GetDriveItemByIdEndpoint(ItemId: Text): Text + begin + exit(StrSubstNo(DriveItemByIdLbl, SiteId, ItemId)); + end; + + /// + /// Gets the endpoint for getting the children of an item by ID. + /// + /// The item ID. + /// The endpoint. + procedure GetDriveItemChildrenByIdEndpoint(ItemId: Text): Text + begin + exit(StrSubstNo(DriveItemChildrenLbl, SiteId, ItemId)); + end; + + /// + /// Gets the endpoint for getting the children of an item by path. + /// + /// The path to the item. + /// The endpoint. + procedure GetDriveItemChildrenByPathEndpoint(ItemPath: Text): Text + begin + exit(StrSubstNo(DriveItemChildrenByPathLbl, SiteId, ItemPath)); + end; + + /// + /// Gets the endpoint for getting the content of an item by ID. + /// + /// The item ID. + /// The endpoint. + procedure GetDriveItemContentByIdEndpoint(ItemId: Text): Text + begin + exit(StrSubstNo(DriveItemContentLbl, SiteId, ItemId)); + end; + + /// + /// Gets the endpoint for getting the content of an item by path. + /// + /// The path to the item. + /// The endpoint. + procedure GetDriveItemContentByPathEndpoint(ItemPath: Text): Text + begin + exit(StrSubstNo(DriveItemContentByPathLbl, SiteId, ItemPath)); + end; + + /// + /// Gets the endpoint for uploading content to an item. + /// + /// The path to the folder. + /// The name of the file. + /// The endpoint. + procedure GetUploadEndpoint(FolderPath: Text; FileName: Text): Text + var + ItemPath: Text; + begin + if FolderPath = '' then + ItemPath := FileName + else + ItemPath := FolderPath + '/' + FileName; + + exit(StrSubstNo(DriveItemContentByPathLbl, SiteId, ItemPath)); + end; + + /// + /// Adds OData query parameters to an endpoint + /// + /// The base endpoint URL + /// Optional parameters including OData parameters + /// The endpoint with OData parameters if applicable + procedure AddOptionalParametersToEndpoint(BaseEndpoint: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Text + var + UriBuilder: Codeunit "Uri Builder"; + Uri: Codeunit Uri; + ODataParameters: Dictionary of [Text, Text]; + QueryParameters: Dictionary of [Text, Text]; + ParameterKey: Text; + FinalUri: Text; + AbsoluteUrl: Text; + BaseUrl: Text; + begin + // If no parameters, return original endpoint + if not HasParameters(GraphOptionalParameters) then + exit(BaseEndpoint); + + // Get the appropriate Graph API base URL + BaseUrl := GetGraphApiBaseUrl(); + + // Ensure we have an absolute URL before initializing the Uri + if IsRelativePath(BaseEndpoint) then + AbsoluteUrl := BaseUrl + BaseEndpoint + else + AbsoluteUrl := BaseEndpoint; + + // Initialize URI with absolute URL + Uri.Init(AbsoluteUrl); + UriBuilder.Init(Uri.GetAbsoluteUri()); + + // Add OData query parameters + ODataParameters := GraphOptionalParameters.GetODataQueryParameters(); + foreach ParameterKey in ODataParameters.Keys() do + UriBuilder.AddODataQueryParameter(ParameterKey, ODataParameters.Get(ParameterKey)); + + // Add regular query parameters + QueryParameters := GraphOptionalParameters.GetQueryParameters(); + foreach ParameterKey in QueryParameters.Keys() do + UriBuilder.AddQueryParameter(ParameterKey, QueryParameters.Get(ParameterKey)); + + // Get final URI + UriBuilder.GetUri(Uri); + FinalUri := Uri.GetAbsoluteUri(); + + // If the original endpoint was relative, strip the base URL to return a relative path + if IsRelativePath(BaseEndpoint) then + FinalUri := ReplaceString(FinalUri, BaseUrl, ''); + + exit(FinalUri); + end; + + local procedure HasParameters(GraphOptionalParameters: Codeunit "Graph Optional Parameters"): Boolean + var + ODataParameters: Dictionary of [Text, Text]; + QueryParameters: Dictionary of [Text, Text]; + begin + ODataParameters := GraphOptionalParameters.GetODataQueryParameters(); + QueryParameters := GraphOptionalParameters.GetQueryParameters(); + exit((ODataParameters.Count() > 0) or (QueryParameters.Count() > 0)); + end; + + local procedure IsRelativePath(Path: Text): Boolean + begin + exit(Path.StartsWith('/')); + end; + + local procedure ReplaceString(String: Text; OldSubString: Text; NewSubString: Text): Text + begin + exit(String.Replace(OldSubString, NewSubString)); + end; + + local procedure GetGraphApiBaseUrl(): Text + begin + exit(SharePointGraphReqHelper.GetGraphApiBaseUrl()); + end; + + /// + /// Gets the endpoint for getting the root of a specific drive. + /// + /// The ID of the drive. + /// The endpoint. + procedure GetSpecificDriveRootEndpoint(DriveId: Text): Text + begin + exit(StrSubstNo(SpecificDriveRootLbl, SiteId, DriveId)); + end; + + /// + /// Gets the endpoint for getting the children of the root folder of a specific drive. + /// + /// The ID of the drive. + /// The endpoint. + procedure GetSpecificDriveRootChildrenEndpoint(DriveId: Text): Text + begin + exit(StrSubstNo(SpecificDriveRootChildrenLbl, SiteId, DriveId)); + end; + + /// + /// Gets the endpoint for getting the children of an item by path in a specific drive. + /// + /// The ID of the drive. + /// The path to the item. + /// The endpoint. + procedure GetSpecificDriveItemChildrenByPathEndpoint(DriveId: Text; ItemPath: Text): Text + begin + exit(StrSubstNo(SpecificDriveItemChildrenByPathLbl, SiteId, DriveId, ItemPath)); + end; + + /// + /// Gets the endpoint for uploading content to an item in a specific drive. + /// + /// The ID of the drive. + /// The path to the folder. + /// The name of the file. + /// The endpoint. + procedure GetSpecificDriveUploadEndpoint(DriveId: Text; FolderPath: Text; FileName: Text): Text + var + ItemPath: Text; + begin + if FolderPath = '' then + ItemPath := FileName + else + ItemPath := FolderPath + '/' + FileName; + + exit(StrSubstNo(SpecificDriveItemContentByPathLbl, SiteId, DriveId, ItemPath)); + end; + +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDrive.Table.al b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDrive.Table.al new file mode 100644 index 0000000000..78ae9a19d3 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDrive.Table.al @@ -0,0 +1,108 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Represents a SharePoint drive (document library) as returned by Microsoft Graph API. +/// +table 9133 "SharePoint Graph Drive" +{ + Access = Public; + TableType = Temporary; + DataClassification = SystemMetadata; // Data classification is SystemMetadata as the table is temporary + InherentEntitlements = X; + InherentPermissions = X; + + fields + { + field(1; Id; Text[250]) + { + Caption = 'Id'; + DataClassification = CustomerContent; + Description = 'Unique identifier of the drive'; + } + field(2; Name; Text[250]) + { + Caption = 'Name'; + DataClassification = CustomerContent; + Description = 'Name of the drive (document library)'; + } + field(3; DriveType; Text[50]) + { + Caption = 'Drive Type'; + DataClassification = CustomerContent; + Description = 'Type of drive (personal, business, documentLibrary)'; + } + field(4; WebUrl; Text[2048]) + { + Caption = 'Web URL'; + DataClassification = CustomerContent; + Description = 'URL to access the drive in a web browser'; + } + field(5; OwnerName; Text[250]) + { + Caption = 'Owner Name'; + DataClassification = CustomerContent; + Description = 'Display name of the drive owner'; + } + field(6; OwnerEmail; Text[250]) + { + Caption = 'Owner Email'; + DataClassification = CustomerContent; + Description = 'Email address of the drive owner'; + } + field(7; CreatedDateTime; DateTime) + { + Caption = 'Created Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the drive was created'; + } + field(8; LastModifiedDateTime; DateTime) + { + Caption = 'Last Modified Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the drive was last modified'; + } + field(9; Description; Text[2048]) + { + Caption = 'Description'; + DataClassification = CustomerContent; + Description = 'Description of the drive'; + } + field(10; QuotaTotal; BigInteger) + { + Caption = 'Quota Total'; + DataClassification = CustomerContent; + Description = 'Total storage quota in bytes'; + } + field(11; QuotaUsed; BigInteger) + { + Caption = 'Quota Used'; + DataClassification = CustomerContent; + Description = 'Used storage in bytes'; + } + field(12; QuotaRemaining; BigInteger) + { + Caption = 'Quota Remaining'; + DataClassification = CustomerContent; + Description = 'Remaining storage quota in bytes'; + } + field(13; QuotaState; Text[50]) + { + Caption = 'Quota State'; + DataClassification = CustomerContent; + Description = 'State of the quota (normal, nearing, critical, exceeded)'; + } + } + + keys + { + key(Key1; Id) + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDriveItem.Table.al b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDriveItem.Table.al new file mode 100644 index 0000000000..9960c50b2f --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphDriveItem.Table.al @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Represents a SharePoint drive item (file or folder) as returned by Microsoft Graph API. +/// +table 9132 "SharePoint Graph Drive Item" +{ + Access = Public; + TableType = Temporary; + DataClassification = SystemMetadata; // Data classification is SystemMetadata as the table is temporary + InherentEntitlements = X; + InherentPermissions = X; + + fields + { + field(1; Id; Text[250]) + { + Caption = 'Id'; + DataClassification = CustomerContent; + Description = 'Unique identifier of the drive item'; + } + field(2; DriveId; Text[250]) + { + Caption = 'Drive Id'; + DataClassification = CustomerContent; + Description = 'ID of the parent drive'; + } + field(3; Name; Text[250]) + { + Caption = 'Name'; + DataClassification = CustomerContent; + Description = 'Name of the item (file or folder name)'; + } + field(4; ParentId; Text[250]) + { + Caption = 'Parent Id'; + DataClassification = CustomerContent; + Description = 'ID of the parent folder'; + } + field(5; Path; Text[2048]) + { + Caption = 'Path'; + DataClassification = CustomerContent; + Description = 'Path to the item from the drive root'; + } + field(6; WebUrl; Text[2048]) + { + Caption = 'Web URL'; + DataClassification = CustomerContent; + Description = 'URL to view the item in a web browser'; + } + field(7; DownloadUrl; Text[2048]) + { + Caption = 'Download URL'; + DataClassification = CustomerContent; + Description = 'URL to download the item content'; + } + field(8; CreatedDateTime; DateTime) + { + Caption = 'Created Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the item was created'; + } + field(9; LastModifiedDateTime; DateTime) + { + Caption = 'Last Modified Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the item was last modified'; + } + field(10; Size; BigInteger) + { + Caption = 'Size'; + DataClassification = CustomerContent; + Description = 'Size of the item in bytes'; + } + field(11; IsFolder; Boolean) + { + Caption = 'Is Folder'; + DataClassification = CustomerContent; + Description = 'Indicates if the item is a folder'; + } + field(12; FileType; Text[50]) + { + Caption = 'File Type'; + DataClassification = CustomerContent; + Description = 'Type/extension of the file'; + } + } + + keys + { + key(Key1; Id) + { + Clustered = true; + } + key(Key2; DriveId, Id) + { + } + } +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/models/SharePointGraphList.Table.al b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphList.Table.al new file mode 100644 index 0000000000..47454a5988 --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphList.Table.al @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Represents a SharePoint list as returned by Microsoft Graph API. +/// +table 9130 "SharePoint Graph List" +{ + Access = Public; + TableType = Temporary; + DataClassification = SystemMetadata; // Data classification is SystemMetadata as the table is temporary + InherentEntitlements = X; + InherentPermissions = X; + + fields + { + field(1; Id; Text[250]) + { + Caption = 'Id'; + DataClassification = CustomerContent; + Description = 'Unique identifier of the list'; + } + field(2; DisplayName; Text[250]) + { + Caption = 'Display Name'; + DataClassification = CustomerContent; + Description = 'Name of the list for display purposes'; + } + field(3; Name; Text[250]) + { + Caption = 'Name'; + DataClassification = CustomerContent; + Description = 'Name of the list'; + } + field(4; Description; Text[2048]) + { + Caption = 'Description'; + DataClassification = CustomerContent; + Description = 'Description of the list'; + } + field(5; WebUrl; Text[2048]) + { + Caption = 'Web URL'; + DataClassification = CustomerContent; + Description = 'URL to view the list in a web browser'; + } + field(6; Template; Text[100]) + { + Caption = 'Template'; + DataClassification = CustomerContent; + Description = 'List template used to create this list (genericList, documentLibrary, etc.)'; + } + field(7; ListItemEntityType; Text[250]) + { + Caption = 'List Item Entity Type'; + DataClassification = CustomerContent; + Description = 'Entity type name for list items in this list'; + } + field(8; DriveId; Text[250]) + { + Caption = 'Drive ID'; + DataClassification = CustomerContent; + Description = 'Drive ID (for document libraries)'; + } + field(9; LastModifiedDateTime; DateTime) + { + Caption = 'Last Modified Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the list was last modified'; + } + field(10; CreatedDateTime; DateTime) + { + Caption = 'Created Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the list was created'; + } + } + + keys + { + key(Key1; Id) + { + Clustered = true; + } + } +} \ No newline at end of file diff --git a/src/System Application/App/SharePoint/src/graph/models/SharePointGraphListItem.Table.al b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphListItem.Table.al new file mode 100644 index 0000000000..eea8ed510f --- /dev/null +++ b/src/System Application/App/SharePoint/src/graph/models/SharePointGraphListItem.Table.al @@ -0,0 +1,135 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Integration.Sharepoint; + +/// +/// Represents a SharePoint list item as returned by Microsoft Graph API. +/// +table 9131 "SharePoint Graph List Item" +{ + Access = Public; + TableType = Temporary; + DataClassification = CustomerContent; + InherentEntitlements = X; + InherentPermissions = X; + + fields + { + field(1; Id; Text[250]) + { + Caption = 'Id'; + DataClassification = CustomerContent; + Description = 'Unique identifier of the list item'; + } + field(2; ListId; Text[250]) + { + Caption = 'List Id'; + DataClassification = CustomerContent; + Description = 'ID of the parent list'; + } + field(3; Title; Text[250]) + { + Caption = 'Title'; + DataClassification = CustomerContent; + Description = 'Title of the list item'; + } + field(4; ContentType; Text[100]) + { + Caption = 'Content Type'; + DataClassification = CustomerContent; + Description = 'Content type of the list item'; + } + field(5; WebUrl; Text[2048]) + { + Caption = 'Web URL'; + DataClassification = CustomerContent; + Description = 'URL to view the list item in a web browser'; + } + field(6; CreatedDateTime; DateTime) + { + Caption = 'Created Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the list item was created'; + } + field(7; LastModifiedDateTime; DateTime) + { + Caption = 'Last Modified Date Time'; + DataClassification = CustomerContent; + Description = 'Date and time when the list item was last modified'; + } + field(8; FieldsJson; Blob) + { + Caption = 'Fields JSON'; + DataClassification = CustomerContent; + Description = 'JSON representation of the list item''s custom fields'; + } + } + + keys + { + key(Key1; Id) + { + Clustered = true; + } + key(Key2; ListId, Id) + { + } + } + + /// + /// Sets the custom fields for the list item as a JSON object. + /// + /// JSON object containing the custom fields + procedure SetFieldsJson(FieldsJsonObject: JsonObject) + var + OutStream: OutStream; + begin + FieldsJson.CreateOutStream(OutStream, TextEncoding::UTF8); + FieldsJsonObject.WriteTo(OutStream); + end; + + /// + /// Gets the custom fields for the list item as a JSON object. + /// + /// JSON object that will contain the custom fields + /// True if fields were retrieved successfully, false otherwise + procedure GetFieldsJson(var FieldsJsonObject: JsonObject): Boolean + var + InStream: InStream; + JsonText: Text; + begin + FieldsJson.CreateInStream(InStream, TextEncoding::UTF8); + InStream.ReadText(JsonText); + if JsonText = '' then + exit(false); + + exit(FieldsJsonObject.ReadFrom(JsonText)); + end; + + /// + /// Gets a specific field value from the custom fields. + /// + /// Name of the field to retrieve + /// Text value that will contain the field value + /// True if field was found, false otherwise + procedure GetFieldValue(FieldName: Text; var FieldValue: Text): Boolean + var + FieldsJsonObject: JsonObject; + FieldToken: JsonToken; + begin + if not GetFieldsJson(FieldsJsonObject) then + exit(false); + + if not FieldsJsonObject.Get(FieldName, FieldToken) then + exit(false); + + if not FieldToken.IsValue() then + exit(false); + + FieldValue := FieldToken.AsValue().AsText(); + exit(true); + end; +} \ No newline at end of file diff --git a/src/System Application/Test Library/SharePoint/app.json b/src/System Application/Test Library/SharePoint/app.json index 8dcc942cbf..a499ac23d9 100644 --- a/src/System Application/Test Library/SharePoint/app.json +++ b/src/System Application/Test Library/SharePoint/app.json @@ -34,6 +34,18 @@ "name": "URI", "publisher": "Microsoft", "version": "28.0.0.0" + }, + { + "id": "812b339d-a9db-4a6e-84e4-fe35cbef0c44", + "name": "Rest Client", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, + { + "id": "6d72c93d-164a-494c-8d65-24d7f41d7b61", + "name": "Microsoft Graph", + "publisher": "Microsoft", + "version": "28.0.0.0" } ], "screenshots": [], @@ -42,7 +54,11 @@ "idRanges": [ { "from": 132972, - "to": 132975 + "to": 132976 + }, + { + "from": 132981, + "to": 132982 } ], "contextSensitiveHelpUrl": "https://learn.microsoft.com/dynamics365/business-central/", @@ -50,5 +66,12 @@ "allowDebugging": true, "allowDownloadingSource": true, "includeSourceInSymbolFile": true - } + }, + "internalsVisibleTo": [ + { + "id": "977e6b76-d7c1-41fa-b38b-21399cd140a7", + "name": "SharePoint Test", + "publisher": "Microsoft" + } + ] } diff --git a/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphAuthSpy.Codeunit.al b/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphAuthSpy.Codeunit.al new file mode 100644 index 0000000000..6241c8ea23 --- /dev/null +++ b/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphAuthSpy.Codeunit.al @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Test.Integration.Sharepoint; + +using System.Integration.Graph.Authorization; +using System.RestClient; + +codeunit 132974 "SharePoint Graph Auth Spy" implements "Graph Authorization" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + Invoked: Boolean; + + procedure IsInvoked(): Boolean + begin + exit(Invoked); + end; + + procedure GetHttpAuthorization(): Interface "Http Authentication"; + var + HttpAuthenticationAnonymous: Codeunit "Http Authentication Anonymous"; + begin + Invoked := true; + exit(HttpAuthenticationAnonymous); + end; +} \ No newline at end of file diff --git a/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphTestLibrary.Codeunit.al b/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphTestLibrary.Codeunit.al new file mode 100644 index 0000000000..07b1c5b4c9 --- /dev/null +++ b/src/System Application/Test Library/SharePoint/src/graph/SharePointGraphTestLibrary.Codeunit.al @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Integration.Sharepoint; + +using System.RestClient; + +codeunit 132975 "SharePoint Graph Test Library" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + MockHttpClientHandler: Codeunit "SharePoint Http Client Handler"; + + procedure SetMockResponse(var NewHttpResponseMessage: Codeunit "Http Response Message") + begin + MockHttpClientHandler.SetResponse(NewHttpResponseMessage); + end; + + procedure GetHttpRequestMessage(var OutHttpRequestMessage: Codeunit "Http Request Message") + begin + MockHttpClientHandler.GetHttpRequestMessage(OutHttpRequestMessage); + end; + + procedure ExpectRequestToFailWithError(ErrorText: Text) + begin + MockHttpClientHandler.ExpectSendToFailWithError(ErrorText); + end; + + procedure ResetMockHandler() + begin + Clear(this.MockHttpClientHandler); + end; + + procedure GetMockHandler(): Interface "Http Client Handler" + begin + exit(this.MockHttpClientHandler); + end; +} \ No newline at end of file diff --git a/src/System Application/Test Library/SharePoint/src/graph/SharePointHttpClientHandler.Codeunit.al b/src/System Application/Test Library/SharePoint/src/graph/SharePointHttpClientHandler.Codeunit.al new file mode 100644 index 0000000000..4389d9dd66 --- /dev/null +++ b/src/System Application/Test Library/SharePoint/src/graph/SharePointHttpClientHandler.Codeunit.al @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Integration.Sharepoint; + +using System.RestClient; + +codeunit 132981 "SharePoint Http Client Handler" implements "Http Client Handler" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + HttpRequestMessage: Codeunit "Http Request Message"; + HttpResponseMessage: Codeunit "Http Response Message"; + ResponseMessageSet: Boolean; + SendError: Text; + + procedure Send(HttpClient: HttpClient; InHttpRequestMessage: Codeunit "Http Request Message"; var OutHttpResponseMessage: Codeunit "Http Response Message") Success: Boolean; + begin + ClearLastError(); + exit(TrySend(InHttpRequestMessage, OutHttpResponseMessage)); + end; + + procedure ExpectSendToFailWithError(NewSendError: Text) + begin + this.SendError := NewSendError; + end; + + procedure SetResponse(var NewHttpResponseMessage: Codeunit "Http Response Message") + begin + this.HttpResponseMessage := NewHttpResponseMessage; + this.ResponseMessageSet := true; + end; + + procedure GetHttpRequestMessage(var OutHttpRequestMessage: Codeunit "Http Request Message") + begin + OutHttpRequestMessage := this.HttpRequestMessage; + end; + + [TryFunction] + local procedure TrySend(InHttpRequestMessage: Codeunit "Http Request Message"; var OutHttpResponseMessage: Codeunit "Http Response Message") + begin + this.HttpRequestMessage := InHttpRequestMessage; + if SendError <> '' then + Error(SendError); + + if ResponseMessageSet then + OutHttpResponseMessage := this.HttpResponseMessage; + end; +} \ No newline at end of file diff --git a/src/System Application/Test/DisabledTests/SharePointGraphAdvancedTest.json b/src/System Application/Test/DisabledTests/SharePointGraphAdvancedTest.json new file mode 100644 index 0000000000..63ce5b1732 --- /dev/null +++ b/src/System Application/Test/DisabledTests/SharePointGraphAdvancedTest.json @@ -0,0 +1,7 @@ +[ + { + "codeunitId": 132985, + "CodeunitName": "SharePoint Graph Advanced Test", + "Method": "TestCopyItemByPath" + } +] \ No newline at end of file diff --git a/src/System Application/Test/SharePoint/app.json b/src/System Application/Test/SharePoint/app.json index 3209143c22..2d37d3b9ff 100644 --- a/src/System Application/Test/SharePoint/app.json +++ b/src/System Application/Test/SharePoint/app.json @@ -46,6 +46,24 @@ "name": "BLOB Storage", "publisher": "Microsoft", "version": "28.0.0.0" + }, + { + "id": "812b339d-a9db-4a6e-84e4-fe35cbef0c44", + "name": "Rest Client", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, + { + "id": "6d72c93d-164a-494c-8d65-24d7f41d7b61", + "name": "Microsoft Graph", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, + { + "id": "1b2efb4b-8c44-4d74-a56f-60646645bb21", + "name": "URI", + "publisher": "Microsoft", + "version": "28.0.0.0" } ], "screenshots": [], @@ -55,6 +73,10 @@ { "from": 132970, "to": 132971 + }, + { + "from": 132983, + "to": 132985 } ], "contextSensitiveHelpUrl": "https://docs.microsoft.com/dynamics365/business-central/", diff --git a/src/System Application/Test/SharePoint/src/graph/SharePointGraphAdvancedTest.Codeunit.al b/src/System Application/Test/SharePoint/src/graph/SharePointGraphAdvancedTest.Codeunit.al new file mode 100644 index 0000000000..0030c7688b --- /dev/null +++ b/src/System Application/Test/SharePoint/src/graph/SharePointGraphAdvancedTest.Codeunit.al @@ -0,0 +1,967 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Sharepoint; +using System.RestClient; +using System.TestLibraries.Utilities; +using System.Utilities; + +codeunit 132985 "SharePoint Graph Advanced Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + SharePointGraphAuthSpy: Codeunit "SharePoint Graph Auth Spy"; + SharePointGraphTestLibrary: Codeunit "SharePoint Graph Test Library"; + SharePointGraphClient: Codeunit "SharePoint Graph Client"; + LibraryAssert: Codeunit "Library Assert"; + SharePointUrlLbl: Label 'https://contoso.sharepoint.com/sites/test', Locked = true; + IsInitialized: Boolean; + + [Test] + procedure TestOptionalParameters() + var + TempSharePointGraphList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpRequestMessage: Codeunit "Http Request Message"; + Uri: Codeunit Uri; + UnescapeDataString: Text; + begin + // [GIVEN] Mock response for an API call + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Setting optional parameters + SharePointGraphClient.SetODataSelect(GraphOptionalParameters, 'displayName,id,webUrl'); + SharePointGraphClient.SetODataFilter(GraphOptionalParameters, 'contains(displayName,''Document'')'); + SharePointGraphClient.SetODataOrderBy(GraphOptionalParameters, 'displayName asc'); + + SharePointGraphClient.GetLists(TempSharePointGraphList, GraphOptionalParameters); + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + + // [THEN] Request URI should include the correct query parameters + Uri.Init(HttpRequestMessage.GetRequestUri()); + UnescapeDataString := Uri.UnescapeDataString(Uri.GetQuery()); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$select=displayName,id,webUrl'), 'Query should contain select parameter'); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$filter=contains(displayName,''Document'')'), 'Query should contain filter parameter'); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$orderby=displayName asc'), 'Query should contain orderby parameter'); + end; + + [Test] + procedure TestODataExpandParameter() + var + TempSharePointGraphList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpRequestMessage: Codeunit "Http Request Message"; + Uri: Codeunit Uri; + UnescapeDataString: Text; + begin + // [GIVEN] Mock response for an API call + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Setting expand parameter + SharePointGraphClient.SetODataExpand(GraphOptionalParameters, 'columns,items'); + + SharePointGraphClient.GetLists(TempSharePointGraphList, GraphOptionalParameters); + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + + // [THEN] Request URI should include the expand query parameter + Uri.Init(HttpRequestMessage.GetRequestUri()); + UnescapeDataString := Uri.UnescapeDataString(Uri.GetQuery()); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$expand=columns,items'), 'Query should contain expand parameter'); + end; + + [Test] + procedure TestPagination() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + HttpRequestMessage: Codeunit "Http Request Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] We need to handle multiple requests for pagination + Initialize(); + + // [WHEN] Calling GetFolderItems (which should handle the pagination automatically) + // Note: Since we can't easily queue multiple responses in the current mock implementation, + // we'll need to modify the test approach + + // First, let's test that the first page is retrieved correctly + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetPaginatedResponsePage1()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // For now, we'll test pagination by verifying the request includes the nextLink + SharePointGraphResponse := SharePointGraphClient.GetFolderItems('01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM', TempDriveItem); + + // Get the request to verify it was made correctly + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + + // [THEN] First page should be retrieved successfully + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetFolderItems should succeed for first page'); + LibraryAssert.IsTrue(HttpRequestMessage.GetRequestUri().Contains('/items/01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM/children'), 'Request should be for folder children'); + + // Note: Full pagination testing would require enhancing the mock handler to queue multiple responses + // For now, we're testing that the pagination URL is correctly formed in the response + LibraryAssert.AreEqual(2, TempDriveItem.Count(), 'Should return 2 items from first page'); + end; + + [Test] + procedure TestConflictBehavior() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + HttpRequestMessage: Codeunit "Http Request Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + TempBlob: Codeunit "Temp Blob"; + FileInStream: InStream; + FileOutStream: OutStream; + begin + // [GIVEN] Mock response for UploadFile + Initialize(); + + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetUploadFileResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Preparing a file and calling UploadFile with conflict behavior + TempBlob.CreateOutStream(FileOutStream); + FileOutStream.WriteText('Test content for uploaded file'); + TempBlob.CreateInStream(FileInStream); + + SharePointGraphResponse := SharePointGraphClient.UploadFile('Documents', 'Test.txt', FileInStream, TempDriveItem, Enum::"Graph ConflictBehavior"::Replace); + + // [THEN] Request should include the correct conflict behavior + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), SharePointGraphClient.GetDiagnostics().GetErrorMessage()); + LibraryAssert.IsTrue(HttpRequestMessage.GetRequestUri().Contains('microsoft.graph.conflictBehavior=replace'), 'URL should include conflict behavior parameter'); + end; + + [Test] + procedure TestErrorHandling() + var + TempBlob: Codeunit "Temp Blob"; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + SharePointHttpDiagnostics: Interface "HTTP Diagnostics"; + Headers: HttpHeaders; + begin + // Test rate limiting response (429 Too Many Requests) + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(429); + MockHttpContent := HttpContent.Create(GetRateLimitResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpResponseMessage.SetReasonPhrase('Too Many Requests'); + + Headers := MockHttpResponseMessage.GetHeaders(); + Headers.Add('Retry-After', '5'); + MockHttpResponseMessage.SetHeaders(Headers); + + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling API that is rate limited + SharePointGraphResponse := SharePointGraphClient.DownloadFile('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempBlob); + + // [THEN] Operation should fail and return rate limit info + LibraryAssert.IsFalse(SharePointGraphResponse.IsSuccessful(), 'Operation should fail due to rate limiting'); + SharePointHttpDiagnostics := SharePointGraphClient.GetDiagnostics(); + LibraryAssert.AreEqual(429, SharePointHttpDiagnostics.GetHttpStatusCode(), 'Status code should be 429'); + LibraryAssert.AreEqual('Too Many Requests', SharePointHttpDiagnostics.GetResponseReasonPhrase(), 'Reason phrase should match'); + LibraryAssert.AreEqual(5, SharePointHttpDiagnostics.GetHttpRetryAfter(), 'Retry-After should be 5 seconds'); + end; + + [Test] + procedure TestForbiddenError() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + SharePointHttpDiagnostics: Interface "HTTP Diagnostics"; + begin + // [GIVEN] Mock forbidden response (403) + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(403); + MockHttpContent := HttpContent.Create(GetForbiddenResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpResponseMessage.SetReasonPhrase('Forbidden'); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling API without proper permissions + SharePointGraphResponse := SharePointGraphClient.GetLists(TempList); + + // [THEN] Operation should fail with 403 + LibraryAssert.IsFalse(SharePointGraphResponse.IsSuccessful(), 'Operation should fail due to lack of permissions'); + SharePointHttpDiagnostics := SharePointGraphClient.GetDiagnostics(); + LibraryAssert.AreEqual(403, SharePointHttpDiagnostics.GetHttpStatusCode(), 'Status code should be 403'); + LibraryAssert.AreEqual('Forbidden', SharePointHttpDiagnostics.GetResponseReasonPhrase(), 'Reason phrase should match'); + end; + + [Test] + procedure TestServerError() + var + TempBlob: Codeunit "Temp Blob"; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + SharePointHttpDiagnostics: Interface "HTTP Diagnostics"; + begin + // [GIVEN] Mock server error response (500) + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(500); + MockHttpContent := HttpContent.Create(GetServerErrorResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpResponseMessage.SetReasonPhrase('Internal Server Error'); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling API that encounters server error + SharePointGraphResponse := SharePointGraphClient.DownloadFile('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempBlob); + + // [THEN] Operation should fail with 500 + LibraryAssert.IsFalse(SharePointGraphResponse.IsSuccessful(), 'Operation should fail due to server error'); + SharePointHttpDiagnostics := SharePointGraphClient.GetDiagnostics(); + LibraryAssert.AreEqual(500, SharePointHttpDiagnostics.GetHttpStatusCode(), 'Status code should be 500'); + LibraryAssert.AreEqual('Internal Server Error', SharePointHttpDiagnostics.GetResponseReasonPhrase(), 'Reason phrase should match'); + end; + + [Test] + procedure TestBadRequestError() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + SharePointHttpDiagnostics: Interface "HTTP Diagnostics"; + begin + // [GIVEN] Mock bad request response (400) + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(400); + MockHttpContent := HttpContent.Create(GetBadRequestResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpResponseMessage.SetReasonPhrase('Bad Request'); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling API with invalid parameters + SharePointGraphResponse := SharePointGraphClient.CreateFolder('Documents', 'Invalid*Name?', TempDriveItem); + + // [THEN] Operation should fail with 400 + LibraryAssert.IsFalse(SharePointGraphResponse.IsSuccessful(), 'Operation should fail due to bad request'); + SharePointHttpDiagnostics := SharePointGraphClient.GetDiagnostics(); + LibraryAssert.AreEqual(400, SharePointHttpDiagnostics.GetHttpStatusCode(), 'Status code should be 400'); + LibraryAssert.AreEqual('Bad Request', SharePointHttpDiagnostics.GetResponseReasonPhrase(), 'Reason phrase should match'); + end; + + [Test] + procedure TestGetDefaultDriveId() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + DriveId: Text; + begin + // [GIVEN] Mock response for GetDefaultDrive + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDefaultDrive to get ID only + SharePointGraphResponse := SharePointGraphClient.GetDefaultDrive(DriveId); + + // [THEN] Operation should succeed and return drive ID + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDefaultDrive should succeed'); + LibraryAssert.AreEqual('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8', DriveId, 'Drive ID should match'); + end; + + [Test] + procedure TestGetDefaultDrive() + var + TempDrive: Record "SharePoint Graph Drive" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetDefaultDrive + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDefaultDrive with full details + SharePointGraphResponse := SharePointGraphClient.GetDefaultDrive(TempDrive); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDefaultDrive should succeed'); + LibraryAssert.AreEqual('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8', TempDrive.Id, 'Drive ID should match'); + LibraryAssert.AreEqual('Documents', TempDrive.Name, 'Drive name should match'); + LibraryAssert.AreEqual('documentLibrary', TempDrive.DriveType, 'Drive type should match'); + LibraryAssert.IsTrue(TempDrive.QuotaTotal > 0, 'Quota total should be populated'); + LibraryAssert.IsTrue(TempDrive.QuotaUsed > 0, 'Quota used should be populated'); + end; + + [Test] + procedure TestGetDrive() + var + TempDrive: Record "SharePoint Graph Drive" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetDrive + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDrive + SharePointGraphResponse := SharePointGraphClient.GetDrive('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8', TempDrive); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDrive should succeed'); + LibraryAssert.AreEqual('Documents', TempDrive.Name, 'Drive name should match'); + end; + + [Test] + procedure TestGetDriveWithOptionalParameters() + var + TempDrive: Record "SharePoint Graph Drive" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpRequestMessage: Codeunit "Http Request Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + Uri: Codeunit Uri; + UnescapeDataString: Text; + begin + // [GIVEN] Mock response for GetDrive + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDrive with optional parameters + SharePointGraphClient.SetODataSelect(GraphOptionalParameters, 'id,name,driveType'); + SharePointGraphResponse := SharePointGraphClient.GetDrive('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8', TempDrive, GraphOptionalParameters); + + // [THEN] Operation should succeed and include query parameters + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDrive should succeed'); + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + Uri.Init(HttpRequestMessage.GetRequestUri()); + UnescapeDataString := Uri.UnescapeDataString(Uri.GetQuery()); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$select=id,name,driveType'), 'Query should contain select parameter'); + end; + + [Test] + procedure TestGetDriveItemWithOptionalParameters() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpRequestMessage: Codeunit "Http Request Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + Uri: Codeunit Uri; + UnescapeDataString: Text; + begin + // [GIVEN] Mock response for GetDriveItem + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDriveItem with optional parameters + SharePointGraphClient.SetODataSelect(GraphOptionalParameters, 'id,name,size'); + SharePointGraphResponse := SharePointGraphClient.GetDriveItem('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempDriveItem, GraphOptionalParameters); + + // [THEN] Operation should succeed and include query parameters + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDriveItem should succeed'); + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + Uri.Init(HttpRequestMessage.GetRequestUri()); + UnescapeDataString := Uri.UnescapeDataString(Uri.GetQuery()); + LibraryAssert.IsTrue(UnescapeDataString.Contains('$select=id,name,size'), 'Query should contain select parameter'); + end; + + [Test] + procedure TestGetItemsByPath() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetItemsByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetFolderItemsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetItemsByPath + SharePointGraphResponse := SharePointGraphClient.GetItemsByPath('Documents/Reports', TempDriveItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetItemsByPath should succeed'); + LibraryAssert.AreEqual(2, TempDriveItem.Count(), 'Should return 2 items'); + end; + + [Test] + procedure TestDeleteItem() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for DeleteItem + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(204); + MockHttpContent := HttpContent.Create(''); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling DeleteItem + SharePointGraphResponse := SharePointGraphClient.DeleteItem('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'DeleteItem should succeed'); + end; + + [Test] + procedure TestDeleteItemByPath() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for DeleteItemByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(204); + MockHttpContent := HttpContent.Create(''); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling DeleteItemByPath + SharePointGraphResponse := SharePointGraphClient.DeleteItemByPath('Documents/FileToDelete.txt'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'DeleteItemByPath should succeed'); + end; + + [Test] + procedure TestDeleteItemNotFound() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for DeleteItem with 404 + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(404); + MockHttpContent := HttpContent.Create(GetNotFoundResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling DeleteItem on non-existent item + SharePointGraphResponse := SharePointGraphClient.DeleteItem('01NONEXISTENTITEMID'); + + // [THEN] Operation should succeed (404 is acceptable for delete) + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'DeleteItem should succeed even with 404'); + end; + + [Test] + procedure TestItemExists() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + Exists: Boolean; + begin + // [GIVEN] Mock response for ItemExists + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling ItemExists + SharePointGraphResponse := SharePointGraphClient.ItemExists('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', Exists); + + // [THEN] Operation should succeed and item should exist + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'ItemExists should succeed'); + LibraryAssert.IsTrue(Exists, 'Item should exist'); + end; + + [Test] + procedure TestItemExistsNotFound() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + Exists: Boolean; + begin + // [GIVEN] Mock response for ItemExists with 404 + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(404); + MockHttpContent := HttpContent.Create(GetNotFoundResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling ItemExists on non-existent item + SharePointGraphResponse := SharePointGraphClient.ItemExists('01NONEXISTENTITEMID', Exists); + + // [THEN] Operation should succeed and item should not exist + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'ItemExists should succeed'); + LibraryAssert.IsFalse(Exists, 'Item should not exist'); + end; + + [Test] + procedure TestItemExistsByPath() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + Exists: Boolean; + begin + // [GIVEN] Mock response for ItemExistsByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling ItemExistsByPath + SharePointGraphResponse := SharePointGraphClient.ItemExistsByPath('Documents/Report.docx', Exists); + + // [THEN] Operation should succeed and item should exist + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'ItemExistsByPath should succeed'); + LibraryAssert.IsTrue(Exists, 'Item should exist'); + end; + + [Test] + procedure TestCopyItem() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CopyItem + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(202); + MockHttpContent := HttpContent.Create(''); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CopyItem + SharePointGraphResponse := SharePointGraphClient.CopyItem('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', '01TARGETFOLDERID123', 'CopiedFile.txt'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CopyItem should succeed'); + end; + + [Test] + procedure TestCopyItemByPath() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CopyItemByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(202); + MockHttpContent := HttpContent.Create(''); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CopyItemByPath + SharePointGraphResponse := SharePointGraphClient.CopyItemByPath('Documents/Original.txt', 'Documents/Archive', 'CopiedFile.txt'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CopyItemByPath should succeed'); + end; + + [Test] + procedure TestMoveItem() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for MoveItem + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling MoveItem + SharePointGraphResponse := SharePointGraphClient.MoveItem('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', '01TARGETFOLDERID123', 'MovedFile.txt'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'MoveItem should succeed'); + end; + + [Test] + procedure TestMoveItemByPath() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for MoveItemByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling MoveItemByPath + SharePointGraphResponse := SharePointGraphClient.MoveItemByPath('Documents/Original.txt', 'Documents/Archive', 'MovedFile.txt'); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'MoveItemByPath should succeed'); + end; + + local procedure Initialize() + var + MockHttpClientHandler: Interface "Http Client Handler"; + begin + if IsInitialized then + exit; + + // Get the mock handler from the test library + MockHttpClientHandler := SharePointGraphTestLibrary.GetMockHandler(); + + // Initialize with the mock handler + SharePointGraphClient.Initialize(SharePointUrlLbl, Enum::"Graph API Version"::"v1.0", SharePointGraphAuthSpy, MockHttpClientHandler); + + // Set test IDs to prevent HTTP calls for site and drive discovery + SharePointGraphClient.SetSiteIdForTesting('contoso.sharepoint.com,e6991d99-75d5-4be4-4ede-2c82b1d40cd6,1b58abad-4105-4125-a0e0-7a6d39571a5b'); + SharePointGraphClient.SetDefaultDriveIdForTesting('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8'); + + IsInitialized := true; + end; + + local procedure GetListsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.list)",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01bjtwww-5j35-426b-a4d5-608f6e2a9f84",'); + ResponseText.Append(' "displayName": "Test Documents",'); + ResponseText.Append(' "description": "Test library for documents",'); + ResponseText.Append(' "list": {'); + ResponseText.Append(' "template": "documentLibrary",'); + ResponseText.Append(' "hidden": false,'); + ResponseText.Append(' "contentTypesEnabled": true'); + ResponseText.Append(' },'); + ResponseText.Append(' "createdDateTime": "2022-05-23T12:16:04Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents"'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "27c78f81-f4d9-4ee9-85bd-5d57ade1b5f4",'); + ResponseText.Append(' "displayName": "HR Documents",'); + ResponseText.Append(' "description": "HR library for documents",'); + ResponseText.Append(' "list": {'); + ResponseText.Append(' "template": "documentLibrary",'); + ResponseText.Append(' "hidden": false,'); + ResponseText.Append(' "contentTypesEnabled": true'); + ResponseText.Append(' },'); + ResponseText.Append(' "createdDateTime": "2022-05-23T12:16:04Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/HR%20Documents"'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetPaginatedResponsePage1(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems(''01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM'')/children",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP1",'); + ResponseText.Append(' "name": "Subfolder",'); + ResponseText.Append(' "createdDateTime": "2022-09-15T10:12:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-03-20T14:35:16Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201/Subfolder",'); + ResponseText.Append(' "folder": {'); + ResponseText.Append(' "childCount": 1'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP2",'); + ResponseText.Append(' "name": "Presentation.pptx",'); + ResponseText.Append(' "createdDateTime": "2022-10-05T11:42:18Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-05-12T15:27:39Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201/Presentation.pptx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "TU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 87621'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetUploadFileResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems/$entity",'); + ResponseText.Append(' "id": "01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ",'); + ResponseText.Append(' "name": "Test.txt",'); + ResponseText.Append(' "createdDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Test.txt",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "text/plain",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "KU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 25'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetRateLimitResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "429",'); + ResponseText.Append(' "message": "Too many requests. Please try again later.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede",'); + ResponseText.Append(' "client-request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetForbiddenResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "accessDenied",'); + ResponseText.Append(' "message": "Access denied. You do not have permission to perform this action.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "4c3e2f6a-fc2d-52b2-91f3-2gc9bf5fcfdf",'); + ResponseText.Append(' "client-request-id": "4c3e2f6a-fc2d-52b2-91f3-2gc9bf5fcfdf"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetServerErrorResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "internalServerError",'); + ResponseText.Append(' "message": "An internal server error occurred.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "5d4f3a7b-ad3e-63c3-02a4-3hd0ca6adfea",'); + ResponseText.Append(' "client-request-id": "5d4f3a7b-ad3e-63c3-02a4-3hd0ca6adfea"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetBadRequestResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "invalidRequest",'); + ResponseText.Append(' "message": "The request is malformed or incorrect.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "6e5a4b8c-be4f-74d4-13b5-4ie1db7befb",'); + ResponseText.Append(' "client-request-id": "6e5a4b8c-be4f-74d4-13b5-4ie1db7befb"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetDriveResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives/$entity",'); + ResponseText.Append(' "id": "b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8",'); + ResponseText.Append(' "name": "Documents",'); + ResponseText.Append(' "driveType": "documentLibrary",'); + ResponseText.Append(' "description": "Default document library",'); + ResponseText.Append(' "createdDateTime": "2022-01-15T08:30:00Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents",'); + ResponseText.Append(' "owner": {'); + ResponseText.Append(' "user": {'); + ResponseText.Append(' "displayName": "System Account",'); + ResponseText.Append(' "email": "system@contoso.com"'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "quota": {'); + ResponseText.Append(' "total": 1099511627776,'); + ResponseText.Append(' "used": 524288000,'); + ResponseText.Append(' "remaining": 1098987339776,'); + ResponseText.Append(' "state": "normal"'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetDriveItemResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems/$entity",'); + ResponseText.Append(' "id": "01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ",'); + ResponseText.Append(' "name": "Report.docx",'); + ResponseText.Append(' "createdDateTime": "2023-05-10T14:25:37Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-06-20T09:42:13Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Report.docx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "dF5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 45321'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetFolderItemsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP1",'); + ResponseText.Append(' "name": "Q1Report.docx",'); + ResponseText.Append(' "createdDateTime": "2022-09-15T10:12:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-03-20T14:35:16Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Reports/Q1Report.docx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 45321'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP2",'); + ResponseText.Append(' "name": "Q2Report.docx",'); + ResponseText.Append(' "createdDateTime": "2022-10-05T11:42:18Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-05-12T15:27:39Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Reports/Q2Report.docx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 52347'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetNotFoundResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "itemNotFound",'); + ResponseText.Append(' "message": "The resource could not be found.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede",'); + ResponseText.Append(' "client-request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/SharePoint/src/graph/SharePointGraphClientTest.Codeunit.al b/src/System Application/Test/SharePoint/src/graph/SharePointGraphClientTest.Codeunit.al new file mode 100644 index 0000000000..2e584283f3 --- /dev/null +++ b/src/System Application/Test/SharePoint/src/graph/SharePointGraphClientTest.Codeunit.al @@ -0,0 +1,584 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Sharepoint; +using System.RestClient; +using System.TestLibraries.Utilities; + +codeunit 132984 "SharePoint Graph Client Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + SharePointGraphAuthSpy: Codeunit "SharePoint Graph Auth Spy"; + SharePointGraphTestLibrary: Codeunit "SharePoint Graph Test Library"; + SharePointGraphClient: Codeunit "SharePoint Graph Client"; + LibraryAssert: Codeunit "Library Assert"; + SharePointUrlLbl: Label 'https://contoso.sharepoint.com/sites/test', Locked = true; + IsInitialized: Boolean; + + [Test] + procedure TestAuthorizationInvoked() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + begin + // [GIVEN] Mock response for an API call + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Initialize the client and make an API call + SharePointGraphClient.GetLists(TempList); + + // [THEN] Authorization should be invoked + LibraryAssert.IsTrue(SharePointGraphAuthSpy.IsInvoked(), 'Authorization should be invoked'); + end; + + [Test] + procedure TestRequestUriFormat() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + HttpRequestMessage: Codeunit "Http Request Message"; + begin + // [GIVEN] Mock response for an API call + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Initialize the client and make an API call + SharePointGraphClient.GetLists(TempList); + + // [THEN] Request URI should be correct + SharePointGraphTestLibrary.GetHttpRequestMessage(HttpRequestMessage); + LibraryAssert.IsTrue(HttpRequestMessage.GetRequestUri().Contains('https://graph.microsoft.com/v1.0/sites/'), 'Request URI should contain the correct endpoint'); + end; + + [Test] + procedure TestGetLists() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetLists + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetLists + SharePointGraphResponse := SharePointGraphClient.GetLists(TempList); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetLists should succeed'); + LibraryAssert.AreEqual(2, TempList.Count(), 'Should return 2 lists'); + + TempList.FindFirst(); + LibraryAssert.AreEqual('Test Documents', TempList.DisplayName, 'DisplayName should match'); + LibraryAssert.AreEqual('Test library for documents', TempList.Description, 'Description should match'); + + TempList.FindLast(); + LibraryAssert.AreEqual('HR Documents', TempList.DisplayName, 'DisplayName should match'); + end; + + [Test] + procedure TestCreateList() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CreateList + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetCreateListResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CreateList + SharePointGraphResponse := SharePointGraphClient.CreateList('New Test List', 'Created for testing', TempList); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CreateList should succeed'); + LibraryAssert.AreEqual('New Test List', TempList.DisplayName, 'DisplayName should match'); + LibraryAssert.AreEqual('Created for testing', TempList.Description, 'Description should match'); + LibraryAssert.AreEqual('01bjtwww-5j35-426b-a4d5-608f6e2a9f84', TempList.Id, 'Id should match'); + end; + + [Test] + procedure TestGetListItems() + var + TempListItem: Record "SharePoint Graph List Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetListItems + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetListItemsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetListItems + SharePointGraphResponse := SharePointGraphClient.GetListItems('01bjtwww-5j35-426b-a4d5-608f6e2a9f84', TempListItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetListItems should succeed'); + LibraryAssert.AreEqual(2, TempListItem.Count(), 'Should return 2 list items'); + + TempListItem.FindFirst(); + LibraryAssert.AreEqual('Test Item 1', TempListItem.Title, 'Title should match'); + + TempListItem.FindLast(); + LibraryAssert.AreEqual('Test Item 2', TempListItem.Title, 'Title should match'); + end; + + [Test] + procedure TestCreateListItem() + var + TempListItem: Record "SharePoint Graph List Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + FieldsJson: JsonObject; + begin + // [GIVEN] Mock response for CreateListItem + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetCreateListItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CreateListItem + FieldsJson.Add('Title', 'New Test Item'); + SharePointGraphResponse := SharePointGraphClient.CreateListItem('01bjtwww-5j35-426b-a4d5-608f6e2a9f84', FieldsJson, TempListItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CreateListItem should succeed'); + LibraryAssert.AreEqual('New Test Item', TempListItem.Title, 'Title should match'); + LibraryAssert.AreEqual('3', TempListItem.Id, 'Id should match'); + end; + + [Test] + procedure TestGetDrives() + var + TempDrive: Record "SharePoint Graph Drive" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetDrives + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDrivesResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDrives + SharePointGraphResponse := SharePointGraphClient.GetDrives(TempDrive); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDrives should succeed'); + LibraryAssert.AreEqual(2, TempDrive.Count(), 'Should return 2 drives'); + + TempDrive.FindFirst(); + LibraryAssert.AreEqual('Documents', TempDrive.Name, 'Name should match'); + LibraryAssert.AreEqual('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8', TempDrive.Id, 'Id should match'); + + TempDrive.FindLast(); + LibraryAssert.AreEqual('HR Files', TempDrive.Name, 'Name should match'); + end; + + [Test] + procedure TestGetRootItems() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetRootItems + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetRootItems + SharePointGraphResponse := SharePointGraphClient.GetRootItems(TempDriveItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetRootItems should succeed'); + LibraryAssert.AreEqual(2, TempDriveItem.Count(), 'Should return 2 items'); + + TempDriveItem.FindFirst(); + LibraryAssert.AreEqual('Folder 1', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsTrue(TempDriveItem.IsFolder, 'Should be a folder'); + + TempDriveItem.FindLast(); + LibraryAssert.AreEqual('Document.docx', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsFalse(TempDriveItem.IsFolder, 'Should be a file'); + end; + + [Test] + procedure TestCreateFolder() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CreateFolder + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetCreateFolderResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CreateFolder + SharePointGraphResponse := SharePointGraphClient.CreateFolder('Documents', 'New Folder', TempDriveItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CreateFolder should succeed'); + LibraryAssert.AreEqual('New Folder', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsTrue(TempDriveItem.IsFolder, 'Should be a folder'); + LibraryAssert.AreEqual('01EZJNRYOELVX64AZW4BC2WGFBGY2D2MAE', TempDriveItem.Id, 'Id should match'); + end; + + [Test] + procedure TestCreateFolderToSpecificDrive() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CreateFolder to specific drive + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetCreateFolderResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CreateFolder to a specific drive + SharePointGraphResponse := SharePointGraphClient.CreateFolder('b!specificDriveId123', 'Documents', 'New Folder', TempDriveItem); + + // [THEN] Operation should succeed + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CreateFolder to specific drive should succeed'); + LibraryAssert.AreEqual('New Folder', TempDriveItem.Name, 'Name should match'); + end; + + [Test] + procedure TestCreateListItemWithTitle() + var + TempListItem: Record "SharePoint Graph List Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for CreateListItem with title + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetCreateListItemResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling CreateListItem with simple title + SharePointGraphResponse := SharePointGraphClient.CreateListItem('01bjtwww-5j35-426b-a4d5-608f6e2a9f84', 'New Test Item', TempListItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'CreateListItem with title should succeed'); + LibraryAssert.AreEqual('New Test Item', TempListItem.Title, 'Title should match'); + end; + + [Test] + procedure TestErrorResponse() + var + TempList: Record "SharePoint Graph List" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + SharePointHttpDiagnostics: Interface "HTTP Diagnostics"; + begin + // [GIVEN] Mock error response + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(401); + MockHttpContent := HttpContent.Create(GetErrorResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpResponseMessage.SetReasonPhrase('Unauthorized'); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling API + SharePointGraphResponse := SharePointGraphClient.GetLists(TempList); + + // [THEN] Operation should fail and return correct error info + LibraryAssert.IsFalse(SharePointGraphResponse.IsSuccessful(), 'GetLists should fail'); + SharePointHttpDiagnostics := SharePointGraphClient.GetDiagnostics(); + LibraryAssert.AreEqual(401, SharePointHttpDiagnostics.GetHttpStatusCode(), 'Status code should match'); + LibraryAssert.AreEqual('Unauthorized', SharePointHttpDiagnostics.GetResponseReasonPhrase(), 'Reason phrase should match'); + end; + + local procedure Initialize() + var + MockHttpClientHandler: Interface "Http Client Handler"; + begin + if IsInitialized then + exit; + + // Get the mock handler from the test library + MockHttpClientHandler := SharePointGraphTestLibrary.GetMockHandler(); + + // Initialize with the mock handler + SharePointGraphClient.Initialize(SharePointUrlLbl, Enum::"Graph API Version"::"v1.0", SharePointGraphAuthSpy, MockHttpClientHandler); + + // Set test IDs to prevent HTTP calls for site and drive discovery + SharePointGraphClient.SetSiteIdForTesting('contoso.sharepoint.com,e6991d99-75d5-4be4-4ede-2c82b1d40cd6,1b58abad-4105-4125-a0e0-7a6d39571a5b'); + SharePointGraphClient.SetDefaultDriveIdForTesting('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8'); + + IsInitialized := true; + end; + + local procedure GetListsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.list)",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01bjtwww-5j35-426b-a4d5-608f6e2a9f84",'); + ResponseText.Append(' "displayName": "Test Documents",'); + ResponseText.Append(' "description": "Test library for documents",'); + ResponseText.Append(' "list": {'); + ResponseText.Append(' "template": "documentLibrary",'); + ResponseText.Append(' "hidden": false,'); + ResponseText.Append(' "contentTypesEnabled": true'); + ResponseText.Append(' },'); + ResponseText.Append(' "createdDateTime": "2022-05-23T12:16:04Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents"'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "27c78f81-f4d9-4ee9-85bd-5d57ade1b5f4",'); + ResponseText.Append(' "displayName": "HR Documents",'); + ResponseText.Append(' "description": "HR library for documents",'); + ResponseText.Append(' "list": {'); + ResponseText.Append(' "template": "documentLibrary",'); + ResponseText.Append(' "hidden": false,'); + ResponseText.Append(' "contentTypesEnabled": true'); + ResponseText.Append(' },'); + ResponseText.Append(' "createdDateTime": "2022-05-23T12:16:04Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/HR%20Documents"'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetCreateListResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(''root'')/lists/$entity",'); + ResponseText.Append(' "id": "01bjtwww-5j35-426b-a4d5-608f6e2a9f84",'); + ResponseText.Append(' "displayName": "New Test List",'); + ResponseText.Append(' "description": "Created for testing",'); + ResponseText.Append(' "list": {'); + ResponseText.Append(' "template": "genericList",'); + ResponseText.Append(' "hidden": false,'); + ResponseText.Append(' "contentTypesEnabled": true'); + ResponseText.Append(' },'); + ResponseText.Append(' "createdDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Lists/New%20Test%20List"'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetListItemsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(''root'')/lists(''01bjtwww-5j35-426b-a4d5-608f6e2a9f84'')/items",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "1",'); + ResponseText.Append(' "createdDateTime": "2023-05-15T08:12:39Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-06-20T14:45:12Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Lists/Test%20List/1_.000",'); + ResponseText.Append(' "fields": {'); + ResponseText.Append(' "Title": "Test Item 1",'); + ResponseText.Append(' "Description": "This is a test item",'); + ResponseText.Append(' "Priority": "High"'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "2",'); + ResponseText.Append(' "createdDateTime": "2023-05-20T09:21:17Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-06-21T10:15:48Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Lists/Test%20List/2_.000",'); + ResponseText.Append(' "fields": {'); + ResponseText.Append(' "Title": "Test Item 2",'); + ResponseText.Append(' "Description": "This is another test item",'); + ResponseText.Append(' "Priority": "Medium"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetCreateListItemResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(''root'')/lists(''01bjtwww-5j35-426b-a4d5-608f6e2a9f84'')/items/$entity",'); + ResponseText.Append(' "id": "3",'); + ResponseText.Append(' "createdDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Lists/Test%20List/3_.000",'); + ResponseText.Append(' "fields": {'); + ResponseText.Append(' "Title": "New Test Item"'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetDrivesResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(''root'')/drives",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8",'); + ResponseText.Append(' "name": "Documents",'); + ResponseText.Append(' "driveType": "documentLibrary",'); + ResponseText.Append(' "createdDateTime": "2021-08-17T21:43:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents"'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c9",'); + ResponseText.Append(' "name": "HR Files",'); + ResponseText.Append(' "driveType": "documentLibrary",'); + ResponseText.Append(' "createdDateTime": "2021-08-17T21:43:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/HR%20Files"'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetDriveItemsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#sites(''root'')/drives(''b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8'')/root/children",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM",'); + ResponseText.Append(' "name": "Folder 1",'); + ResponseText.Append(' "createdDateTime": "2022-08-10T14:24:11Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-03-15T09:58:42Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201",'); + ResponseText.Append(' "folder": {'); + ResponseText.Append(' "childCount": 3'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ",'); + ResponseText.Append(' "name": "Document.docx",'); + ResponseText.Append(' "createdDateTime": "2022-09-05T10:12:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-05-20T11:42:18Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Document.docx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "KU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 12345'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetCreateFolderResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems/$entity",'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BC2WGFBGY2D2MAE",'); + ResponseText.Append(' "name": "New Folder",'); + ResponseText.Append(' "createdDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/New%20Folder",'); + ResponseText.Append(' "folder": {'); + ResponseText.Append(' "childCount": 0'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetErrorResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "error": {'); + ResponseText.Append(' "code": "InvalidAuthenticationToken",'); + ResponseText.Append(' "message": "Access token has expired or is not yet valid.",'); + ResponseText.Append(' "innerError": {'); + ResponseText.Append(' "date": "2023-07-15T12:00:00",'); + ResponseText.Append(' "request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede",'); + ResponseText.Append(' "client-request-id": "3b2d1e5f-fb1c-41a1-90e2-1fc8ae4ebede"'); + ResponseText.Append(' }'); + ResponseText.Append(' }'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + +} \ No newline at end of file diff --git a/src/System Application/Test/SharePoint/src/graph/SharePointGraphFileTest.Codeunit.al b/src/System Application/Test/SharePoint/src/graph/SharePointGraphFileTest.Codeunit.al new file mode 100644 index 0000000000..2dde38a867 --- /dev/null +++ b/src/System Application/Test/SharePoint/src/graph/SharePointGraphFileTest.Codeunit.al @@ -0,0 +1,300 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Integration.Sharepoint; + +using System.Integration.Graph; +using System.Integration.Sharepoint; +using System.RestClient; +using System.TestLibraries.Utilities; +using System.Utilities; + +codeunit 132983 "SharePoint Graph File Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + SharePointGraphAuthSpy: Codeunit "SharePoint Graph Auth Spy"; + SharePointGraphTestLibrary: Codeunit "SharePoint Graph Test Library"; + SharePointGraphClient: Codeunit "SharePoint Graph Client"; + LibraryAssert: Codeunit "Library Assert"; + SharePointUrlLbl: Label 'https://contoso.sharepoint.com/sites/test', Locked = true; + IsInitialized: Boolean; + + [Test] + procedure TestUploadFile() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + TempBlob: Codeunit "Temp Blob"; + FileInStream: InStream; + FileOutStream: OutStream; + ExpectedSize: BigInteger; + begin + // [GIVEN] Mock response for UploadFile + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(201); + MockHttpContent := HttpContent.Create(GetUploadFileResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Preparing a file and calling UploadFile + TempBlob.CreateOutStream(FileOutStream); + FileOutStream.WriteText('Test content for uploaded file'); + TempBlob.CreateInStream(FileInStream); + + SharePointGraphResponse := SharePointGraphClient.UploadFile('Documents', 'Test.txt', FileInStream, TempDriveItem); + + // [THEN] Operation should succeed and return correct data + ExpectedSize := 25; + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'UploadFile should succeed'); + LibraryAssert.AreEqual('Test.txt', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsFalse(TempDriveItem.IsFolder, 'Should be a file'); + LibraryAssert.AreEqual('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempDriveItem.Id, 'Id should match'); + LibraryAssert.AreEqual(ExpectedSize, TempDriveItem.Size, 'Size should match'); + end; + + [Test] + procedure TestDownloadFile() + var + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + TempBlob: Codeunit "Temp Blob"; + FileInStream: InStream; + Content: Text; + begin + // [GIVEN] Mock response for DownloadFile + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create('Downloaded file content'); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling DownloadFile + SharePointGraphResponse := SharePointGraphClient.DownloadFile('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempBlob); + + // [THEN] Operation should succeed and return the file content + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'DownloadFile should succeed'); + TempBlob.CreateInStream(FileInStream); + FileInStream.ReadText(Content); + LibraryAssert.AreEqual('Downloaded file content', Content, 'File content should match'); + end; + + [Test] + procedure TestDownloadFileByPath() + var + TempBlob: Codeunit "Temp Blob"; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + FileInStream: InStream; + Content: Text; + begin + // [GIVEN] Mock response for DownloadFileByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create('Downloaded file content by path'); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling DownloadFileByPath + SharePointGraphResponse := SharePointGraphClient.DownloadFileByPath('Documents/Test.txt', TempBlob); + + // [THEN] Operation should succeed and return the file content + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'DownloadFileByPath should succeed'); + TempBlob.CreateInStream(FileInStream); + FileInStream.ReadText(Content); + LibraryAssert.AreEqual('Downloaded file content by path', Content, 'File content should match'); + end; + + [Test] + procedure TestGetDriveItemByPath() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + ExpectedSize: BigInteger; + begin + // [GIVEN] Mock response for GetDriveItemByPath + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetDriveItemByPathResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetDriveItemByPath + SharePointGraphResponse := SharePointGraphClient.GetDriveItemByPath('Documents/Report.docx', TempDriveItem); + + // [THEN] Operation should succeed and return correct data + ExpectedSize := 45321; + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetDriveItemByPath should succeed'); + LibraryAssert.AreEqual('Report.docx', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsFalse(TempDriveItem.IsFolder, 'Should be a file'); + LibraryAssert.AreEqual('01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ', TempDriveItem.Id, 'Id should match'); + LibraryAssert.AreEqual(ExpectedSize, TempDriveItem.Size, 'Size should match'); + end; + + [Test] + procedure TestGetFolderItems() + var + TempDriveItem: Record "SharePoint Graph Drive Item" temporary; + HttpContent: Codeunit "Http Content"; + MockHttpContent: Codeunit "Http Content"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + SharePointGraphResponse: Codeunit "SharePoint Graph Response"; + begin + // [GIVEN] Mock response for GetFolderItems + Initialize(); + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetFolderItemsResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + SharePointGraphTestLibrary.SetMockResponse(MockHttpResponseMessage); + + // [WHEN] Calling GetFolderItems + SharePointGraphResponse := SharePointGraphClient.GetFolderItems('01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM', TempDriveItem); + + // [THEN] Operation should succeed and return correct data + LibraryAssert.IsTrue(SharePointGraphResponse.IsSuccessful(), 'GetFolderItems should succeed'); + LibraryAssert.AreEqual(3, TempDriveItem.Count(), 'Should return 3 items'); + + TempDriveItem.FindSet(); + LibraryAssert.AreEqual('Subfolder', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsTrue(TempDriveItem.IsFolder, 'Should be a folder'); + + TempDriveItem.Next(); + LibraryAssert.AreEqual('Presentation.pptx', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsFalse(TempDriveItem.IsFolder, 'Should be a file'); + + TempDriveItem.Next(); + LibraryAssert.AreEqual('Budget.xlsx', TempDriveItem.Name, 'Name should match'); + LibraryAssert.IsFalse(TempDriveItem.IsFolder, 'Should be a file'); + end; + + local procedure Initialize() + var + MockHttpClientHandler: Interface "Http Client Handler"; + begin + if IsInitialized then + exit; + + // Get the mock handler from the test library + MockHttpClientHandler := SharePointGraphTestLibrary.GetMockHandler(); + + // Initialize with the mock handler + SharePointGraphClient.Initialize(SharePointUrlLbl, Enum::"Graph API Version"::"v1.0", SharePointGraphAuthSpy, MockHttpClientHandler); + + // Set test IDs to prevent HTTP calls for site and drive discovery + SharePointGraphClient.SetSiteIdForTesting('contoso.sharepoint.com,e6991d99-75d5-4be4-4ede-2c82b1d40cd6,1b58abad-4105-4125-a0e0-7a6d39571a5b'); + SharePointGraphClient.SetDefaultDriveIdForTesting('b!mR2-5tV1S-RO3C82s1DNbdCrWBwFQKFUoOB6bTlXClvD9fcjLXO5TbNk5sDyD7c8'); + + IsInitialized := true; + end; + + local procedure GetUploadFileResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems/$entity",'); + ResponseText.Append(' "id": "01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ",'); + ResponseText.Append(' "name": "Test.txt",'); + ResponseText.Append(' "createdDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-07-15T10:31:30Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Test.txt",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "text/plain",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "KU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 25'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetDriveItemByPathResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems/$entity",'); + ResponseText.Append(' "id": "01EZJNRYQYENJ6SXVPCNBYA3QZRHKJWLNZ",'); + ResponseText.Append(' "name": "Report.docx",'); + ResponseText.Append(' "createdDateTime": "2023-05-10T14:25:37Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-06-20T09:42:13Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Report.docx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "dF5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 45321'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; + + local procedure GetFolderItemsResponse(): Text + var + ResponseText: TextBuilder; + begin + ResponseText.Append('{'); + ResponseText.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#driveItems(''01EZJNRYOELVX64AZW4BA3DHJXMFBQZXPM'')/children",'); + ResponseText.Append(' "value": ['); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP1",'); + ResponseText.Append(' "name": "Subfolder",'); + ResponseText.Append(' "createdDateTime": "2022-09-15T10:12:32Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-03-20T14:35:16Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201/Subfolder",'); + ResponseText.Append(' "folder": {'); + ResponseText.Append(' "childCount": 1'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP2",'); + ResponseText.Append(' "name": "Presentation.pptx",'); + ResponseText.Append(' "createdDateTime": "2022-10-05T11:42:18Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-05-12T15:27:39Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201/Presentation.pptx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "TU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 87621'); + ResponseText.Append(' },'); + ResponseText.Append(' {'); + ResponseText.Append(' "id": "01EZJNRYOELVX64AZW4BA3DHJXMFBQZXP3",'); + ResponseText.Append(' "name": "Budget.xlsx",'); + ResponseText.Append(' "createdDateTime": "2022-11-10T09:33:44Z",'); + ResponseText.Append(' "lastModifiedDateTime": "2023-06-05T16:19:22Z",'); + ResponseText.Append(' "webUrl": "https://contoso.sharepoint.com/sites/test/Shared%20Documents/Folder%201/Budget.xlsx",'); + ResponseText.Append(' "file": {'); + ResponseText.Append(' "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",'); + ResponseText.Append(' "hashes": {'); + ResponseText.Append(' "quickXorHash": "JU5GC7lcTJbHDrcPKJc8rJtEhCo="'); + ResponseText.Append(' }'); + ResponseText.Append(' },'); + ResponseText.Append(' "size": 52347'); + ResponseText.Append(' }'); + ResponseText.Append(' ]'); + ResponseText.Append('}'); + exit(ResponseText.ToText()); + end; +} \ No newline at end of file