GC: Interface: Cache Refactor (#1043)

## Description
Code changed to support caching on the default folder of `exchange.Mail` as well as an independent node within the Inbox.
<!-- Insert PR description-->

## Type of change

- [x] 🌻 Feature

## Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* related to Issue #1004<issue>

## Test Plan
- [x]  Unit test
This commit is contained in:
Danny 2022-10-05 16:04:50 -04:00 committed by GitHub
parent 3a1eb1efd2
commit 5bcdaef769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 38 deletions

View File

@ -4,14 +4,15 @@ import (
"context" "context"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
"github.com/microsoftgraph/msgraph-sdk-go/models" msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders/item/childfolders/delta"
msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders/delta"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ cachedContainer = &mailFolder{}
// cachedContainer is used for local unit tests but also makes it so that this // 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 // code can be broken into generic- and service-specific chunks later on to
// reuse logic in IDToPath. // reuse logic in IDToPath.
@ -21,11 +22,16 @@ type cachedContainer interface {
SetPath(*path.Builder) SetPath(*path.Builder)
} }
// mailFolder structure that implements the cachedContainer interface
type mailFolder struct { type mailFolder struct {
models.MailFolderable folder container
p *path.Builder p *path.Builder
} }
//=========================================
// Required Functions to satisfy interfaces
//=====================================
func (mf mailFolder) Path() *path.Builder { func (mf mailFolder) Path() *path.Builder {
return mf.p return mf.p
} }
@ -34,15 +40,36 @@ func (mf *mailFolder) SetPath(newPath *path.Builder) {
mf.p = newPath mf.p = newPath
} }
type mailFolderCache struct { func (mf *mailFolder) GetDisplayName() *string {
cache map[string]cachedContainer return mf.folder.GetDisplayName()
gs graph.Service
userID string
} }
// populateRoot fetches and populates the root folder in the cache so the cache //nolint:revive
// knows when to stop resolving the path. func (mf *mailFolder) GetId() *string {
func (mc *mailFolderCache) populateRoot(ctx context.Context) error { return mf.folder.GetId()
}
//nolint:revive
func (mf *mailFolder) GetParentFolderId() *string {
return mf.folder.GetParentFolderId()
}
// mailFolderCache struct used to improve lookup of directories within exchange.Mail
// cache map of cachedContainers where the key = M365ID
// nameLookup map: Key: DisplayName Value: ID
type mailFolderCache struct {
cache map[string]cachedContainer
gs graph.Service
userID, rootID string
}
// populateMailRoot fetches and populates the "base" directory from user's inbox.
// Action ensures that cache will stop at appropriate level.
// @param directory: M365 ID of the root all intended inquiries.
// Function should only be used directly when it is known that all
// folder inquiries are going to a specific node. In all other cases
// @error iff the struct is not properly instantiated
func (mc *mailFolderCache) populateMailRoot(ctx context.Context, directoryID string) error {
wantedOpts := []string{"displayName", "parentFolderId"} wantedOpts := []string{"displayName", "parentFolderId"}
opts, err := optionsForMailFoldersItem(wantedOpts) opts, err := optionsForMailFoldersItem(wantedOpts)
@ -54,7 +81,7 @@ func (mc *mailFolderCache) populateRoot(ctx context.Context) error {
gs. gs.
Client(). Client().
UsersById(mc.userID). UsersById(mc.userID).
MailFoldersById(rootFolderAlias). MailFoldersById(directoryID).
Get(ctx, opts) Get(ctx, opts)
if err != nil { if err != nil {
return errors.Wrapf(err, "fetching root folder") return errors.Wrapf(err, "fetching root folder")
@ -62,18 +89,23 @@ func (mc *mailFolderCache) populateRoot(ctx context.Context) error {
// Root only needs the ID because we hide it's name for Mail. // Root only needs the ID because we hide it's name for Mail.
idPtr := f.GetId() idPtr := f.GetId()
if idPtr == nil || len(*idPtr) == 0 { if idPtr == nil || len(*idPtr) == 0 {
return errors.New("root folder has no ID") return errors.New("root folder has no ID")
} }
mc.cache[*idPtr] = &mailFolder{ temp := mailFolder{
MailFolderable: f, folder: f,
p: &path.Builder{}, p: &path.Builder{},
} }
mc.cache[*idPtr] = &temp
mc.rootID = *idPtr
return nil return nil
} }
// checkRequiredValues is a helper function to ensure that
// all the pointers are set prior to being called.
func checkRequiredValues(c container) error { func checkRequiredValues(c container) error {
idPtr := c.GetId() idPtr := c.GetId()
if idPtr == nil || len(*idPtr) == 0 { if idPtr == nil || len(*idPtr) == 0 {
@ -93,26 +125,33 @@ func checkRequiredValues(c container) error {
return nil return nil
} }
func (mc *mailFolderCache) Populate(ctx context.Context) error { // Populate utility function for populating the mailFolderCache.
if mc.cache == nil { // Number of Graph Queries: 1.
mc.cache = map[string]cachedContainer{} // @param baseID: M365ID of the base of the exchange.Mail.Folder
// Use rootFolderAlias for input if baseID unknown
func (mc *mailFolderCache) Populate(ctx context.Context, baseID string) error {
if len(baseID) == 0 {
return errors.New("populate function requires: M365ID as input")
} }
if err := mc.populateRoot(ctx); err != nil { err := mc.Init(ctx, baseID)
if err != nil {
return err return err
} }
builder := mc. query := mc.
gs. gs.
Client(). Client().
UsersById(mc.userID). UsersById(mc.userID).
MailFolders(). MailFoldersById(mc.rootID).ChildFolders().
Delta() Delta()
var errs *multierror.Error var errs *multierror.Error
// TODO: Cannot use Iterator for delta
// Awaiting resolution: https://github.com/microsoftgraph/msgraph-sdk-go/issues/272
for { for {
resp, err := builder.Get(ctx, nil) resp, err := query.Get(ctx, nil)
if err != nil { if err != nil {
return err return err
} }
@ -123,7 +162,9 @@ func (mc *mailFolderCache) Populate(ctx context.Context) error {
continue continue
} }
mc.cache[*f.GetId()] = &mailFolder{MailFolderable: f} mc.cache[*f.GetId()] = &mailFolder{
folder: f,
}
} }
r := resp.GetAdditionalData() r := resp.GetAdditionalData()
@ -134,7 +175,7 @@ func (mc *mailFolderCache) Populate(ctx context.Context) error {
} }
link := *(n.(*string)) link := *(n.(*string))
builder = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter()) query = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
} }
return errs.ErrorOrNil() return errs.ErrorOrNil()
@ -164,3 +205,14 @@ func (mc *mailFolderCache) IDToPath(
return fullPath, nil return fullPath, nil
} }
// Init ensures that the structure's fields are initialized.
// Fields Initialized when cache == nil:
// [mc.cache, mc.rootID]
func (mc *mailFolderCache) Init(ctx context.Context, baseNode string) error {
if mc.cache == nil {
mc.cache = map[string]cachedContainer{}
}
return mc.populateMailRoot(ctx, baseNode)
}

View File

@ -22,6 +22,8 @@ const (
//nolint:lll //nolint:lll
testFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAABl7AqpAAA=" testFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAABl7AqpAAA="
//nolint:lll
topFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAEIAAA="
// Full folder path for the folder above. // Full folder path for the folder above.
expectedFolderPath = "toplevel/subFolder/subsubfolder" expectedFolderPath = "toplevel/subFolder/subsubfolder"
) )
@ -303,19 +305,35 @@ func TestMailFolderCacheIntegrationSuite(t *testing.T) {
} }
func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() { func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
ctx := context.Background() tests := []struct {
t := suite.T() name string
userID := tester.M365UserID(t) root string
}{
mfc := mailFolderCache{ {
userID: userID, name: "Default Root",
gs: suite.gs, root: rootFolderAlias,
},
{
name: "Node Root",
root: topFolderID,
},
} }
ctx := context.Background()
userID := tester.M365UserID(suite.T())
require.NoError(t, mfc.Populate(ctx)) for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
mfc := mailFolderCache{
userID: userID,
gs: suite.gs,
}
p, err := mfc.IDToPath(ctx, testFolderID) require.NoError(t, mfc.Populate(ctx, test.root))
require.NoError(t, err)
assert.Equal(t, expectedFolderPath, p.String()) p, err := mfc.IDToPath(ctx, testFolderID)
require.NoError(t, err)
assert.Equal(t, expectedFolderPath, p.String())
})
}
} }

View File

@ -395,7 +395,7 @@ func maybeGetAndPopulateFolderResolver(
return nil, nil return nil, nil
} }
if err := res.Populate(ctx); err != nil { if err := res.Populate(ctx, rootFolderAlias); err != nil {
return nil, errors.Wrap(err, "populating directory resolver") return nil, errors.Wrap(err, "populating directory resolver")
} }

View File

@ -36,6 +36,9 @@ type ContainerResolver interface {
// to that container. The path has a similar format to paths on the local // to that container. The path has a similar format to paths on the local
// file system. // file system.
IDToPath(ctx context.Context, m365ID string) (*path.Builder, error) IDToPath(ctx context.Context, m365ID string) (*path.Builder, error)
// Populate performs any setup logic the resolver may need. // Populate performs initialization steps for the resolver
Populate(context.Context) error // @param ctx is necessary param for Graph API tracing
// @param baseFolderID represents the M365ID base that the resolver will
// conclude its search. Default input is "".
Populate(ctx context.Context, baseFolderID string) error
} }