Second attempt at directory cache/resolver for a user (#724)
## Description Directory cache capable of converting a folder ID to a complete path to the folder ## Type of change Please check the type of change your PR introduces: - [x] 🌻 Feature - [ ] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Test - [ ] 🐹 Trivial/Minor ## Issue(s) <!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. --> #456 ## Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
784f006da5
commit
4a96f2571d
179
src/internal/connector/exchange/mail_folder_cache.go
Normal file
179
src/internal/connector/exchange/mail_folder_cache.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package exchange
|
||||||
|
|
||||||
|
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"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/internal/connector/graph"
|
||||||
|
"github.com/alcionai/corso/internal/path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// rootFolderAlias is the per-user root container alias for the exchange email
|
||||||
|
// hierarchy.
|
||||||
|
rootFolderAlias = "msgfolderroot"
|
||||||
|
// nextDataLink is a random map key so we can iterate through delta results.
|
||||||
|
nextDataLink = "@odata.nextLink"
|
||||||
|
)
|
||||||
|
|
||||||
|
type container interface {
|
||||||
|
descendable
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mailFolder struct {
|
||||||
|
models.MailFolderable
|
||||||
|
p *path.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf mailFolder) Path() *path.Builder {
|
||||||
|
return mf.p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mf *mailFolder) SetPath(newPath *path.Builder) {
|
||||||
|
mf.p = newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
type mailFolderCache struct {
|
||||||
|
cache map[string]cachedContainer
|
||||||
|
gs graph.Service
|
||||||
|
userID 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(context.Context) error {
|
||||||
|
wantedOpts := []string{"displayName", "parentFolderId"}
|
||||||
|
|
||||||
|
opts, err := optionsForMailFoldersItem(wantedOpts)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "getting options for mail folders %v", wantedOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := mc.
|
||||||
|
gs.
|
||||||
|
Client().
|
||||||
|
UsersById(mc.userID).
|
||||||
|
MailFoldersById(rootFolderAlias).
|
||||||
|
GetWithRequestConfigurationAndResponseHandler(opts, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "fetching root folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
p: &path.Builder{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mailFolderCache) Populate(ctx context.Context) error {
|
||||||
|
if mc.cache == nil {
|
||||||
|
mc.cache = map[string]cachedContainer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mc.populateRoot(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := mc.
|
||||||
|
gs.
|
||||||
|
Client().
|
||||||
|
UsersById(mc.userID).
|
||||||
|
MailFolders().
|
||||||
|
Delta()
|
||||||
|
|
||||||
|
var errs *multierror.Error
|
||||||
|
|
||||||
|
for {
|
||||||
|
resp, err := builder.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range resp.GetValue() {
|
||||||
|
if err := checkRequiredValues(f); err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mc.cache[*f.GetId()] = &mailFolder{MailFolderable: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := resp.GetAdditionalData()
|
||||||
|
|
||||||
|
n, ok := r[nextDataLink]
|
||||||
|
if !ok || n == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
link := *(n.(*string))
|
||||||
|
builder = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs.ErrorOrNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *mailFolderCache) IDToPath(
|
||||||
|
ctx context.Context,
|
||||||
|
folderID string,
|
||||||
|
) (*path.Builder, error) {
|
||||||
|
c, ok := mc.cache[folderID]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("folder %s not cached", folderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := c.Path()
|
||||||
|
if p != nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPath, err := mc.IDToPath(ctx, *c.GetParentFolderId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "retrieving parent folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := parentPath.Append(*c.GetDisplayName())
|
||||||
|
c.SetPath(fullPath)
|
||||||
|
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
||||||
321
src/internal/connector/exchange/mail_folder_cache_test.go
Normal file
321
src/internal/connector/exchange/mail_folder_cache_test.go
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
package exchange
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
stdpath "path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/internal/connector/graph"
|
||||||
|
"github.com/alcionai/corso/internal/path"
|
||||||
|
"github.com/alcionai/corso/internal/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Need to use a hard-coded ID because GetAllFolderNamesForUser only gets
|
||||||
|
// top-level folders right now.
|
||||||
|
//nolint:lll
|
||||||
|
testFolderID = "AAMkAGZmNjNlYjI3LWJlZWYtNGI4Mi04YjMyLTIxYThkNGQ4NmY1MwAuAAAAAADCNgjhM9QmQYWNcI7hCpPrAQDSEBNbUIB9RL6ePDeF3FIYAABl7AqpAAA="
|
||||||
|
|
||||||
|
// Full folder path for the folder above.
|
||||||
|
expectedFolderPath = "toplevel/subFolder/subsubfolder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockContainer struct {
|
||||||
|
id *string
|
||||||
|
name *string
|
||||||
|
parentID *string
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func (m mockContainer) GetId() *string {
|
||||||
|
return m.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockContainer) GetDisplayName() *string {
|
||||||
|
return m.name
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func (m mockContainer) GetParentFolderId() *string {
|
||||||
|
return m.parentID
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailFolderCacheUnitSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMailFolderCacheUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(MailFolderCacheUnitSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *MailFolderCacheUnitSuite) TestCheckRequiredValues() {
|
||||||
|
id := uuid.NewString()
|
||||||
|
name := "foo"
|
||||||
|
parentID := uuid.NewString()
|
||||||
|
emptyString := ""
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
c mockContainer
|
||||||
|
check assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NilID",
|
||||||
|
c: mockContainer{
|
||||||
|
id: nil,
|
||||||
|
name: &name,
|
||||||
|
parentID: &parentID,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NilDisplayName",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &id,
|
||||||
|
name: nil,
|
||||||
|
parentID: &parentID,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NilParentFolderID",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &id,
|
||||||
|
name: &name,
|
||||||
|
parentID: nil,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyID",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &emptyString,
|
||||||
|
name: &name,
|
||||||
|
parentID: &parentID,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyDisplayName",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &id,
|
||||||
|
name: &emptyString,
|
||||||
|
parentID: &parentID,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptyParentFolderID",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &id,
|
||||||
|
name: &name,
|
||||||
|
parentID: &emptyString,
|
||||||
|
},
|
||||||
|
check: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AllValues",
|
||||||
|
c: mockContainer{
|
||||||
|
id: &id,
|
||||||
|
name: &name,
|
||||||
|
parentID: &parentID,
|
||||||
|
},
|
||||||
|
check: assert.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
test.check(t, checkRequiredValues(test.c))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockCachedContainer(name string) *mockCachedContainer {
|
||||||
|
return &mockCachedContainer{
|
||||||
|
id: uuid.NewString(),
|
||||||
|
parentID: uuid.NewString(),
|
||||||
|
displayName: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockCachedContainer struct {
|
||||||
|
id string
|
||||||
|
parentID string
|
||||||
|
displayName string
|
||||||
|
p *path.Builder
|
||||||
|
expectedPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func (m mockCachedContainer) GetId() *string {
|
||||||
|
return &m.id
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:revive
|
||||||
|
func (m mockCachedContainer) GetParentFolderId() *string {
|
||||||
|
return &m.parentID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockCachedContainer) GetDisplayName() *string {
|
||||||
|
return &m.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockCachedContainer) Path() *path.Builder {
|
||||||
|
return m.p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCachedContainer) SetPath(newPath *path.Builder) {
|
||||||
|
m.p = newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfiguredMailFolderCacheUnitSuite cannot run its tests in parallel.
|
||||||
|
type ConfiguredMailFolderCacheUnitSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
mc mailFolderCache
|
||||||
|
|
||||||
|
allContainers []*mockCachedContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfiguredMailFolderCacheUnitSuite) SetupTest() {
|
||||||
|
suite.allContainers = []*mockCachedContainer{}
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
suite.allContainers = append(
|
||||||
|
suite.allContainers,
|
||||||
|
newMockCachedContainer(strings.Repeat("sub", i)+"folder"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base case for the recursive lookup.
|
||||||
|
suite.allContainers[0].p = path.Builder{}.Append(suite.allContainers[0].displayName)
|
||||||
|
suite.allContainers[0].expectedPath = suite.allContainers[0].displayName
|
||||||
|
|
||||||
|
for i := 1; i < len(suite.allContainers); i++ {
|
||||||
|
suite.allContainers[i].parentID = suite.allContainers[i-1].id
|
||||||
|
suite.allContainers[i].expectedPath = stdpath.Join(
|
||||||
|
suite.allContainers[i-1].expectedPath,
|
||||||
|
suite.allContainers[i].displayName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.mc = mailFolderCache{cache: map[string]cachedContainer{}}
|
||||||
|
|
||||||
|
for _, c := range suite.allContainers {
|
||||||
|
suite.mc.cache[c.id] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfiguredMailFolderCacheUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ConfiguredMailFolderCacheUnitSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, c := range suite.allContainers {
|
||||||
|
suite.T().Run(*c.GetDisplayName(), func(t *testing.T) {
|
||||||
|
p, err := suite.mc.IDToPath(ctx, c.id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, c.expectedPath, p.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
c := suite.allContainers[len(suite.allContainers)-1]
|
||||||
|
|
||||||
|
p, err := suite.mc.IDToPath(ctx, c.id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, c.expectedPath, p.String())
|
||||||
|
|
||||||
|
c.parentID = "foo"
|
||||||
|
|
||||||
|
p, err = suite.mc.IDToPath(ctx, c.id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, c.expectedPath, p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderErrorsParentNotFound() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
last := suite.allContainers[len(suite.allContainers)-1]
|
||||||
|
almostLast := suite.allContainers[len(suite.allContainers)-2]
|
||||||
|
|
||||||
|
delete(suite.mc.cache, almostLast.id)
|
||||||
|
|
||||||
|
_, err := suite.mc.IDToPath(ctx, last.id)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderErrorsNotFound() {
|
||||||
|
t := suite.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err := suite.mc.IDToPath(ctx, "foo")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailFolderCacheIntegrationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
gs graph.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *MailFolderCacheIntegrationSuite) SetupSuite() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
_, err := tester.GetRequiredEnvVars(tester.M365AcctCredEnvs...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
a := tester.NewM365Account(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m365, err := a.M365Config()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
service, err := createService(m365, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
suite.gs = service
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMailFolderCacheIntegrationSuite(t *testing.T) {
|
||||||
|
if err := tester.RunOnAny(
|
||||||
|
tester.CorsoCITests,
|
||||||
|
tester.CorsoGraphConnectorTests,
|
||||||
|
); err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Run(t, new(MailFolderCacheIntegrationSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *MailFolderCacheIntegrationSuite) TestDeltaFetch() {
|
||||||
|
ctx := context.Background()
|
||||||
|
t := suite.T()
|
||||||
|
userID := tester.M365UserID(t)
|
||||||
|
|
||||||
|
mfc := mailFolderCache{
|
||||||
|
userID: userID,
|
||||||
|
gs: suite.gs,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, mfc.Populate(ctx))
|
||||||
|
|
||||||
|
p, err := mfc.IDToPath(ctx, testFolderID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedFolderPath, p.String())
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
mscontacts "github.com/microsoftgraph/msgraph-sdk-go/users/item/contacts"
|
mscontacts "github.com/microsoftgraph/msgraph-sdk-go/users/item/contacts"
|
||||||
msevents "github.com/microsoftgraph/msgraph-sdk-go/users/item/events"
|
msevents "github.com/microsoftgraph/msgraph-sdk-go/users/item/events"
|
||||||
msfolder "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders"
|
msfolder "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders"
|
||||||
|
msfolderitem "github.com/microsoftgraph/msgraph-sdk-go/users/item/mailfolders/item"
|
||||||
msmessage "github.com/microsoftgraph/msgraph-sdk-go/users/item/messages"
|
msmessage "github.com/microsoftgraph/msgraph-sdk-go/users/item/messages"
|
||||||
msitem "github.com/microsoftgraph/msgraph-sdk-go/users/item/messages/item"
|
msitem "github.com/microsoftgraph/msgraph-sdk-go/users/item/messages/item"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -224,6 +225,27 @@ func optionsForMailFolders(moreOps []string) (*msfolder.MailFoldersRequestBuilde
|
|||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// optionsForMailFoldersItem transforms the options into a more dynamic call for MailFoldersById.
|
||||||
|
// moreOps is a []string of options(e.g. "displayName", "isHidden")
|
||||||
|
// Returns first call in MailFoldersById().GetWithRequestConfigurationAndResponseHandler(options, handler)
|
||||||
|
func optionsForMailFoldersItem(
|
||||||
|
moreOps []string,
|
||||||
|
) (*msfolderitem.MailFolderItemRequestBuilderGetRequestConfiguration, error) {
|
||||||
|
selecting, err := buildOptions(moreOps, folders)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestParameters := &msfolderitem.MailFolderItemRequestBuilderGetQueryParameters{
|
||||||
|
Select: selecting,
|
||||||
|
}
|
||||||
|
options := &msfolderitem.MailFolderItemRequestBuilderGetRequestConfiguration{
|
||||||
|
QueryParameters: requestParameters,
|
||||||
|
}
|
||||||
|
|
||||||
|
return options, nil
|
||||||
|
}
|
||||||
|
|
||||||
// optionsForEvents ensures valid option inputs for exchange.Events
|
// optionsForEvents ensures valid option inputs for exchange.Events
|
||||||
// @return is first call in Events().GetWithRequestConfigurationAndResponseHandler(options, handler)
|
// @return is first call in Events().GetWithRequestConfigurationAndResponseHandler(options, handler)
|
||||||
func optionsForEvents(moreOps []string) (*msevents.EventsRequestBuilderGetRequestConfiguration, error) {
|
func optionsForEvents(moreOps []string) (*msevents.EventsRequestBuilderGetRequestConfiguration, error) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user