GC: Use of graph.ContainerResolver for directory structure and data retrieval (#1134)
## Description `graph.ContainerResolver` has the capacity to keep the directory structure of the m365 objects that are helpful within a user's account. Leveraging this abstraction allows for a better flow of data from M365 into storage. ## Type of change - [x] ⚡ : Optimization ## Issue(s) * closes #1125<issue> * closes #1122 ## Test Plan - [x] ⚡ Unit test
This commit is contained in:
parent
d30f6963c0
commit
bb7e48a82e
@ -17,7 +17,6 @@ import (
|
||||
"github.com/alcionai/corso/src/pkg/account"
|
||||
"github.com/alcionai/corso/src/pkg/control"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/selectors"
|
||||
)
|
||||
|
||||
type ExchangeServiceSuite struct {
|
||||
@ -233,37 +232,6 @@ func (suite *ExchangeServiceSuite) TestOptionsForContacts() {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetupExchangeCollection ensures SetupExchangeCollectionVars returns a non-nil variable for
|
||||
// the following selector types:
|
||||
// - Mail
|
||||
// - Contacts
|
||||
// - Events
|
||||
func (suite *ExchangeServiceSuite) TestSetupExchangeCollection() {
|
||||
userID := tester.M365UserID(suite.T())
|
||||
sel := selectors.NewExchangeBackup()
|
||||
// Exchange mail uses a different system to fetch items. Right now the old
|
||||
// function for it will return an error so we know if it gets called.
|
||||
sel.Include(
|
||||
sel.ContactFolders([]string{userID}, selectors.Any()),
|
||||
sel.EventCalendars([]string{userID}, selectors.Any()),
|
||||
)
|
||||
|
||||
eb, err := sel.ToExchangeBackup()
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
scopes := eb.Scopes()
|
||||
|
||||
for _, test := range scopes {
|
||||
suite.T().Run(test.Category().String(), func(t *testing.T) {
|
||||
discriminateFunc, graphQuery, iterFunc, err := SetupExchangeCollectionVars(test)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, discriminateFunc)
|
||||
assert.NotNil(t, graphQuery)
|
||||
assert.NotNil(t, iterFunc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGraphQueryFunctions verifies if Query functions APIs
|
||||
// through Microsoft Graph are functional
|
||||
func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() {
|
||||
|
||||
@ -68,10 +68,12 @@ func loadService(t *testing.T) *exchangeService {
|
||||
return service
|
||||
}
|
||||
|
||||
// TestIterativeFunctions verifies that GraphQuery to Iterate
|
||||
// functions are valid for current versioning of msgraph-go-sdk.
|
||||
// TestCollectionFunctions verifies ability to gather
|
||||
// containers functions are valid for current versioning of msgraph-go-sdk.
|
||||
// Tests for mail have been moved to graph_connector_test.go.
|
||||
func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
|
||||
// exchange.Mail uses a sequential delta function.
|
||||
// TODO: Add exchange.Mail when delta iterator functionality implemented
|
||||
func (suite *ExchangeIteratorSuite) TestCollectionFunctions() {
|
||||
ctx, flush := tester.NewContext()
|
||||
defer flush()
|
||||
|
||||
@ -93,63 +95,39 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queryFunction GraphQuery
|
||||
iterativeFunction GraphIterateFunc
|
||||
queryFunc GraphQuery
|
||||
scope selectors.ExchangeScope
|
||||
transformer absser.ParsableFactory
|
||||
folderNames map[string]struct{}
|
||||
iterativeFunction func(
|
||||
container map[string]graph.Container,
|
||||
aFilter string,
|
||||
errUpdater func(string, error)) func(any) bool
|
||||
transformer absser.ParsableFactory
|
||||
}{
|
||||
{
|
||||
name: "Contacts Iterative Check",
|
||||
queryFunction: GetAllContactFolderNamesForUser,
|
||||
iterativeFunction: IterateSelectAllContactsForCollections,
|
||||
scope: contactScope[0],
|
||||
queryFunc: GetAllContactFolderNamesForUser,
|
||||
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
||||
}, {
|
||||
name: "Contact Folder Traversal",
|
||||
queryFunction: GetAllContactFolderNamesForUser,
|
||||
iterativeFunction: IterateSelectAllContactsForCollections,
|
||||
scope: contactScope[0],
|
||||
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
||||
}, {
|
||||
iterativeFunction: IterativeCollectContactContainers,
|
||||
},
|
||||
{
|
||||
name: "Events Iterative Check",
|
||||
queryFunction: GetAllCalendarNamesForUser,
|
||||
iterativeFunction: IterateSelectAllEventsFromCalendars,
|
||||
scope: eventScope[0],
|
||||
queryFunc: GetAllCalendarNamesForUser,
|
||||
transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue,
|
||||
}, {
|
||||
name: "Folder Iterative Check Contacts",
|
||||
queryFunction: GetAllContactFolderNamesForUser,
|
||||
iterativeFunction: IterateFilterContainersForCollections,
|
||||
scope: contactScope[0],
|
||||
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
||||
}, {
|
||||
name: "Default Contacts Folder",
|
||||
queryFunction: GetDefaultContactFolderForUser,
|
||||
iterativeFunction: IterateSelectAllContactsForCollections,
|
||||
scope: contactScope[0],
|
||||
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
||||
iterativeFunction: IterativeCollectCalendarContainers,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
suite.T().Run(test.name, func(t *testing.T) {
|
||||
service := loadService(t)
|
||||
response, err := test.queryFunction(ctx, service, userID)
|
||||
response, err := test.queryFunc(ctx, service, userID)
|
||||
require.NoError(t, err)
|
||||
// Create Iterator
|
||||
// Iterator Creation
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(response,
|
||||
&service.adapter,
|
||||
test.transformer)
|
||||
require.NoError(t, err)
|
||||
|
||||
qp := graph.QueryParams{
|
||||
User: userID,
|
||||
Scope: test.scope,
|
||||
Credentials: service.credentials,
|
||||
FailFast: false,
|
||||
}
|
||||
// Create collection for iterate test
|
||||
collections := make(map[string]*Collection)
|
||||
collections := make(map[string]graph.Container)
|
||||
var errs error
|
||||
errUpdater := func(id string, err error) {
|
||||
errs = support.WrapAndAppend(id, err, errs)
|
||||
@ -157,14 +135,7 @@ func (suite *ExchangeIteratorSuite) TestIterativeFunctions() {
|
||||
// callbackFunc iterates through all models.Messageable and fills exchange.Collection.jobs[]
|
||||
// with corresponding item IDs. New collections are created for each directory
|
||||
callbackFunc := test.iterativeFunction(
|
||||
ctx,
|
||||
qp,
|
||||
errUpdater,
|
||||
collections,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
collections, "", errUpdater)
|
||||
iterateError := pageIterator.Iterate(ctx, callbackFunc)
|
||||
assert.NoError(t, iterateError)
|
||||
assert.NoError(t, errs)
|
||||
|
||||
@ -101,7 +101,7 @@ func (mc *mailFolderCache) Populate(
|
||||
|
||||
for _, f := range resp.GetValue() {
|
||||
if err := mc.AddToCache(ctx, f); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
errs = multierror.Append(errs, errors.Wrap(err, "delta fetch"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ func (suite *MailFolderCacheUnitSuite) TestCheckRequiredValues() {
|
||||
|
||||
for _, test := range table {
|
||||
suite.T().Run(test.name, func(t *testing.T) {
|
||||
test.check(t, checkRequiredValues(test.c))
|
||||
test.check(t, graph.CheckRequiredValues(test.c))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -366,6 +366,7 @@ func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
|
||||
require.NoError(t, mfc.Populate(ctx, test.root, test.path...))
|
||||
|
||||
p, err := mfc.IDToPath(ctx, testFolderID)
|
||||
t.Logf("Path: %s\n", p.String())
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPath := stdpath.Join(append(test.path, expectedFolderPath)...)
|
||||
|
||||
@ -19,7 +19,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/selectors"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
@ -119,19 +118,6 @@ func categoryToOptionIdentifier(category path.CategoryType) optionIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
func scopeToOptionIdentifier(selector selectors.ExchangeScope) optionIdentifier {
|
||||
switch selector.Category() {
|
||||
case selectors.ExchangeMailFolder, selectors.ExchangeMail:
|
||||
return messages
|
||||
case selectors.ExchangeContactFolder, selectors.ExchangeContact:
|
||||
return contacts
|
||||
case selectors.ExchangeEventCalendar, selectors.ExchangeEvent:
|
||||
return events
|
||||
default:
|
||||
return unknown
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// exchange.Query Option Section
|
||||
// These functions can be used to filter a response on M365
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
absser "github.com/microsoft/kiota-abstractions-go/serialization"
|
||||
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
|
||||
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
@ -144,18 +143,18 @@ func GetAllMailFolders(
|
||||
) ([]graph.CachedContainer, error) {
|
||||
containers := make([]graph.CachedContainer, 0)
|
||||
|
||||
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.EmailCategory)
|
||||
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EmailCategory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range resolver.Items() {
|
||||
directories := c.Path().Elements()
|
||||
if len(directories) == 0 {
|
||||
directory := c.Path().String()
|
||||
if len(directory) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if qp.Scope.Matches(selectors.ExchangeMailFolder, directories[len(directories)-1]) {
|
||||
if qp.Scope.Matches(selectors.ExchangeMailFolder, directory) {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
}
|
||||
@ -173,15 +172,15 @@ func GetAllCalendars(
|
||||
) ([]graph.CachedContainer, error) {
|
||||
containers := make([]graph.CachedContainer, 0)
|
||||
|
||||
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.EventsCategory)
|
||||
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.EventsCategory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range resolver.Items() {
|
||||
directories := c.Path().Elements()
|
||||
directory := c.Path().String()
|
||||
|
||||
if qp.Scope.Matches(selectors.ExchangeEventCalendar, directories[len(directories)-1]) {
|
||||
if qp.Scope.Matches(selectors.ExchangeEventCalendar, directory) {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
}
|
||||
@ -198,23 +197,22 @@ func GetAllContactFolders(
|
||||
qp graph.QueryParams,
|
||||
gs graph.Service,
|
||||
) ([]graph.CachedContainer, error) {
|
||||
var (
|
||||
query string
|
||||
containers = make([]graph.CachedContainer, 0)
|
||||
)
|
||||
var query string
|
||||
|
||||
resolver, err := MaybeGetAndPopulateFolderResolver(ctx, qp, path.ContactsCategory)
|
||||
containers := make([]graph.CachedContainer, 0)
|
||||
|
||||
resolver, err := PopulateExchangeContainerResolver(ctx, qp, path.ContactsCategory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range resolver.Items() {
|
||||
directories := c.Path().Elements()
|
||||
directory := c.Path().String()
|
||||
|
||||
if len(directories) == 0 {
|
||||
if len(directory) == 0 {
|
||||
query = DefaultContactFolder
|
||||
} else {
|
||||
query = directories[len(directories)-1]
|
||||
query = directory
|
||||
}
|
||||
|
||||
if qp.Scope.Matches(selectors.ExchangeContactFolder, query) {
|
||||
@ -225,42 +223,30 @@ func GetAllContactFolders(
|
||||
return containers, err
|
||||
}
|
||||
|
||||
// SetupExchangeCollectionVars is a helper function returns a sets
|
||||
// Exchange.Type specific functions based on scope.
|
||||
// The []GraphQuery slice provides fallback queries in the event that
|
||||
// initial queries provide zero results.
|
||||
func SetupExchangeCollectionVars(scope selectors.ExchangeScope) (
|
||||
absser.ParsableFactory,
|
||||
[]GraphQuery,
|
||||
GraphIterateFunc,
|
||||
error,
|
||||
) {
|
||||
if scope.IncludesCategory(selectors.ExchangeMail) {
|
||||
return nil, nil, nil, errors.New("mail no longer supported this way")
|
||||
}
|
||||
func GetContainers(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
gs graph.Service,
|
||||
) ([]graph.CachedContainer, error) {
|
||||
category := graph.ScopeToPathCategory(qp.Scope)
|
||||
|
||||
if scope.IncludesCategory(selectors.ExchangeContact) {
|
||||
return models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
|
||||
[]GraphQuery{GetAllContactFolderNamesForUser, GetDefaultContactFolderForUser},
|
||||
IterateSelectAllContactsForCollections,
|
||||
nil
|
||||
switch category {
|
||||
case path.ContactsCategory:
|
||||
return GetAllContactFolders(ctx, qp, gs)
|
||||
case path.EmailCategory:
|
||||
return GetAllMailFolders(ctx, qp, gs)
|
||||
case path.EventsCategory:
|
||||
return GetAllCalendars(ctx, qp, gs)
|
||||
default:
|
||||
return nil, fmt.Errorf("path.Category %s not supported", category)
|
||||
}
|
||||
|
||||
if scope.IncludesCategory(selectors.ExchangeEvent) {
|
||||
return models.CreateCalendarCollectionResponseFromDiscriminatorValue,
|
||||
[]GraphQuery{GetAllCalendarNamesForUser},
|
||||
IterateSelectAllEventsFromCalendars,
|
||||
nil
|
||||
}
|
||||
|
||||
return nil, nil, nil, errors.New("exchange scope option not supported")
|
||||
}
|
||||
|
||||
// MaybeGetAndPopulateFolderResolver gets a folder resolver if one is available for
|
||||
// PopulateExchangeContainerResolver gets a folder resolver if one is available for
|
||||
// this category of data. If one is not available, returns nil so that other
|
||||
// logic in the caller can complete as long as they check if the resolver is not
|
||||
// nil. If an error occurs populating the resolver, returns an error.
|
||||
func MaybeGetAndPopulateFolderResolver(
|
||||
func PopulateExchangeContainerResolver(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
category path.CategoryType,
|
||||
@ -298,7 +284,7 @@ func MaybeGetAndPopulateFolderResolver(
|
||||
cacheRoot = DefaultCalendar
|
||||
|
||||
default:
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("ContainerResolver not present for %s type", category)
|
||||
}
|
||||
|
||||
if err := res.Populate(ctx, cacheRoot); err != nil {
|
||||
@ -308,70 +294,43 @@ func MaybeGetAndPopulateFolderResolver(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func resolveCollectionPath(
|
||||
ctx context.Context,
|
||||
resolver graph.ContainerResolver,
|
||||
tenantID, user, folderID string,
|
||||
category path.CategoryType,
|
||||
) (path.Path, error) {
|
||||
if resolver == nil {
|
||||
// Allows caller to default to old-style path.
|
||||
return nil, errors.WithStack(errNilResolver)
|
||||
func pathAndMatch(qp graph.QueryParams, category path.CategoryType, c graph.CachedContainer) (path.Path, bool) {
|
||||
var (
|
||||
directory string
|
||||
pb = c.Path()
|
||||
)
|
||||
|
||||
// Clause ensures that DefaultContactFolder is inspected properly
|
||||
if category == path.ContactsCategory && *c.GetDisplayName() == DefaultContactFolder {
|
||||
pb = c.Path().Append(DefaultContactFolder)
|
||||
}
|
||||
|
||||
p, err := resolver.IDToPath(ctx, folderID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "resolving folder ID")
|
||||
}
|
||||
|
||||
return p.ToDataLayerExchangePathForCategory(
|
||||
tenantID,
|
||||
user,
|
||||
dirPath, err := pb.ToDataLayerExchangePathForCategory(
|
||||
qp.Credentials.AzureTenantID,
|
||||
qp.User,
|
||||
category,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func getCollectionPath(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
resolver graph.ContainerResolver,
|
||||
directory string,
|
||||
category path.CategoryType,
|
||||
) (path.Path, error) {
|
||||
returnPath, err := resolveCollectionPath(
|
||||
ctx,
|
||||
resolver,
|
||||
qp.Credentials.AzureTenantID,
|
||||
qp.User,
|
||||
directory,
|
||||
category,
|
||||
)
|
||||
if err == nil {
|
||||
return returnPath, nil
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
aPath, err1 := path.Builder{}.Append(directory).
|
||||
ToDataLayerExchangePathForCategory(
|
||||
qp.Credentials.AzureTenantID,
|
||||
qp.User,
|
||||
category,
|
||||
false,
|
||||
)
|
||||
if err1 == nil {
|
||||
return aPath, nil
|
||||
if dirPath == nil && category == path.EmailCategory {
|
||||
return nil, false // Only true for root mail folder
|
||||
}
|
||||
|
||||
return nil,
|
||||
support.WrapAndAppend(
|
||||
fmt.Sprintf(
|
||||
"both path generate functions failed for %s:%s:%s",
|
||||
qp.User,
|
||||
category,
|
||||
directory),
|
||||
err,
|
||||
err1,
|
||||
)
|
||||
directory = pb.String()
|
||||
|
||||
switch category {
|
||||
case path.EmailCategory:
|
||||
return dirPath, qp.Scope.Matches(selectors.ExchangeMailFolder, directory)
|
||||
case path.ContactsCategory:
|
||||
return dirPath, qp.Scope.Matches(selectors.ExchangeContactFolder, directory)
|
||||
case path.EventsCategory:
|
||||
return dirPath, qp.Scope.Matches(selectors.ExchangeEventCalendar, directory)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func AddItemsToCollection(
|
||||
|
||||
@ -301,6 +301,7 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
|
||||
t := suite.T()
|
||||
user := tester.M365UserID(t)
|
||||
a := tester.NewM365Account(t)
|
||||
service := loadService(t)
|
||||
credentials, err := a.M365Config()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -314,39 +315,27 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
|
||||
contains: "Birthdays",
|
||||
expectedCount: assert.Greater,
|
||||
getScope: func() selectors.ExchangeScope {
|
||||
sel := selectors.NewExchangeBackup()
|
||||
sel.Include(sel.EventCalendars([]string{user}, selectors.Any()))
|
||||
|
||||
scopes := sel.Scopes()
|
||||
assert.Equal(t, len(scopes), 1)
|
||||
|
||||
return scopes[0]
|
||||
return selectors.
|
||||
NewExchangeBackup().
|
||||
EventCalendars([]string{user}, selectors.Any())[0]
|
||||
},
|
||||
}, {
|
||||
name: "Default Calendar",
|
||||
contains: DefaultCalendar,
|
||||
expectedCount: assert.Equal,
|
||||
getScope: func() selectors.ExchangeScope {
|
||||
sel := selectors.NewExchangeBackup()
|
||||
sel.Include(sel.EventCalendars([]string{user}, []string{DefaultCalendar}))
|
||||
|
||||
scopes := sel.Scopes()
|
||||
assert.Equal(t, len(scopes), 1)
|
||||
|
||||
return scopes[0]
|
||||
return selectors.
|
||||
NewExchangeBackup().
|
||||
EventCalendars([]string{user}, []string{DefaultCalendar})[0]
|
||||
},
|
||||
}, {
|
||||
name: "Default Mail",
|
||||
contains: DefaultMailFolder,
|
||||
expectedCount: assert.Equal,
|
||||
getScope: func() selectors.ExchangeScope {
|
||||
sel := selectors.NewExchangeBackup()
|
||||
sel.Include(sel.MailFolders([]string{user}, []string{DefaultMailFolder}))
|
||||
|
||||
scopes := sel.Scopes()
|
||||
assert.Equal(t, len(scopes), 1)
|
||||
|
||||
return scopes[0]
|
||||
return selectors.
|
||||
NewExchangeBackup().
|
||||
MailFolders([]string{user}, []string{DefaultMailFolder})[0]
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -359,16 +348,14 @@ func (suite *ServiceFunctionsIntegrationSuite) TestCollectContainers() {
|
||||
FailFast: failFast,
|
||||
Credentials: credentials,
|
||||
}
|
||||
collections := make(map[string]*Collection)
|
||||
err := CollectFolders(ctx, qp, collections, nil, nil)
|
||||
collections, err := GetContainers(ctx, qp, service)
|
||||
assert.NoError(t, err)
|
||||
test.expectedCount(t, len(collections), containerCount)
|
||||
|
||||
keys := make([]string, 0, len(collections))
|
||||
for k := range collections {
|
||||
keys = append(keys, k)
|
||||
for _, k := range collections {
|
||||
keys = append(keys, *k.GetDisplayName())
|
||||
}
|
||||
t.Logf("Collections Made: %v\n", keys)
|
||||
assert.Contains(t, keys, test.contains)
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/pkg/errors"
|
||||
@ -12,436 +13,81 @@ import (
|
||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||
"github.com/alcionai/corso/src/internal/connector/support"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/selectors"
|
||||
)
|
||||
|
||||
var errNilResolver = errors.New("nil resolver")
|
||||
|
||||
// GraphIterateFuncs are iterate functions to be used with the M365 iterators (e.g. msgraphgocore.NewPageIterator)
|
||||
// @returns a callback func that works with msgraphgocore.PageIterator.Iterate function
|
||||
type GraphIterateFunc func(
|
||||
// FilterContainersAndFillCollections is a utility function
|
||||
// that places the M365 object ids belonging to specific directories
|
||||
// into a Collection. Messages outside of those directories are omitted.
|
||||
// @param collection is filled with during this function.
|
||||
// Supports all exchange applications: Contacts, Events, and Mail
|
||||
func FilterContainersAndFillCollections(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
errUpdater func(string, error),
|
||||
collections map[string]*Collection,
|
||||
statusUpdater support.StatusUpdater,
|
||||
resolver graph.ContainerResolver,
|
||||
) func(any) bool
|
||||
|
||||
// IterateSelectAllEventsForCollections
|
||||
// utility function for iterating through events
|
||||
// and storing events in collections based on
|
||||
// the calendarID which originates from M365.
|
||||
// @param pageItem is a CalendarCollectionResponse possessing two populated fields:
|
||||
// - id - M365 ID
|
||||
// - Name - Calendar Name
|
||||
func IterateSelectAllEventsFromCalendars(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
errUpdater func(string, error),
|
||||
collections map[string]*Collection,
|
||||
statusUpdater support.StatusUpdater,
|
||||
resolver graph.ContainerResolver,
|
||||
) func(any) bool {
|
||||
) error {
|
||||
var (
|
||||
isEnabled bool
|
||||
service graph.Service
|
||||
category = graph.ScopeToPathCategory(qp.Scope)
|
||||
collectionType = categoryToOptionIdentifier(category)
|
||||
errs error
|
||||
)
|
||||
|
||||
return func(pageItem any) bool {
|
||||
if !isEnabled {
|
||||
// Create Collections based on qp.Scope
|
||||
err := CollectFolders(ctx, qp, collections, statusUpdater, resolver)
|
||||
for _, c := range resolver.Items() {
|
||||
dirPath, ok := pathAndMatch(qp, category, c)
|
||||
if ok {
|
||||
// Create only those that match
|
||||
service, err := createService(qp.Credentials, qp.FailFast)
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
errors.Wrap(err, support.ConnectorStackErrorTrace(err)),
|
||||
)
|
||||
errs = support.WrapAndAppend(
|
||||
qp.User+" failed to create service during FilterContainerAndFillCollection",
|
||||
err,
|
||||
errs)
|
||||
|
||||
return false
|
||||
if qp.FailFast {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
|
||||
service, err = createService(qp.Credentials, qp.FailFast)
|
||||
if err != nil {
|
||||
errUpdater(qp.User, err)
|
||||
return false
|
||||
}
|
||||
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
pageItem = CreateCalendarDisplayable(pageItem)
|
||||
|
||||
calendar, ok := pageItem.(graph.Displayable)
|
||||
if !ok {
|
||||
errUpdater(
|
||||
edc := NewCollection(
|
||||
qp.User,
|
||||
fmt.Errorf("unable to parse pageItem into CalendarDisplayable: %T", pageItem),
|
||||
dirPath,
|
||||
collectionType,
|
||||
service,
|
||||
statusUpdater,
|
||||
)
|
||||
collections[*c.GetId()] = &edc
|
||||
}
|
||||
|
||||
if calendar.GetDisplayName() == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
collection, ok := collections[*calendar.GetDisplayName()]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
eventIDs, err := ReturnEventIDsFromCalendar(ctx, service, qp.User, *calendar.GetId())
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
errors.Wrap(err, support.ConnectorStackErrorTrace(err)))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
collection.jobs = append(collection.jobs, eventIDs...)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// CollectionsFromResolver returns the set of collections that match the
|
||||
// selector parameters.
|
||||
func CollectionsFromResolver(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
resolver graph.ContainerResolver,
|
||||
statusUpdater support.StatusUpdater,
|
||||
collections map[string]*Collection,
|
||||
) error {
|
||||
option, category, notMatcher := getCategoryAndValidation(qp.Scope)
|
||||
for directoryID, col := range collections {
|
||||
fetchFunc, err := getFetchIDFunc(category)
|
||||
if err != nil {
|
||||
errs = support.WrapAndAppend(
|
||||
qp.User,
|
||||
err,
|
||||
errs)
|
||||
|
||||
if qp.FailFast {
|
||||
return errs
|
||||
}
|
||||
|
||||
for _, item := range resolver.Items() {
|
||||
pathString := item.Path().String()
|
||||
// Skip the root folder for mail which has an empty path.
|
||||
if len(pathString) == 0 || notMatcher(&pathString) {
|
||||
continue
|
||||
}
|
||||
|
||||
completePath, err := item.Path().ToDataLayerExchangePathForCategory(
|
||||
qp.Credentials.AzureTenantID,
|
||||
qp.User,
|
||||
category,
|
||||
false,
|
||||
)
|
||||
jobs, err := fetchFunc(ctx, col.service, qp.User, directoryID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "resolving collection item path")
|
||||
}
|
||||
|
||||
service, err := createService(qp.Credentials, qp.FailFast)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "making service instance")
|
||||
}
|
||||
|
||||
tmp := NewCollection(
|
||||
qp.User,
|
||||
completePath,
|
||||
option,
|
||||
service,
|
||||
statusUpdater,
|
||||
)
|
||||
|
||||
collections[*item.GetId()] = &tmp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCategoryAndValidation(es selectors.ExchangeScope) (
|
||||
optionIdentifier,
|
||||
path.CategoryType,
|
||||
func(namePtr *string) bool,
|
||||
) {
|
||||
var (
|
||||
option = scopeToOptionIdentifier(es)
|
||||
category path.CategoryType
|
||||
validate func(namePtr *string) bool
|
||||
)
|
||||
|
||||
switch option {
|
||||
case messages:
|
||||
category = path.EmailCategory
|
||||
validate = func(namePtr *string) bool {
|
||||
if namePtr == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !es.Matches(selectors.ExchangeMailFolder, *namePtr)
|
||||
}
|
||||
case contacts:
|
||||
category = path.ContactsCategory
|
||||
validate = func(namePtr *string) bool {
|
||||
if namePtr == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !es.Matches(selectors.ExchangeContactFolder, *namePtr)
|
||||
}
|
||||
case events:
|
||||
category = path.EventsCategory
|
||||
validate = func(namePtr *string) bool {
|
||||
if namePtr == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !es.Matches(selectors.ExchangeEventCalendar, *namePtr)
|
||||
}
|
||||
}
|
||||
|
||||
return option, category, validate
|
||||
}
|
||||
|
||||
func IterateFilterContainersForCollections(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
errUpdater func(string, error),
|
||||
collections map[string]*Collection,
|
||||
statusUpdater support.StatusUpdater,
|
||||
resolver graph.ContainerResolver,
|
||||
) func(any) bool {
|
||||
var (
|
||||
isSet bool
|
||||
collectPath string
|
||||
option optionIdentifier
|
||||
category path.CategoryType
|
||||
validate func(*string) bool
|
||||
)
|
||||
|
||||
return func(folderItem any) bool {
|
||||
if !isSet {
|
||||
option, category, validate = getCategoryAndValidation(qp.Scope)
|
||||
|
||||
isSet = true
|
||||
}
|
||||
|
||||
if option == events {
|
||||
folderItem = CreateCalendarDisplayable(folderItem)
|
||||
}
|
||||
|
||||
folder, ok := folderItem.(graph.Displayable)
|
||||
if !ok {
|
||||
errUpdater(qp.User,
|
||||
fmt.Errorf("unable to convert input of %T for category: %s", folderItem, category.String()),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if validate(folder.GetDisplayName()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if option == messages {
|
||||
collectPath = *folder.GetId()
|
||||
} else {
|
||||
collectPath = *folder.GetDisplayName()
|
||||
}
|
||||
|
||||
dirPath, err := getCollectionPath(
|
||||
ctx,
|
||||
qp,
|
||||
resolver,
|
||||
collectPath,
|
||||
category,
|
||||
)
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
"failure converting path during IterateFilterFolderDirectoriesForCollections",
|
||||
err,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
service, err := createService(qp.Credentials, qp.FailFast)
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
*folder.GetDisplayName(),
|
||||
errors.Wrap(err, "creating service to iterate filterFolder directories for user: "+qp.User))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
temp := NewCollection(
|
||||
qp.User,
|
||||
dirPath,
|
||||
option,
|
||||
service,
|
||||
statusUpdater,
|
||||
)
|
||||
collections[*folder.GetDisplayName()] = &temp
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func IterateSelectAllContactsForCollections(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
errUpdater func(string, error),
|
||||
collections map[string]*Collection,
|
||||
statusUpdater support.StatusUpdater,
|
||||
resolver graph.ContainerResolver,
|
||||
) func(any) bool {
|
||||
var (
|
||||
isPrimarySet bool
|
||||
service graph.Service
|
||||
)
|
||||
|
||||
return func(folderItem any) bool {
|
||||
folder, ok := folderItem.(models.ContactFolderable)
|
||||
if !ok {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
errors.New("casting folderItem to models.ContactFolderable"),
|
||||
)
|
||||
}
|
||||
|
||||
if !isPrimarySet && folder.GetParentFolderId() != nil {
|
||||
err := CollectFolders(
|
||||
ctx,
|
||||
qp,
|
||||
collections,
|
||||
statusUpdater,
|
||||
resolver,
|
||||
)
|
||||
if err != nil {
|
||||
errUpdater(qp.User, err)
|
||||
return false
|
||||
}
|
||||
|
||||
service, err = createService(qp.Credentials, qp.FailFast)
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
errors.Wrap(err, "unable to create service during IterateSelectAllContactsForCollections"),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
isPrimarySet = true
|
||||
|
||||
// Create and Populate Default Contacts folder Collection if true
|
||||
if qp.Scope.Matches(selectors.ExchangeContactFolder, DefaultContactFolder) {
|
||||
dirPath, err := path.Builder{}.Append(DefaultContactFolder).ToDataLayerExchangePathForCategory(
|
||||
qp.Credentials.AzureTenantID,
|
||||
qp.User,
|
||||
path.ContactsCategory,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
err,
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
edc := NewCollection(
|
||||
qp.User,
|
||||
dirPath,
|
||||
contacts,
|
||||
service,
|
||||
statusUpdater,
|
||||
)
|
||||
|
||||
listOfIDs, err := ReturnContactIDsFromDirectory(ctx, service, qp.User, *folder.GetParentFolderId())
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
qp.User,
|
||||
err,
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
edc.jobs = append(edc.jobs, listOfIDs...)
|
||||
collections[DefaultContactFolder] = &edc
|
||||
}
|
||||
}
|
||||
|
||||
if folder.GetDisplayName() == nil {
|
||||
// This should never happen. Skipping to avoid kernel panic
|
||||
return true
|
||||
}
|
||||
|
||||
collection, ok := collections[*folder.GetDisplayName()]
|
||||
if !ok {
|
||||
return true // Not included
|
||||
}
|
||||
|
||||
listOfIDs, err := ReturnContactIDsFromDirectory(ctx, service, qp.User, *folder.GetId())
|
||||
if err != nil {
|
||||
errUpdater(
|
||||
errs = support.WrapAndAppend(
|
||||
qp.User,
|
||||
err,
|
||||
errs,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
collection.jobs = append(collection.jobs, listOfIDs...)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// IDistFunc collection of helper functions which return a list of strings
|
||||
// from a response.
|
||||
type IDListFunc func(ctx context.Context, gs graph.Service, user, m365ID string) ([]string, error)
|
||||
|
||||
// ReturnContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
|
||||
// of the targeted directory
|
||||
func ReturnContactIDsFromDirectory(ctx context.Context, gs graph.Service, user, directoryID string) ([]string, error) {
|
||||
options, err := optionsForContactFoldersItem([]string{"parentFolderId"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
col.jobs = append(col.jobs, jobs...)
|
||||
}
|
||||
|
||||
stringArray := []string{}
|
||||
|
||||
response, err := gs.Client().
|
||||
UsersById(user).
|
||||
ContactFoldersById(directoryID).
|
||||
Contacts().
|
||||
Get(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(
|
||||
response,
|
||||
gs.Adapter(),
|
||||
models.CreateContactCollectionResponseFromDiscriminatorValue,
|
||||
)
|
||||
|
||||
callbackFunc := func(pageItem any) bool {
|
||||
entry, ok := pageItem.(models.Contactable)
|
||||
if !ok {
|
||||
err = errors.New("casting pageItem to models.Contactable")
|
||||
return false
|
||||
}
|
||||
|
||||
stringArray = append(stringArray, *entry.GetId())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if iterateErr := pageIterator.Iterate(ctx, callbackFunc); iterateErr != nil {
|
||||
return nil, iterateErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stringArray, nil
|
||||
return errs
|
||||
}
|
||||
|
||||
func IterativeCollectContactContainers(
|
||||
@ -491,8 +137,25 @@ func IterativeCollectCalendarContainers(
|
||||
}
|
||||
}
|
||||
|
||||
// ReturnEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar.
|
||||
func ReturnEventIDsFromCalendar(
|
||||
// FetchIDFunc collection of helper functions which return a list of strings
|
||||
// from a response.
|
||||
type FetchIDFunc func(ctx context.Context, gs graph.Service, user, containerID string) ([]string, error)
|
||||
|
||||
func getFetchIDFunc(category path.CategoryType) (FetchIDFunc, error) {
|
||||
switch category {
|
||||
case path.EmailCategory:
|
||||
return FetchMessageIDsFromDirectory, nil
|
||||
case path.EventsCategory:
|
||||
return FetchEventIDsFromCalendar, nil
|
||||
case path.ContactsCategory:
|
||||
return FetchContactIDsFromDirectory, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("category %s not supported by getFetchIDFunc", category)
|
||||
}
|
||||
}
|
||||
|
||||
// FetchEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar.
|
||||
func FetchEventIDsFromCalendar(
|
||||
ctx context.Context,
|
||||
gs graph.Service,
|
||||
user, calendarID string,
|
||||
@ -504,11 +167,7 @@ func ReturnEventIDsFromCalendar(
|
||||
CalendarsById(calendarID).
|
||||
Events().Get(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
|
||||
}
|
||||
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(
|
||||
@ -516,27 +175,158 @@ func ReturnEventIDsFromCalendar(
|
||||
gs.Adapter(),
|
||||
models.CreateEventCollectionResponseFromDiscriminatorValue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "iterator creation failure during fetchEventIDs")
|
||||
}
|
||||
|
||||
callbackFunc := func(pageItem any) bool {
|
||||
entry, ok := pageItem.(models.Eventable)
|
||||
var errs *multierror.Error
|
||||
|
||||
err = pageIterator.Iterate(ctx, func(pageItem any) bool {
|
||||
entry, ok := pageItem.(graph.Idable)
|
||||
if !ok {
|
||||
err = errors.New("casting pageItem to models.Eventable")
|
||||
return false
|
||||
errs = multierror.Append(errs, errors.New("item without GetId() call"))
|
||||
return true
|
||||
}
|
||||
|
||||
if entry.GetId() == nil {
|
||||
errs = multierror.Append(errs, errors.New("item with nil ID"))
|
||||
}
|
||||
|
||||
ids = append(ids, *entry.GetId())
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(
|
||||
err,
|
||||
support.ConnectorStackErrorTrace(err)+
|
||||
" :iterateFailure for fetching events from calendar "+calendarID,
|
||||
)
|
||||
}
|
||||
|
||||
if iterateErr := pageIterator.Iterate(ctx, callbackFunc); iterateErr != nil {
|
||||
return nil,
|
||||
errors.Wrap(iterateErr, support.ConnectorStackErrorTrace(err))
|
||||
}
|
||||
return ids, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
// FetchContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
|
||||
// of the targeted directory
|
||||
func FetchContactIDsFromDirectory(ctx context.Context, gs graph.Service, user, directoryID string) ([]string, error) {
|
||||
options, err := optionsForContactFoldersItem([]string{"parentFolderId"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
ids := []string{}
|
||||
|
||||
response, err := gs.Client().
|
||||
UsersById(user).
|
||||
ContactFoldersById(directoryID).
|
||||
Contacts().
|
||||
Get(ctx, options)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
|
||||
}
|
||||
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(
|
||||
response,
|
||||
gs.Adapter(),
|
||||
models.CreateContactCollectionResponseFromDiscriminatorValue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failure to create iterator during FecthContactIDs")
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
|
||||
err = pageIterator.Iterate(ctx, func(pageItem any) bool {
|
||||
entry, ok := pageItem.(graph.Idable)
|
||||
if !ok {
|
||||
errs = multierror.Append(
|
||||
errs,
|
||||
errors.New("casting pageItem to models.Contactable"),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
ids = append(ids, *entry.GetId())
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil,
|
||||
errors.Wrap(
|
||||
err,
|
||||
support.ConnectorStackErrorTrace(err)+
|
||||
" :iterate failure during fetching contactIDs from directory "+directoryID,
|
||||
)
|
||||
}
|
||||
|
||||
return ids, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
// FetchMessageIDsFromDirectory function that returns a list of all the m365IDs of the exchange.Mail
|
||||
// of the targeted directory
|
||||
func FetchMessageIDsFromDirectory(
|
||||
ctx context.Context,
|
||||
gs graph.Service,
|
||||
user, directoryID string,
|
||||
) ([]string, error) {
|
||||
ids := []string{}
|
||||
|
||||
options, err := optionsForFolderMessages([]string{"id"})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting query options")
|
||||
}
|
||||
|
||||
response, err := gs.Client().
|
||||
UsersById(user).
|
||||
MailFoldersById(directoryID).
|
||||
Messages().
|
||||
Get(ctx, options)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(
|
||||
errors.Wrap(err, support.ConnectorStackErrorTrace(err)),
|
||||
"initial folder query",
|
||||
)
|
||||
}
|
||||
|
||||
pageIter, err := msgraphgocore.NewPageIterator(
|
||||
response,
|
||||
gs.Adapter(),
|
||||
models.CreateMessageCollectionResponseFromDiscriminatorValue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating graph iterator")
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
|
||||
err = pageIter.Iterate(ctx, func(pageItem any) bool {
|
||||
item, ok := pageItem.(graph.Idable)
|
||||
if !ok {
|
||||
errs = multierror.Append(errs, errors.New("item without ID function"))
|
||||
return true
|
||||
}
|
||||
|
||||
if item.GetId() == nil {
|
||||
errs = multierror.Append(errs, errors.New("item with nil ID"))
|
||||
return true
|
||||
}
|
||||
|
||||
ids = append(ids, *item.GetId())
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(
|
||||
err,
|
||||
support.ConnectorStackErrorTrace(err)+
|
||||
" :iterateFailure for fetching messages from directory "+directoryID,
|
||||
)
|
||||
}
|
||||
|
||||
return ids, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
@ -2,15 +2,10 @@ package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
absser "github.com/microsoft/kiota-abstractions-go/serialization"
|
||||
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||
"github.com/alcionai/corso/src/internal/connector/support"
|
||||
)
|
||||
|
||||
// GraphQuery represents functions which perform exchange-specific queries
|
||||
@ -125,84 +120,3 @@ func RetrieveEventDataForUser(ctx context.Context, gs graph.Service, user, m365I
|
||||
func RetrieveMessageDataForUser(ctx context.Context, gs graph.Service, user, m365ID string) (absser.Parsable, error) {
|
||||
return gs.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil)
|
||||
}
|
||||
|
||||
// CollectFolders is a utility function for creating Collections based off parameters found
|
||||
// in the ExchangeScope found in the graph.QueryParams
|
||||
// TODO(ashmrtn): This may not need to do the query if we decide the cache
|
||||
// should always:
|
||||
// 1. be passed in
|
||||
// 2. be populated with all folders for the user
|
||||
func CollectFolders(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
collections map[string]*Collection,
|
||||
statusUpdater support.StatusUpdater,
|
||||
resolver graph.ContainerResolver,
|
||||
) error {
|
||||
var (
|
||||
query GraphQuery
|
||||
transformer absser.ParsableFactory
|
||||
queryService, err = createService(qp.Credentials, qp.FailFast)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(
|
||||
err,
|
||||
"unable to create graph.Service within CollectFolders service for "+qp.User,
|
||||
)
|
||||
}
|
||||
|
||||
option := scopeToOptionIdentifier(qp.Scope)
|
||||
switch option {
|
||||
case messages:
|
||||
query = GetAllFolderNamesForUser
|
||||
transformer = models.CreateMailFolderCollectionResponseFromDiscriminatorValue
|
||||
case contacts:
|
||||
query = GetAllContactFolderNamesForUser
|
||||
transformer = models.CreateContactFolderCollectionResponseFromDiscriminatorValue
|
||||
case events:
|
||||
query = GetAllCalendarNamesForUser
|
||||
transformer = models.CreateCalendarCollectionResponseFromDiscriminatorValue
|
||||
default:
|
||||
return fmt.Errorf("unsupported option %s used in CollectFolders", option)
|
||||
}
|
||||
|
||||
response, err := query(ctx, queryService, qp.User)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"unable to query mail folder for %s: details: %s",
|
||||
qp.User,
|
||||
support.ConnectorStackErrorTrace(err),
|
||||
)
|
||||
}
|
||||
|
||||
// Iterator required to ensure all potential folders are inspected
|
||||
// when the breadth of the folder space is large
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(
|
||||
response,
|
||||
&queryService.adapter,
|
||||
transformer)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create iterator during mail folder query service")
|
||||
}
|
||||
|
||||
errUpdater := func(id string, e error) {
|
||||
err = support.WrapAndAppend(id, e, err)
|
||||
}
|
||||
|
||||
callbackFunc := IterateFilterContainersForCollections(
|
||||
ctx,
|
||||
qp,
|
||||
errUpdater,
|
||||
collections,
|
||||
statusUpdater,
|
||||
resolver,
|
||||
)
|
||||
|
||||
iterateFailure := pageIterator.Iterate(ctx, callbackFunc)
|
||||
if iterateFailure != nil {
|
||||
err = support.WrapAndAppend(qp.User+" iterate failure", iterateFailure, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -520,8 +520,8 @@ func establishMailRestoreLocation(
|
||||
}
|
||||
|
||||
// establishContactsRestoreLocation creates Contact Folders in sequence
|
||||
// and updates the container resolver appropriately. Contact Folders
|
||||
// are displayed in a flat representation. Therefore, only the root can be populated and all content
|
||||
// and updates the container resolver appropriately. Contact Folders are
|
||||
// displayed in a flat representation. Therefore, only the root can be populated and all content
|
||||
// must be restored into the root location.
|
||||
// @param folders is the list of intended folders from root to leaf (e.g. [root ...])
|
||||
// @param isNewCache bool representation of whether Populate function needs to be run
|
||||
|
||||
109
src/internal/connector/graph/cache_container.go
Normal file
109
src/internal/connector/graph/cache_container.go
Normal file
@ -0,0 +1,109 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
)
|
||||
|
||||
// CachedContainer is used for local unit tests but also makes it so that this
|
||||
// code can be broken into generic- and service-specific chunks later on to
|
||||
// reuse logic in IDToPath.
|
||||
type CachedContainer interface {
|
||||
Container
|
||||
Path() *path.Builder
|
||||
SetPath(*path.Builder)
|
||||
}
|
||||
|
||||
// checkRequiredValues is a helper function to ensure that
|
||||
// all the pointers are set prior to being called.
|
||||
func CheckRequiredValues(c Container) error {
|
||||
idPtr := c.GetId()
|
||||
if idPtr == nil || len(*idPtr) == 0 {
|
||||
return errors.New("folder without ID")
|
||||
}
|
||||
|
||||
ptr := c.GetDisplayName()
|
||||
if ptr == nil || len(*ptr) == 0 {
|
||||
return errors.Errorf("folder %s without display name", *idPtr)
|
||||
}
|
||||
|
||||
ptr = c.GetParentFolderId()
|
||||
if ptr == nil || len(*ptr) == 0 {
|
||||
return errors.Errorf("folder %s without parent ID", *idPtr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//======================================
|
||||
// cachedContainer Implementations
|
||||
//======================================
|
||||
|
||||
var _ CachedContainer = &CacheFolder{}
|
||||
|
||||
type CacheFolder struct {
|
||||
Container
|
||||
p *path.Builder
|
||||
}
|
||||
|
||||
// NewCacheFolder public constructor for struct
|
||||
func NewCacheFolder(c Container, pb *path.Builder) CacheFolder {
|
||||
cf := CacheFolder{
|
||||
Container: c,
|
||||
p: pb,
|
||||
}
|
||||
|
||||
return cf
|
||||
}
|
||||
|
||||
//=========================================
|
||||
// Required Functions to satisfy interfaces
|
||||
//=========================================
|
||||
|
||||
func (cf CacheFolder) Path() *path.Builder {
|
||||
return cf.p
|
||||
}
|
||||
|
||||
func (cf *CacheFolder) SetPath(newPath *path.Builder) {
|
||||
cf.p = newPath
|
||||
}
|
||||
|
||||
// CalendarDisplayable is a transformative struct that aligns
|
||||
// models.Calendarable interface with the container interface.
|
||||
// Calendars do not have the 2 of the
|
||||
type CalendarDisplayable struct {
|
||||
models.Calendarable
|
||||
parentID string
|
||||
}
|
||||
|
||||
// GetDisplayName returns the *string of the calendar name
|
||||
func (c CalendarDisplayable) GetDisplayName() *string {
|
||||
return c.GetName()
|
||||
}
|
||||
|
||||
// GetParentFolderId returns the default calendar name address
|
||||
// EventCalendars have a flat hierarchy and Calendars are rooted
|
||||
// at the default
|
||||
//nolint:revive
|
||||
func (c CalendarDisplayable) GetParentFolderId() *string {
|
||||
return &c.parentID
|
||||
}
|
||||
|
||||
// CreateCalendarDisplayable helper function to create the
|
||||
// calendarDisplayable during msgraph-sdk-go iterative process
|
||||
// @param entry is the input supplied by pageIterator.Iterate()
|
||||
// @param parentID of Calendar sets. Only populate when used with
|
||||
// EventCalendarCache
|
||||
func CreateCalendarDisplayable(entry any, parentID string) *CalendarDisplayable {
|
||||
calendar, ok := entry.(models.Calendarable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CalendarDisplayable{
|
||||
Calendarable: calendar,
|
||||
parentID: parentID,
|
||||
}
|
||||
}
|
||||
@ -53,15 +53,6 @@ type Container interface {
|
||||
Displayable
|
||||
}
|
||||
|
||||
// CachedContainer is used for local unit tests but also makes it so that this
|
||||
// code can be broken into generic- and service-specific chunks later on to
|
||||
// reuse logic in IDToPath.
|
||||
type CachedContainer interface {
|
||||
Container
|
||||
Path() *path.Builder
|
||||
SetPath(*path.Builder)
|
||||
}
|
||||
|
||||
// ContainerResolver houses functions for getting information about containers
|
||||
// from remote APIs (i.e. resolve folder paths with Graph API). Resolvers may
|
||||
// cache information about containers.
|
||||
@ -83,6 +74,7 @@ type ContainerResolver interface {
|
||||
PathInCache(pathString string) (string, bool)
|
||||
|
||||
AddToCache(ctx context.Context, m365Container Container) error
|
||||
|
||||
// Items returns the containers in the cache.
|
||||
Items() []CachedContainer
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
"github.com/alcionai/corso/src/pkg/backup/details"
|
||||
"github.com/alcionai/corso/src/pkg/control"
|
||||
"github.com/alcionai/corso/src/pkg/logger"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
"github.com/alcionai/corso/src/pkg/selectors"
|
||||
)
|
||||
|
||||
@ -274,112 +273,7 @@ func (gc *GraphConnector) RestoreDataCollections(
|
||||
return deets, err
|
||||
}
|
||||
|
||||
func scopeToPathCategory(scope selectors.ExchangeScope) path.CategoryType {
|
||||
if scope.IncludesCategory(selectors.ExchangeMail) {
|
||||
return path.EmailCategory
|
||||
}
|
||||
|
||||
if scope.IncludesCategory(selectors.ExchangeContact) {
|
||||
return path.ContactsCategory
|
||||
}
|
||||
|
||||
if scope.IncludesCategory(selectors.ExchangeEvent) {
|
||||
return path.EventsCategory
|
||||
}
|
||||
|
||||
return path.UnknownCategory
|
||||
}
|
||||
|
||||
func (gc *GraphConnector) fetchItemsByFolder(
|
||||
ctx context.Context,
|
||||
qp graph.QueryParams,
|
||||
resolver graph.ContainerResolver,
|
||||
) (map[string]*exchange.Collection, error) {
|
||||
var errs *multierror.Error
|
||||
|
||||
collections := map[string]*exchange.Collection{}
|
||||
// This gets the collections, but does not get the items in the
|
||||
// collection.
|
||||
err := exchange.CollectionsFromResolver(
|
||||
ctx,
|
||||
qp,
|
||||
resolver,
|
||||
gc.UpdateStatus,
|
||||
collections,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting target collections")
|
||||
}
|
||||
|
||||
for id, col := range collections {
|
||||
// Fetch items for said collection.
|
||||
err := exchange.AddItemsToCollection(ctx, gc.Service(), qp.User, id, col)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, errors.Wrapf(
|
||||
err,
|
||||
"fetching items for collection %s with ID %s",
|
||||
col.FullPath().String(),
|
||||
id,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return collections, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (gc *GraphConnector) legacyFetchItems(
|
||||
ctx context.Context,
|
||||
scope selectors.ExchangeScope,
|
||||
qp graph.QueryParams,
|
||||
resolver graph.ContainerResolver,
|
||||
) (map[string]*exchange.Collection, error) {
|
||||
var (
|
||||
errs error
|
||||
errUpdater = func(id string, err error) {
|
||||
errs = support.WrapAndAppend(id, err, errs)
|
||||
}
|
||||
collections = map[string]*exchange.Collection{}
|
||||
)
|
||||
|
||||
transformer, queries, gIter, err := exchange.SetupExchangeCollectionVars(scope)
|
||||
if err != nil {
|
||||
return nil, support.WrapAndAppend(gc.Service().Adapter().GetBaseUrl(), err, nil)
|
||||
}
|
||||
|
||||
// queries is assumed to provide fallbacks in case of empty results. Any
|
||||
// non-zero collection production will break out of the loop.
|
||||
for _, query := range queries {
|
||||
response, err := query(ctx, &gc.graphService, qp.User)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(
|
||||
err,
|
||||
"user %s M365 query: %s",
|
||||
qp.User, support.ConnectorStackErrorTrace(err))
|
||||
}
|
||||
|
||||
pageIterator, err := msgraphgocore.NewPageIterator(response, &gc.graphService.adapter, transformer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// callbackFunc iterates through all M365 object target and fills exchange.Collection.jobs[]
|
||||
// with corresponding item M365IDs. New collections are created for each directory.
|
||||
// Each directory used the M365 Identifier. The use of ID stops collisions betweens users
|
||||
callbackFunc := gIter(ctx, qp, errUpdater, collections, gc.UpdateStatus, resolver)
|
||||
|
||||
if err := pageIterator.Iterate(ctx, callbackFunc); err != nil {
|
||||
return nil, support.WrapAndAppend(gc.graphService.adapter.GetBaseUrl(), err, errs)
|
||||
}
|
||||
|
||||
if len(collections) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return collections, errs
|
||||
}
|
||||
|
||||
// createCollection - utility function that retrieves M365
|
||||
// createCollections - utility function that retrieves M365
|
||||
// IDs through Microsoft Graph API. The selectors.ExchangeScope
|
||||
// determines the type of collections that are stored.
|
||||
// to the GraphConnector struct.
|
||||
@ -393,7 +287,7 @@ func (gc *GraphConnector) createCollections(
|
||||
allCollections := make([]*exchange.Collection, 0)
|
||||
// Create collection of ExchangeDataCollection
|
||||
for _, user := range users {
|
||||
var collections map[string]*exchange.Collection
|
||||
collections := make(map[string]*exchange.Collection)
|
||||
|
||||
qp := graph.QueryParams{
|
||||
User: user,
|
||||
@ -402,31 +296,24 @@ func (gc *GraphConnector) createCollections(
|
||||
Credentials: gc.credentials,
|
||||
}
|
||||
|
||||
// Currently only mail has a folder cache implemented.
|
||||
resolver, err := exchange.MaybeGetAndPopulateFolderResolver(
|
||||
resolver, err := exchange.PopulateExchangeContainerResolver(
|
||||
ctx,
|
||||
qp,
|
||||
scopeToPathCategory(scope),
|
||||
graph.ScopeToPathCategory(qp.Scope),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting folder cache")
|
||||
}
|
||||
|
||||
if scopeToPathCategory(scope) == path.EmailCategory {
|
||||
if resolver == nil {
|
||||
return nil, errors.New("unable to create mail folder resolver")
|
||||
}
|
||||
err = exchange.FilterContainersAndFillCollections(
|
||||
ctx,
|
||||
qp,
|
||||
collections,
|
||||
gc.UpdateStatus,
|
||||
resolver)
|
||||
|
||||
collections, err = gc.fetchItemsByFolder(ctx, qp, resolver)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
} else {
|
||||
collections, err = gc.legacyFetchItems(ctx, scope, qp, resolver)
|
||||
// Preserving previous behavior.
|
||||
if err != nil {
|
||||
return nil, err // return error if snapshot is incomplete
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "filling collections")
|
||||
}
|
||||
|
||||
for _, collection := range collections {
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/connector/exchange"
|
||||
"github.com/alcionai/corso/src/internal/connector/graph"
|
||||
"github.com/alcionai/corso/src/internal/connector/mockconnector"
|
||||
"github.com/alcionai/corso/src/internal/connector/support"
|
||||
"github.com/alcionai/corso/src/internal/data"
|
||||
@ -193,9 +192,10 @@ func (suite *GraphConnectorIntegrationSuite) TestContactSerializationRegression(
|
||||
{
|
||||
name: "Default Contact Folder",
|
||||
getCollection: func(t *testing.T) []*exchange.Collection {
|
||||
sel := selectors.NewExchangeBackup()
|
||||
sel.Include(sel.ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder}))
|
||||
collections, err := connector.createCollections(ctx, sel.Scopes()[0])
|
||||
scope := selectors.
|
||||
NewExchangeBackup().
|
||||
ContactFolders([]string{suite.user}, []string{exchange.DefaultContactFolder})[0]
|
||||
collections, err := connector.createCollections(ctx, scope)
|
||||
require.NoError(t, err)
|
||||
|
||||
return collections
|
||||
@ -206,7 +206,7 @@ func (suite *GraphConnectorIntegrationSuite) TestContactSerializationRegression(
|
||||
for _, test := range tests {
|
||||
suite.T().Run(test.name, func(t *testing.T) {
|
||||
edcs := test.getCollection(t)
|
||||
assert.Equal(t, len(edcs), 1)
|
||||
require.Equal(t, len(edcs), 1)
|
||||
edc := edcs[0]
|
||||
assert.Equal(t, edc.FullPath().Folder(), exchange.DefaultContactFolder)
|
||||
streamChannel := edc.Items()
|
||||
@ -325,7 +325,6 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
|
||||
var (
|
||||
t = suite.T()
|
||||
userID = tester.M365UserID(t)
|
||||
sel = selectors.NewExchangeBackup()
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
@ -333,11 +332,17 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
|
||||
scope selectors.ExchangeScope
|
||||
folderNames map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "Mail Iterative Check",
|
||||
scope: selectors.NewExchangeBackup().MailFolders([]string{userID}, selectors.Any())[0],
|
||||
folderNames: map[string]struct{}{
|
||||
exchange.DefaultMailFolder: {},
|
||||
"Sent Items": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Folder Iterative Check Mail",
|
||||
// Only select specific folders so the test doesn't flake when the CI
|
||||
// cleanup task deletes things.
|
||||
scope: sel.MailFolders(
|
||||
scope: selectors.NewExchangeBackup().MailFolders(
|
||||
[]string{userID},
|
||||
[]string{exchange.DefaultMailFolder},
|
||||
)[0],
|
||||
@ -351,25 +356,7 @@ func (suite *GraphConnectorIntegrationSuite) TestMailFetch() {
|
||||
|
||||
for _, test := range tests {
|
||||
suite.T().Run(test.name, func(t *testing.T) {
|
||||
qp := graph.QueryParams{
|
||||
User: userID,
|
||||
Scope: test.scope,
|
||||
Credentials: gc.credentials,
|
||||
FailFast: false,
|
||||
}
|
||||
|
||||
resolver, err := exchange.MaybeGetAndPopulateFolderResolver(
|
||||
ctx,
|
||||
qp,
|
||||
scopeToPathCategory(qp.Scope),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
collections, err := gc.fetchItemsByFolder(
|
||||
ctx,
|
||||
qp,
|
||||
resolver,
|
||||
)
|
||||
collections, err := gc.createCollections(ctx, test.scope)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range collections {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user