Update folder cache population * try to fetch missing parent folders * refresh current folder if parent wasn't cached * only populate paths during populatePaths not during IDToFolder Manually tested email folder resolver population with stable reproducer and succeeded --- #### Does this PR need a docs update or release note? - [x] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [ ] ⛔ No #### Type of change - [ ] 🌻 Feature - [x] 🐛 Bugfix - [ ] 🗺️ Documentation - [ ] 🤖 Supportability/Tests - [ ] 💻 CI/Deployment - [ ] 🧹 Tech Debt/Cleanup #### Issue(s) #### Test Plan - [x] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
378 lines
11 KiB
Go
378 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/alcionai/clues"
|
|
"github.com/microsoft/kiota-abstractions-go/serialization"
|
|
kjson "github.com/microsoft/kiota-serialization-json-go"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
|
"github.com/microsoftgraph/msgraph-sdk-go/users"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
|
"github.com/alcionai/corso/src/internal/connector/graph"
|
|
"github.com/alcionai/corso/src/internal/connector/graph/api"
|
|
"github.com/alcionai/corso/src/pkg/backup/details"
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
"github.com/alcionai/corso/src/pkg/selectors"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// controller
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (c Client) Contacts() Contacts {
|
|
return Contacts{c}
|
|
}
|
|
|
|
// Contacts is an interface-compliant provider of the client.
|
|
type Contacts struct {
|
|
Client
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// methods
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// CreateContactFolder makes a contact folder with the displayName of folderName.
|
|
// If successful, returns the created folder object.
|
|
func (c Contacts) CreateContactFolder(
|
|
ctx context.Context,
|
|
user, folderName string,
|
|
) (models.ContactFolderable, error) {
|
|
requestBody := models.NewContactFolder()
|
|
temp := folderName
|
|
requestBody.SetDisplayName(&temp)
|
|
|
|
mdl, err := c.Stable.Client().Users().ByUserId(user).ContactFolders().Post(ctx, requestBody, nil)
|
|
if err != nil {
|
|
return nil, graph.Wrap(ctx, err, "creating contact folder")
|
|
}
|
|
|
|
return mdl, nil
|
|
}
|
|
|
|
// DeleteContainer deletes the ContactFolder associated with the M365 ID if permissions are valid.
|
|
func (c Contacts) DeleteContainer(
|
|
ctx context.Context,
|
|
user, folderID string,
|
|
) error {
|
|
// deletes require unique http clients
|
|
// https://github.com/alcionai/corso/issues/2707
|
|
srv, err := NewService(c.Credentials)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
err = srv.Client().Users().ByUserId(user).ContactFolders().ByContactFolderId(folderID).Delete(ctx, nil)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetItem retrieves a Contactable item.
|
|
func (c Contacts) GetItem(
|
|
ctx context.Context,
|
|
user, itemID string,
|
|
immutableIDs bool,
|
|
_ *fault.Bus, // no attachments to iterate over, so this goes unused
|
|
) (serialization.Parsable, *details.ExchangeInfo, error) {
|
|
options := &users.ItemContactsContactItemRequestBuilderGetRequestConfiguration{
|
|
Headers: buildPreferHeaders(false, immutableIDs),
|
|
}
|
|
|
|
cont, err := c.Stable.Client().Users().ByUserId(user).Contacts().ByContactId(itemID).Get(ctx, options)
|
|
if err != nil {
|
|
return nil, nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return cont, ContactInfo(cont), nil
|
|
}
|
|
|
|
func (c Contacts) GetContainerByID(
|
|
ctx context.Context,
|
|
userID, dirID string,
|
|
) (graph.Container, error) {
|
|
queryParams := &users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "displayName", "parentFolderId"},
|
|
},
|
|
}
|
|
|
|
resp, err := c.Stable.Client().Users().ByUserId(userID).ContactFolders().ByContactFolderId(dirID).Get(ctx, queryParams)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// EnumerateContainers iterates through all of the users current
|
|
// contacts folders, converting each to a graph.CacheFolder, and calling
|
|
// fn(cf) on each one.
|
|
// Folder hierarchy is represented in its current state, and does
|
|
// not contain historical data.
|
|
func (c Contacts) EnumerateContainers(
|
|
ctx context.Context,
|
|
userID, baseDirID string,
|
|
fn func(graph.CachedContainer) error,
|
|
errs *fault.Bus,
|
|
) error {
|
|
service, err := c.Service()
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
queryParams := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "displayName", "parentFolderId"},
|
|
},
|
|
}
|
|
|
|
el := errs.Local()
|
|
builder := service.Client().
|
|
Users().
|
|
ByUserId(userID).
|
|
ContactFolders().
|
|
ByContactFolderId(baseDirID).
|
|
ChildFolders()
|
|
|
|
for {
|
|
if el.Failure() != nil {
|
|
break
|
|
}
|
|
|
|
resp, err := builder.Get(ctx, queryParams)
|
|
if err != nil {
|
|
return graph.Stack(ctx, err)
|
|
}
|
|
|
|
for _, fold := range resp.GetValue() {
|
|
if el.Failure() != nil {
|
|
return el.Failure()
|
|
}
|
|
|
|
if err := graph.CheckIDAndName(fold); err != nil {
|
|
errs.AddRecoverable(graph.Stack(ctx, err).Label(fault.LabelForceNoBackupCreation))
|
|
continue
|
|
}
|
|
|
|
fctx := clues.Add(
|
|
ctx,
|
|
"container_id", ptr.Val(fold.GetId()),
|
|
"container_display_name", ptr.Val(fold.GetDisplayName()))
|
|
|
|
temp := graph.NewCacheFolder(fold, nil, nil)
|
|
if err := fn(&temp); err != nil {
|
|
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
|
|
continue
|
|
}
|
|
}
|
|
|
|
link, ok := ptr.ValOK(resp.GetOdataNextLink())
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
builder = users.NewItemContactFoldersItemChildFoldersRequestBuilder(link, service.Adapter())
|
|
}
|
|
|
|
return el.Failure()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// item pager
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ itemPager = &contactPager{}
|
|
|
|
type contactPager struct {
|
|
gs graph.Servicer
|
|
builder *users.ItemContactFoldersItemContactsRequestBuilder
|
|
options *users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration
|
|
}
|
|
|
|
func NewContactPager(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user, directoryID string,
|
|
immutableIDs bool,
|
|
) itemPager {
|
|
queryParams := &users.ItemContactFoldersItemContactsRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemContactFoldersItemContactsRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "parentFolderId"},
|
|
},
|
|
Headers: buildPreferHeaders(true, immutableIDs),
|
|
}
|
|
|
|
builder := gs.Client().Users().ByUserId(user).ContactFolders().ByContactFolderId(directoryID).Contacts()
|
|
|
|
return &contactPager{gs, builder, queryParams}
|
|
}
|
|
|
|
func (p *contactPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) {
|
|
resp, err := p.builder.Get(ctx, p.options)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return api.EmptyDeltaLinker[models.Contactable]{PageLinkValuer: resp}, nil
|
|
}
|
|
|
|
func (p *contactPager) setNext(nextLink string) {
|
|
p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter())
|
|
}
|
|
|
|
// non delta pagers don't need reset
|
|
func (p *contactPager) reset(context.Context) {}
|
|
|
|
func (p *contactPager) valuesIn(pl api.PageLinker) ([]getIDAndAddtler, error) {
|
|
return toValues[models.Contactable](pl)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// delta item pager
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var _ itemPager = &contactDeltaPager{}
|
|
|
|
type contactDeltaPager struct {
|
|
gs graph.Servicer
|
|
user string
|
|
directoryID string
|
|
builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder
|
|
options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration
|
|
}
|
|
|
|
func getContactDeltaBuilder(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user string,
|
|
directoryID string,
|
|
options *users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration,
|
|
) *users.ItemContactFoldersItemContactsDeltaRequestBuilder {
|
|
builder := gs.Client().Users().ByUserId(user).ContactFolders().ByContactFolderId(directoryID).Contacts().Delta()
|
|
return builder
|
|
}
|
|
|
|
func NewContactDeltaPager(
|
|
ctx context.Context,
|
|
gs graph.Servicer,
|
|
user, directoryID, deltaURL string,
|
|
immutableIDs bool,
|
|
) itemPager {
|
|
options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{
|
|
QueryParameters: &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetQueryParameters{
|
|
Select: []string{"id", "parentFolderId"},
|
|
},
|
|
Headers: buildPreferHeaders(true, immutableIDs),
|
|
}
|
|
|
|
var builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder
|
|
if deltaURL != "" {
|
|
builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(deltaURL, gs.Adapter())
|
|
} else {
|
|
builder = getContactDeltaBuilder(ctx, gs, user, directoryID, options)
|
|
}
|
|
|
|
return &contactDeltaPager{gs, user, directoryID, builder, options}
|
|
}
|
|
|
|
func (p *contactDeltaPager) getPage(ctx context.Context) (api.DeltaPageLinker, error) {
|
|
resp, err := p.builder.Get(ctx, p.options)
|
|
if err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (p *contactDeltaPager) setNext(nextLink string) {
|
|
p.builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(nextLink, p.gs.Adapter())
|
|
}
|
|
|
|
func (p *contactDeltaPager) reset(ctx context.Context) {
|
|
p.builder = getContactDeltaBuilder(ctx, p.gs, p.user, p.directoryID, p.options)
|
|
}
|
|
|
|
func (p *contactDeltaPager) valuesIn(pl api.PageLinker) ([]getIDAndAddtler, error) {
|
|
return toValues[models.Contactable](pl)
|
|
}
|
|
|
|
func (c Contacts) GetAddedAndRemovedItemIDs(
|
|
ctx context.Context,
|
|
user, directoryID, oldDelta string,
|
|
immutableIDs bool,
|
|
canMakeDeltaQueries bool,
|
|
) ([]string, []string, DeltaUpdate, error) {
|
|
service, err := c.Service()
|
|
if err != nil {
|
|
return nil, nil, DeltaUpdate{}, graph.Stack(ctx, err)
|
|
}
|
|
|
|
ctx = clues.Add(
|
|
ctx,
|
|
"category", selectors.ExchangeContact,
|
|
"container_id", directoryID)
|
|
|
|
pager := NewContactPager(ctx, service, user, directoryID, immutableIDs)
|
|
deltaPager := NewContactDeltaPager(ctx, service, user, directoryID, oldDelta, immutableIDs)
|
|
|
|
return getAddedAndRemovedItemIDs(ctx, service, pager, deltaPager, oldDelta, canMakeDeltaQueries)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Serialization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Serialize rserializes the item into a byte slice.
|
|
func (c Contacts) Serialize(
|
|
ctx context.Context,
|
|
item serialization.Parsable,
|
|
user, itemID string,
|
|
) ([]byte, error) {
|
|
contact, ok := item.(models.Contactable)
|
|
if !ok {
|
|
return nil, clues.New(fmt.Sprintf("item is not a Contactable: %T", item))
|
|
}
|
|
|
|
ctx = clues.Add(ctx, "item_id", ptr.Val(contact.GetId()))
|
|
|
|
var (
|
|
err error
|
|
writer = kjson.NewJsonSerializationWriter()
|
|
)
|
|
|
|
defer writer.Close()
|
|
|
|
if err = writer.WriteObjectValue("", contact); err != nil {
|
|
return nil, graph.Stack(ctx, err)
|
|
}
|
|
|
|
bs, err := writer.GetSerializedContent()
|
|
if err != nil {
|
|
return nil, graph.Wrap(ctx, err, "serializing contact")
|
|
}
|
|
|
|
return bs, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func ContactInfo(contact models.Contactable) *details.ExchangeInfo {
|
|
name := ptr.Val(contact.GetDisplayName())
|
|
created := ptr.Val(contact.GetCreatedDateTime())
|
|
|
|
return &details.ExchangeInfo{
|
|
ItemType: details.ExchangeContact,
|
|
ContactName: name,
|
|
Created: created,
|
|
Modified: ptr.OrNow(contact.GetLastModifiedDateTime()),
|
|
}
|
|
}
|