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"
multierror "github.com/hashicorp/go-multierror"
"github.com/microsoftgraph/msgraph-sdk-go/models"
msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders/delta"
msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders/item/childfolders/delta"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path"
)
var _ cachedContainer = &mailFolder{}
// 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.
@ -21,11 +22,16 @@ type cachedContainer interface {
SetPath(*path.Builder)
}
// mailFolder structure that implements the cachedContainer interface
type mailFolder struct {
models.MailFolderable
folder container
p *path.Builder
}
//=========================================
// Required Functions to satisfy interfaces
//=====================================
func (mf mailFolder) Path() *path.Builder {
return mf.p
}
@ -34,15 +40,36 @@ func (mf *mailFolder) SetPath(newPath *path.Builder) {
mf.p = newPath
}
func (mf *mailFolder) GetDisplayName() *string {
return mf.folder.GetDisplayName()
}
//nolint:revive
func (mf *mailFolder) GetId() *string {
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 string
userID, rootID string
}
// populateRoot fetches and populates the root folder in the cache so the cache
// knows when to stop resolving the path.
func (mc *mailFolderCache) populateRoot(ctx context.Context) error {
// 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"}
opts, err := optionsForMailFoldersItem(wantedOpts)
@ -54,7 +81,7 @@ func (mc *mailFolderCache) populateRoot(ctx context.Context) error {
gs.
Client().
UsersById(mc.userID).
MailFoldersById(rootFolderAlias).
MailFoldersById(directoryID).
Get(ctx, opts)
if err != nil {
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.
idPtr := f.GetId()
if idPtr == nil || len(*idPtr) == 0 {
return errors.New("root folder has no ID")
}
mc.cache[*idPtr] = &mailFolder{
MailFolderable: f,
temp := mailFolder{
folder: f,
p: &path.Builder{},
}
mc.cache[*idPtr] = &temp
mc.rootID = *idPtr
return nil
}
// 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 {
@ -93,26 +125,33 @@ func checkRequiredValues(c container) error {
return nil
}
func (mc *mailFolderCache) Populate(ctx context.Context) error {
if mc.cache == nil {
mc.cache = map[string]cachedContainer{}
// Populate utility function for populating the mailFolderCache.
// Number of Graph Queries: 1.
// @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
}
builder := mc.
query := mc.
gs.
Client().
UsersById(mc.userID).
MailFolders().
MailFoldersById(mc.rootID).ChildFolders().
Delta()
var errs *multierror.Error
// TODO: Cannot use Iterator for delta
// Awaiting resolution: https://github.com/microsoftgraph/msgraph-sdk-go/issues/272
for {
resp, err := builder.Get(ctx, nil)
resp, err := query.Get(ctx, nil)
if err != nil {
return err
}
@ -123,7 +162,9 @@ func (mc *mailFolderCache) Populate(ctx context.Context) error {
continue
}
mc.cache[*f.GetId()] = &mailFolder{MailFolderable: f}
mc.cache[*f.GetId()] = &mailFolder{
folder: f,
}
}
r := resp.GetAdditionalData()
@ -134,7 +175,7 @@ func (mc *mailFolderCache) Populate(ctx context.Context) error {
}
link := *(n.(*string))
builder = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
query = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
}
return errs.ErrorOrNil()
@ -164,3 +205,14 @@ func (mc *mailFolderCache) IDToPath(
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
testFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAABl7AqpAAA="
//nolint:lll
topFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAAAAAAEIAAA="
// Full folder path for the folder above.
expectedFolderPath = "toplevel/subFolder/subsubfolder"
)
@ -303,19 +305,35 @@ func TestMailFolderCacheIntegrationSuite(t *testing.T) {
}
func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
tests := []struct {
name string
root string
}{
{
name: "Default Root",
root: rootFolderAlias,
},
{
name: "Node Root",
root: topFolderID,
},
}
ctx := context.Background()
t := suite.T()
userID := tester.M365UserID(t)
userID := tester.M365UserID(suite.T())
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
mfc := mailFolderCache{
userID: userID,
gs: suite.gs,
}
require.NoError(t, mfc.Populate(ctx))
require.NoError(t, mfc.Populate(ctx, test.root))
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
}
if err := res.Populate(ctx); err != nil {
if err := res.Populate(ctx, rootFolderAlias); err != nil {
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
// file system.
IDToPath(ctx context.Context, m365ID string) (*path.Builder, error)
// Populate performs any setup logic the resolver may need.
Populate(context.Context) error
// Populate performs initialization steps for the resolver
// @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
}