From b5a53781133464d26f7e0ade8ba5e33fa58ba390 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 31 Aug 2022 13:28:57 -0400 Subject: [PATCH] GC: Restore: Retrieve CalendarID Feature (#687) Feature added to be able to retrieve containerID for `exchange.Event` types. --- src/internal/connector/exchange/calendar.go | 28 ++++++ .../exchange/exchange_service_test.go | 95 ++++++++++++++----- .../exchange/optionidentifier_string.go | 13 +-- .../connector/exchange/query_options.go | 52 +++++++++- .../connector/exchange/service_functions.go | 32 ++++--- .../connector/exchange/service_iterators.go | 51 +++++----- .../connector/exchange/service_query.go | 9 ++ 7 files changed, 209 insertions(+), 71 deletions(-) create mode 100644 src/internal/connector/exchange/calendar.go diff --git a/src/internal/connector/exchange/calendar.go b/src/internal/connector/exchange/calendar.go new file mode 100644 index 000000000..f29ac52e4 --- /dev/null +++ b/src/internal/connector/exchange/calendar.go @@ -0,0 +1,28 @@ +package exchange + +import ( + "github.com/microsoftgraph/msgraph-sdk-go/models" +) + +// calendarDisplayable is a transformative struct that aligns +// models.Calendarable interface with the displayable interface. +type calendarDisplayable struct { + models.Calendarable +} + +// GetDisplayName returns the *string of the calendar name +func (c calendarDisplayable) GetDisplayName() *string { + return c.GetName() +} + +// CreateCalendarDisplayable helper function to create the +// calendarDisplayable during msgraph-sdk-go iterative process +// @param entry is the input supplied by pageIterator.Iterate() +func CreateCalendarDisplayable(entry any) *calendarDisplayable { + calendar, ok := entry.(models.Calendarable) + if !ok { + return nil + } + + return &calendarDisplayable{calendar} +} diff --git a/src/internal/connector/exchange/exchange_service_test.go b/src/internal/connector/exchange/exchange_service_test.go index 696bf6f39..7cf3b5cb1 100644 --- a/src/internal/connector/exchange/exchange_service_test.go +++ b/src/internal/connector/exchange/exchange_service_test.go @@ -86,6 +86,41 @@ func (suite *ExchangeServiceSuite) TestCreateService() { } } +func (suite *ExchangeServiceSuite) TestOptionsForCalendars() { + tests := []struct { + name string + params []string + checkError assert.ErrorAssertionFunc + }{ + { + name: "Empty Literal", + params: []string{}, + checkError: assert.NoError, + }, + { + name: "Invalid Parameter", + params: []string{"status"}, + checkError: assert.Error, + }, + { + name: "Invalid Parameters", + params: []string{"status", "height", "month"}, + checkError: assert.Error, + }, + { + name: "Valid Parameters", + params: []string{"changeKey", "events", "owner"}, + checkError: assert.NoError, + }, + } + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + _, err := optionsForCalendars(test.params) + test.checkError(t, err) + }) + } +} + // TestOptionsForMessages checks to ensure approved query // options are added to the type specific RequestBuildConfiguration. Expected // will be +1 on all select parameters @@ -255,6 +290,10 @@ func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() { name: "GraphQuery: Get All Events for User", function: GetAllEventsForUser, }, + { + name: "GraphQuery: Get All Calendars for User", + function: GetAllCalendarNamesForUser, + }, } for _, test := range tests { @@ -311,49 +350,61 @@ func (suite *ExchangeServiceSuite) TestParseCalendarIDFromEvent() { // TestGetMailFolderID verifies the ability to retrieve folder ID of folders // at the top level of the file tree -func (suite *ExchangeServiceSuite) TestGetFolderID() { +func (suite *ExchangeServiceSuite) TestGetContainerID() { userID := tester.M365UserID(suite.T()) tests := []struct { - name string - folderName string + name string + containerName string // category references the current optionId :: TODO --> use selector fields category optionIdentifier checkError assert.ErrorAssertionFunc }{ { - name: "Mail Valid", - folderName: "Inbox", - category: messages, - checkError: assert.NoError, + name: "Mail Valid", + containerName: "Inbox", + category: messages, + checkError: assert.NoError, }, { - name: "Mail Invalid", - folderName: "FolderThatIsNotHere", - category: messages, - checkError: assert.Error, + name: "Mail Invalid", + containerName: "FolderThatIsNotHere", + category: messages, + checkError: assert.Error, }, { - name: "Contact Invalid", - folderName: "FolderThatIsNotHereContacts", - category: contacts, - checkError: assert.Error, + name: "Contact Invalid", + containerName: "FolderThatIsNotHereContacts", + category: contacts, + checkError: assert.Error, }, { - name: "Contact Valid", - folderName: "TrialFolder", - category: contacts, - checkError: assert.NoError, + name: "Contact Valid", + containerName: "TrialFolder", + category: contacts, + checkError: assert.NoError, + }, + { + name: "Event Invalid", + containerName: "NotAValid?@V'vCalendar", + category: events, + checkError: assert.Error, + }, + { + name: "Event Valid", + containerName: "Calendar", + category: events, + checkError: assert.NoError, }, } for _, test := range tests { suite.T().Run(test.name, func(t *testing.T) { - _, err := GetFolderID( + _, err := GetContainerID( suite.es, - test.folderName, + test.containerName, userID, test.category) - test.checkError(t, err, "Unable to find folder: "+test.folderName) + test.checkError(t, err, "error with container: "+test.containerName) }) } } diff --git a/src/internal/connector/exchange/optionidentifier_string.go b/src/internal/connector/exchange/optionidentifier_string.go index e549bd0ec..7e93a06e3 100644 --- a/src/internal/connector/exchange/optionidentifier_string.go +++ b/src/internal/connector/exchange/optionidentifier_string.go @@ -10,15 +10,16 @@ func _() { var x [1]struct{} _ = x[unknown-0] _ = x[folders-1] - _ = x[events-2] - _ = x[messages-3] - _ = x[users-4] - _ = x[contacts-5] + _ = x[calendars-2] + _ = x[events-3] + _ = x[messages-4] + _ = x[users-5] + _ = x[contacts-6] } -const _optionIdentifier_name = "unknownfolderseventsmessagesuserscontacts" +const _optionIdentifier_name = "unknownfolderscalendarseventsmessagesuserscontacts" -var _optionIdentifier_index = [...]uint8{0, 7, 14, 20, 28, 33, 41} +var _optionIdentifier_index = [...]uint8{0, 7, 14, 23, 29, 37, 42, 50} func (i optionIdentifier) String() string { if i < 0 || i >= optionIdentifier(len(_optionIdentifier_index)-1) { diff --git a/src/internal/connector/exchange/query_options.go b/src/internal/connector/exchange/query_options.go index bcddb7e7b..93177b29a 100644 --- a/src/internal/connector/exchange/query_options.go +++ b/src/internal/connector/exchange/query_options.go @@ -1,7 +1,10 @@ package exchange import ( + "fmt" + msuser "github.com/microsoftgraph/msgraph-sdk-go/users" + mscalendars "github.com/microsoftgraph/msgraph-sdk-go/users/item/calendars" mscontactfolder "github.com/microsoftgraph/msgraph-sdk-go/users/item/contactfolders" mscontacts "github.com/microsoftgraph/msgraph-sdk-go/users/item/contacts" msevents "github.com/microsoftgraph/msgraph-sdk-go/users/item/events" @@ -17,6 +20,15 @@ import ( // selectors for M365 objects //------------------------------------------------------------ var ( + fieldsForCalendars = map[string]int{ + "changeKey": 1, + "events": 2, + "id": 3, + "isDefaultCalendar": 4, + "name": 5, + "owner": 6, + } + fieldsForEvents = map[string]int{ "calendar": 1, "end": 2, @@ -79,6 +91,7 @@ type optionIdentifier int const ( unknown optionIdentifier = iota folders + calendars events messages users @@ -104,9 +117,12 @@ func categoryToOptionIdentifier(category string) optionIdentifier { } } -//--------------------------------------------------- +//--------------------------------------------------------------------------- // exchange.Query Option Section -//------------------------------------------------ +// These functions can be used to filter a response on M365 +// Graph queries and reduce / filter the amount of data returned +// which reduces the overall latency of complex calls +//---------------------------------------------------------------- // optionsForMessages - used to select allowable options for exchange.Mail types // @param moreOps is []string of options(e.g. "parentFolderId, subject") @@ -146,6 +162,30 @@ func OptionsForSingleMessage(moreOps []string) (*msitem.MessageItemRequestBuilde return options, nil } +// optionsForCalendars places allowed options for exchange.Calendar object +// @param moreOps should reflect elements from fieldsForCalendars +// @return is first call in Calendars().GetWithRequestConfigurationAndResponseHandler +func optionsForCalendars(moreOps []string) ( + *mscalendars.CalendarsRequestBuilderGetRequestConfiguration, + error, +) { + selecting, err := buildOptions(moreOps, calendars) + if err != nil { + return nil, err + } + + requestParams := &mscalendars.CalendarsRequestBuilderGetQueryParameters{ + Select: selecting, + } + options := &mscalendars.CalendarsRequestBuilderGetRequestConfiguration{ + QueryParameters: requestParams, + } + + return options, nil +} + +// optionsForContactFolders places allowed options for exchange.ContactFolder object +// @return is first call in ContactFolders().GetWithRequestConfigurationAndResponseHandler func optionsForContactFolders(moreOps []string) ( *mscontactfolder.ContactFoldersRequestBuilderGetRequestConfiguration, error, @@ -246,10 +286,12 @@ func buildOptions(options []string, optID optionIdentifier) ([]string, error) { ) switch optID { - case events: - allowedOptions = fieldsForEvents + case calendars: + allowedOptions = fieldsForCalendars case contacts: allowedOptions = fieldsForContacts + case events: + allowedOptions = fieldsForEvents case folders: allowedOptions = fieldsForFolders case users: @@ -265,7 +307,7 @@ func buildOptions(options []string, optID optionIdentifier) ([]string, error) { for _, entry := range options { _, ok := allowedOptions[entry] if !ok { - return nil, errors.New("unsupported option") + return nil, fmt.Errorf("unsupported option: %v", entry) } returnedOptions = append(returnedOptions, entry) diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index 224b3616e..954b4a27c 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -151,16 +151,17 @@ func GetAllMailFolders(gs graph.Service, user, nameContains string) ([]MailFolde return mfs, err } -// GetFolderID query function to retrieve the M365 ID based on the folder's displayName. -// @param folderName the target folder's display name. Case sensitive +// GetContainerID query function to retrieve a container's M365 ID. +// @param containerName is the target's name, user-readable and case sensitive // @param category switches query and iteration to support multiple exchange applications // @returns a *string if the folder exists. If the folder does not exist returns nil, error-> folder not found -func GetFolderID(service graph.Service, folderName, user string, category optionIdentifier) (*string, error) { +func GetContainerID(service graph.Service, containerName, user string, category optionIdentifier) (*string, error) { var ( - errs error - folderID *string - query GraphQuery - transform absser.ParsableFactory + errs error + targetID *string + query GraphQuery + transform absser.ParsableFactory + isCalendar bool ) switch category { @@ -170,6 +171,10 @@ func GetFolderID(service graph.Service, folderName, user string, category option case contacts: query = GetAllContactFolderNamesForUser transform = models.CreateContactFolderFromDiscriminatorValue + case events: + query = GetAllCalendarNamesForUser + transform = models.CreateCalendarCollectionResponseFromDiscriminatorValue + isCalendar = true default: return nil, fmt.Errorf("unsupported category %s for GetFolderID()", category) } @@ -192,10 +197,11 @@ func GetFolderID(service graph.Service, folderName, user string, category option return nil, err } - callbackFunc := iterateFindFolderID(category, - &folderID, - folderName, + callbackFunc := iterateFindFolderID( + &targetID, + containerName, service.Adapter().GetBaseUrl(), + isCalendar, errs, ) @@ -203,11 +209,11 @@ func GetFolderID(service graph.Service, folderName, user string, category option return nil, support.WrapAndAppend(service.Adapter().GetBaseUrl(), err, errs) } - if folderID == nil { + if targetID == nil { return nil, ErrFolderNotFound } - return folderID, errs + return targetID, errs } // parseCalendarIDFromEvent returns the M365 ID for a calendar @@ -298,7 +304,7 @@ func establishFolder( folderName, user string, optID optionIdentifier, ) (string, error) { - folderID, err := GetFolderID(service, folderName, user, optID) + folderID, err := GetContainerID(service, folderName, user, optID) if err == nil { return *folderID, nil } diff --git a/src/internal/connector/exchange/service_iterators.go b/src/internal/connector/exchange/service_iterators.go index d66e66585..fd48a8ba7 100644 --- a/src/internal/connector/exchange/service_iterators.go +++ b/src/internal/connector/exchange/service_iterators.go @@ -305,44 +305,45 @@ func IterateFilterFolderDirectoriesForCollections( // the displayable interface. If folder exists, the function updates the // folderID memory address that was passed in. func iterateFindFolderID( - category optionIdentifier, folderID **string, folderName, errorIdentifier string, + isCalendar bool, errs error, ) func(any) bool { return func(entry any) bool { - switch category { - case messages, contacts: - folder, ok := entry.(displayable) - if !ok { - errs = support.WrapAndAppend( - errorIdentifier, - errors.New("struct does not implement displayable"), - errs, - ) + if isCalendar { + entry = CreateCalendarDisplayable(entry) + if entry == nil { return true } - // Display name not set on folder - if folder.GetDisplayName() == nil { - return true - } + } - name := *folder.GetDisplayName() - if folderName == name { - if folder.GetId() == nil { - return true // invalid folder - } - - *folderID = folder.GetId() - - return false - } + folder, ok := entry.(displayable) + if !ok { + errs = support.WrapAndAppend( + errorIdentifier, + errors.New("struct does not implement displayable"), + errs, + ) return true + } + // Display name not set on folder + if folder.GetDisplayName() == nil { + return true + } + + if folderName == *folder.GetDisplayName() { + if folder.GetId() == nil { + return true // invalid folder + } + + *folderID = folder.GetId() - default: return false } + + return true } } diff --git a/src/internal/connector/exchange/service_query.go b/src/internal/connector/exchange/service_query.go index f0a93a8e4..f43567205 100644 --- a/src/internal/connector/exchange/service_query.go +++ b/src/internal/connector/exchange/service_query.go @@ -55,6 +55,15 @@ func GetAllFolderNamesForUser(gs graph.Service, user string) (absser.Parsable, e return gs.Client().UsersById(user).MailFolders().GetWithRequestConfigurationAndResponseHandler(options, nil) } +func GetAllCalendarNamesForUser(gs graph.Service, user string) (absser.Parsable, error) { + options, err := optionsForCalendars([]string{"name"}) + if err != nil { + return nil, err + } + + return gs.Client().UsersById(user).Calendars().GetWithRequestConfigurationAndResponseHandler(options, nil) +} + // GetAllContactFolderNamesForUser is a GraphQuery function for getting ContactFolderId // and display names for contacts. All other information is omitted. // Does not return the primary Contact Folder