Fetch mod time when getting added and removed items (#4266)

Also return mod time when available if
getting the set of added and removed
items. This will be leveraged in later
PRs to implement kopia assisted
incrementals for exchange

Does not change any logic in
collections right now, just adds the
fields to be returned

Also adds an additional return value
denoting if the mod times are expected
to be valid. This is required because
events delta cannot return mod time

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

- [ ] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #2023

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-09-18 15:30:56 -07:00 committed by GitHub
parent 647f7326d7
commit 4a9951b876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 302 additions and 110 deletions

View File

@ -160,7 +160,7 @@ func populateCollections(
ictx = clues.Add(ictx, "previous_path", prevPath) ictx = clues.Add(ictx, "previous_path", prevPath)
added, removed, newDelta, err := bh.itemEnumerator(). added, _, removed, newDelta, err := bh.itemEnumerator().
GetAddedAndRemovedItemIDs( GetAddedAndRemovedItemIDs(
ictx, ictx,
qp.ProtectedResource.ID(), qp.ProtectedResource.ID(),
@ -201,7 +201,7 @@ func populateCollections(
collections[cID] = &edc collections[cID] = &edc
for _, add := range added { for add := range added {
edc.added[add] = struct{}{} edc.added[add] = struct{}{}
} }

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"sync" "sync"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -72,14 +73,15 @@ func (mg mockGetter) GetAddedAndRemovedItemIDs(
_ bool, _ bool,
_ bool, _ bool,
) ( ) (
[]string, map[string]time.Time,
bool,
[]string, []string,
api.DeltaUpdate, api.DeltaUpdate,
error, error,
) { ) {
results, ok := mg.results[cID] results, ok := mg.results[cID]
if !ok { if !ok {
return nil, nil, api.DeltaUpdate{}, clues.New("mock not found for " + cID) return nil, false, nil, api.DeltaUpdate{}, clues.New("mock not found for " + cID)
} }
delta := results.newDelta delta := results.newDelta
@ -87,7 +89,12 @@ func (mg mockGetter) GetAddedAndRemovedItemIDs(
delta.URL = "" delta.URL = ""
} }
return results.added, results.removed, delta, results.err resAdded := make(map[string]time.Time, len(results.added))
for _, add := range results.added {
resAdded[add] = time.Time{}
}
return resAdded, false, results.removed, delta, results.err
} }
var _ graph.ContainerResolver = &mockResolver{} var _ graph.ContainerResolver = &mockResolver{}

View File

@ -2,6 +2,7 @@ package exchange
import ( import (
"context" "context"
"time"
"github.com/microsoft/kiota-abstractions-go/serialization" "github.com/microsoft/kiota-abstractions-go/serialization"
@ -30,7 +31,7 @@ type addedAndRemovedItemGetter interface {
user, containerID, oldDeltaToken string, user, containerID, oldDeltaToken string,
immutableIDs bool, immutableIDs bool,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, api.DeltaUpdate, error) ) (map[string]time.Time, bool, []string, api.DeltaUpdate, error)
} }
type itemGetterSerializer interface { type itemGetterSerializer interface {

View File

@ -5,6 +5,7 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/common/pii" "github.com/alcionai/corso/src/internal/common/pii"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
@ -153,13 +154,13 @@ func populateCollections(
// and will return an error if a delta token is queried. // and will return an error if a delta token is queried.
canMakeDeltaQueries := len(ptr.Val(c.GetEmail())) > 0 canMakeDeltaQueries := len(ptr.Val(c.GetEmail())) > 0
add, rem, du, err := bh.getChannelMessageIDs(ctx, cID, prevDelta, canMakeDeltaQueries) add, _, rem, du, err := bh.getChannelMessageIDs(ctx, cID, prevDelta, canMakeDeltaQueries)
if err != nil { if err != nil {
el.AddRecoverable(ctx, clues.Stack(err)) el.AddRecoverable(ctx, clues.Stack(err))
continue continue
} }
added := str.SliceToMap(add) added := str.SliceToMap(maps.Keys(add))
removed := str.SliceToMap(rem) removed := str.SliceToMap(rem)
if len(du.URL) > 0 { if len(du.URL) > 0 {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -57,8 +58,14 @@ func (bh mockBackupHandler) getChannelMessageIDs(
_ context.Context, _ context.Context,
_, _ string, _, _ string,
_ bool, _ bool,
) ([]string, []string, api.DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, api.DeltaUpdate, error) {
return bh.messageIDs, bh.deletedMsgIDs, api.DeltaUpdate{}, bh.messagesErr idRes := make(map[string]time.Time, len(bh.messageIDs))
for _, id := range bh.messageIDs {
idRes[id] = time.Time{}
}
return idRes, true, bh.deletedMsgIDs, api.DeltaUpdate{}, bh.messagesErr
} }
func (bh mockBackupHandler) includeContainer( func (bh mockBackupHandler) includeContainer(

View File

@ -2,6 +2,7 @@ package groups
import ( import (
"context" "context"
"time"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -40,7 +41,7 @@ func (bh channelsBackupHandler) getChannelMessageIDs(
ctx context.Context, ctx context.Context,
channelID, prevDelta string, channelID, prevDelta string,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, api.DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, api.DeltaUpdate, error) {
return bh.ac.GetChannelMessageIDs(ctx, bh.protectedResource, channelID, prevDelta, canMakeDeltaQueries) return bh.ac.GetChannelMessageIDs(ctx, bh.protectedResource, channelID, prevDelta, canMakeDeltaQueries)
} }

View File

@ -2,6 +2,7 @@ package groups
import ( import (
"context" "context"
"time"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -25,7 +26,7 @@ type backupHandler interface {
ctx context.Context, ctx context.Context,
channelID, prevDelta string, channelID, prevDelta string,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, api.DeltaUpdate, error) ) (map[string]time.Time, bool, []string, api.DeltaUpdate, error)
// includeContainer evaluates whether the channel is included // includeContainer evaluates whether the channel is included
// in the provided scope. // in the provided scope.

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -381,12 +382,12 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
var ( var (
err error err error
items []string items map[string]time.Time
) )
switch category { switch category {
case path.EmailCategory: case path.EmailCategory:
items, _, _, err = ac.Mail().GetAddedAndRemovedItemIDs( items, _, _, _, err = ac.Mail().GetAddedAndRemovedItemIDs(
ctx, ctx,
uidn.ID(), uidn.ID(),
containerID, containerID,
@ -395,7 +396,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
true) true)
case path.EventsCategory: case path.EventsCategory:
items, _, _, err = ac.Events().GetAddedAndRemovedItemIDs( items, _, _, _, err = ac.Events().GetAddedAndRemovedItemIDs(
ctx, ctx,
uidn.ID(), uidn.ID(),
containerID, containerID,
@ -404,7 +405,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
true) true)
case path.ContactsCategory: case path.ContactsCategory:
items, _, _, err = ac.Contacts().GetAddedAndRemovedItemIDs( items, _, _, _, err = ac.Contacts().GetAddedAndRemovedItemIDs(
ctx, ctx,
uidn.ID(), uidn.ID(),
containerID, containerID,
@ -423,7 +424,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
dest := dataset[category].dests[destName] dest := dataset[category].dests[destName]
dest.locRef = locRef.String() dest.locRef = locRef.String()
dest.containerID = containerID dest.containerID = containerID
dest.itemRefs = items dest.itemRefs = maps.Keys(items)
dataset[category].dests[destName] = dest dataset[category].dests[destName] = dest
// Add the directory and all its ancestors to the cache so we can compare // Add the directory and all its ancestors to the cache so we can compare

View File

@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -35,6 +36,10 @@ func (p *channelMessagePageCtrl) GetPage(
return resp, graph.Stack(ctx, err).OrNil() return resp, graph.Stack(ctx, err).OrNil()
} }
func (p *channelMessagePageCtrl) ValidModTimes() bool {
return true
}
func (c Channels) NewChannelMessagePager( func (c Channels) NewChannelMessagePager(
teamID, channelID string, teamID, channelID string,
selectProps ...string, selectProps ...string,
@ -100,6 +105,10 @@ func (p *channelMessageDeltaPageCtrl) Reset(context.Context) {
Delta() Delta()
} }
func (p *channelMessageDeltaPageCtrl) ValidModTimes() bool {
return true
}
func (c Channels) NewChannelMessageDeltaPager( func (c Channels) NewChannelMessageDeltaPager(
teamID, channelID, prevDelta string, teamID, channelID, prevDelta string,
selectProps ...string, selectProps ...string,
@ -141,16 +150,16 @@ func (c Channels) GetChannelMessageIDs(
ctx context.Context, ctx context.Context,
teamID, channelID, prevDeltaLink string, teamID, channelID, prevDeltaLink string,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, DeltaUpdate, error) {
added, removed, du, err := getAddedAndRemovedItemIDs( added, validModTimes, removed, du, err := getAddedAndRemovedItemIDs[models.ChatMessageable](
ctx, ctx,
c.NewChannelMessagePager(teamID, channelID), c.NewChannelMessagePager(teamID, channelID),
c.NewChannelMessageDeltaPager(teamID, channelID, prevDeltaLink), c.NewChannelMessageDeltaPager(teamID, channelID, prevDeltaLink),
prevDeltaLink, prevDeltaLink,
canMakeDeltaQueries, canMakeDeltaQueries,
addedAndRemovedByDeletedDateTime) addedAndRemovedByDeletedDateTime[models.ChatMessageable])
return added, removed, du, clues.Stack(err).OrNil() return added, validModTimes, removed, du, clues.Stack(err).OrNil()
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -180,6 +189,10 @@ func (p *channelMessageRepliesPageCtrl) GetOdataNextLink() *string {
return ptr.To("") return ptr.To("")
} }
func (p *channelMessageRepliesPageCtrl) ValidModTimes() bool {
return true
}
func (c Channels) NewChannelMessageRepliesPager( func (c Channels) NewChannelMessageRepliesPager(
teamID, channelID, messageID string, teamID, channelID, messageID string,
selectProps ...string, selectProps ...string,
@ -242,6 +255,10 @@ func (p *channelPageCtrl) GetPage(
return resp, graph.Stack(ctx, err).OrNil() return resp, graph.Stack(ctx, err).OrNil()
} }
func (p *channelPageCtrl) ValidModTimes() bool {
return false
}
func (c Channels) NewChannelPager( func (c Channels) NewChannelPager(
teamID string, teamID string,
) *channelPageCtrl { ) *channelPageCtrl {

View File

@ -56,7 +56,7 @@ func (suite *ChannelsPagerIntgSuite) TestEnumerateChannelMessages() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
addedIDs, _, du, err := ac.GetChannelMessageIDs( addedIDs, _, _, du, err := ac.GetChannelMessageIDs(
ctx, ctx,
suite.its.group.id, suite.its.group.id,
suite.its.group.testContainerID, suite.its.group.testContainerID,
@ -67,7 +67,7 @@ func (suite *ChannelsPagerIntgSuite) TestEnumerateChannelMessages() {
require.NotZero(t, du.URL, "delta link") require.NotZero(t, du.URL, "delta link")
require.True(t, du.Reset, "reset due to empty prev delta link") require.True(t, du.Reset, "reset due to empty prev delta link")
addedIDs, deletedIDs, du, err := ac.GetChannelMessageIDs( addedIDs, _, deletedIDs, du, err := ac.GetChannelMessageIDs(
ctx, ctx,
suite.its.group.id, suite.its.group.id,
suite.its.group.testContainerID, suite.its.group.testContainerID,
@ -79,7 +79,7 @@ func (suite *ChannelsPagerIntgSuite) TestEnumerateChannelMessages() {
require.NotZero(t, du.URL, "delta link") require.NotZero(t, du.URL, "delta link")
require.False(t, du.Reset, "prev delta link should be valid") require.False(t, du.Reset, "prev delta link should be valid")
for _, id := range addedIDs { for id := range addedIDs {
suite.Run(id+"-replies", func() { suite.Run(id+"-replies", func() {
testEnumerateChannelMessageReplies( testEnumerateChannelMessageReplies(
suite.T(), suite.T(),

View File

@ -17,22 +17,23 @@ const (
// get easily misspelled. // get easily misspelled.
// eg: we don't need a const for "id" // eg: we don't need a const for "id"
const ( const (
bccRecipients = "bccRecipients" bccRecipients = "bccRecipients"
ccRecipients = "ccRecipients" ccRecipients = "ccRecipients"
createdDateTime = "createdDateTime" createdDateTime = "createdDateTime"
displayName = "displayName" displayName = "displayName"
emailAddresses = "emailAddresses" emailAddresses = "emailAddresses"
givenName = "givenName" givenName = "givenName"
isCancelled = "isCancelled" isCancelled = "isCancelled"
isDraft = "isDraft" isDraft = "isDraft"
mobilePhone = "mobilePhone" lastModifiedDateTime = "lastModifiedDateTime"
parentFolderID = "parentFolderId" mobilePhone = "mobilePhone"
receivedDateTime = "receivedDateTime" parentFolderID = "parentFolderId"
recurrence = "recurrence" receivedDateTime = "receivedDateTime"
sentDateTime = "sentDateTime" recurrence = "recurrence"
surname = "surname" sentDateTime = "sentDateTime"
toRecipients = "toRecipients" surname = "surname"
userPrincipalName = "userPrincipalName" toRecipients = "toRecipients"
userPrincipalName = "userPrincipalName"
) )
// header keys // header keys

View File

@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -136,6 +137,10 @@ func (p *contactsPageCtrl) SetNextLink(nextLink string) {
p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter()) p.builder = users.NewItemContactFoldersItemContactsRequestBuilder(nextLink, p.gs.Adapter())
} }
func (p *contactsPageCtrl) ValidModTimes() bool {
return true
}
func (c Contacts) GetItemsInContainerByCollisionKey( func (c Contacts) GetItemsInContainerByCollisionKey(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,
@ -249,12 +254,16 @@ func (p *contactDeltaPager) Reset(ctx context.Context) {
p.builder = getContactDeltaBuilder(ctx, p.gs, p.userID, p.containerID) p.builder = getContactDeltaBuilder(ctx, p.gs, p.userID, p.containerID)
} }
func (p *contactDeltaPager) ValidModTimes() bool {
return true
}
func (c Contacts) GetAddedAndRemovedItemIDs( func (c Contacts) GetAddedAndRemovedItemIDs(
ctx context.Context, ctx context.Context,
userID, containerID, prevDeltaLink string, userID, containerID, prevDeltaLink string,
immutableIDs bool, immutableIDs bool,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, DeltaUpdate, error) {
ctx = clues.Add( ctx = clues.Add(
ctx, ctx,
"data_category", path.ContactsCategory, "data_category", path.ContactsCategory,
@ -266,12 +275,12 @@ func (c Contacts) GetAddedAndRemovedItemIDs(
containerID, containerID,
prevDeltaLink, prevDeltaLink,
immutableIDs, immutableIDs,
idAnd()...) idAnd(lastModifiedDateTime)...)
pager := c.NewContactsPager( pager := c.NewContactsPager(
userID, userID,
containerID, containerID,
immutableIDs, immutableIDs,
idAnd()...) idAnd(lastModifiedDateTime)...)
return getAddedAndRemovedItemIDs[models.Contactable]( return getAddedAndRemovedItemIDs[models.Contactable](
ctx, ctx,
@ -279,5 +288,5 @@ func (c Contacts) GetAddedAndRemovedItemIDs(
deltaPager, deltaPager,
prevDeltaLink, prevDeltaLink,
canMakeDeltaQueries, canMakeDeltaQueries,
addedAndRemovedByAddtlData) addedAndRemovedByAddtlData[models.Contactable])
} }

View File

@ -61,6 +61,10 @@ func (p *driveItemPageCtrl) SetNextLink(nextLink string) {
p.builder = drives.NewItemItemsItemChildrenRequestBuilder(nextLink, p.gs.Adapter()) p.builder = drives.NewItemItemsItemChildrenRequestBuilder(nextLink, p.gs.Adapter())
} }
func (p *driveItemPageCtrl) ValidModTimes() bool {
return true
}
type DriveItemIDType struct { type DriveItemIDType struct {
ItemID string ItemID string
IsFolder bool IsFolder bool
@ -185,6 +189,10 @@ func (p *DriveItemDeltaPageCtrl) Reset(context.Context) {
Delta() Delta()
} }
func (p *DriveItemDeltaPageCtrl) ValidModTimes() bool {
return true
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// user's drives pager // user's drives pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -252,6 +260,10 @@ func (p *userDrivePager) SetNextLink(link string) {
p.builder = users.NewItemDrivesRequestBuilder(link, p.gs.Adapter()) p.builder = users.NewItemDrivesRequestBuilder(link, p.gs.Adapter())
} }
func (p *userDrivePager) ValidModTimes() bool {
return true
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// site's libraries pager // site's libraries pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -303,6 +315,10 @@ func (p *siteDrivePager) SetNextLink(link string) {
p.builder = sites.NewItemDrivesRequestBuilder(link, p.gs.Adapter()) p.builder = sites.NewItemDrivesRequestBuilder(link, p.gs.Adapter())
} }
func (p *siteDrivePager) ValidModTimes() bool {
return true
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// drive pager // drive pager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -141,6 +142,10 @@ func (p *eventsPageCtrl) SetNextLink(nextLink string) {
p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter()) p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter())
} }
func (p *eventsPageCtrl) ValidModTimes() bool {
return true
}
func (c Events) GetItemsInContainerByCollisionKey( func (c Events) GetItemsInContainerByCollisionKey(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,
@ -248,12 +253,16 @@ func (p *eventDeltaPager) Reset(ctx context.Context) {
p.builder = getEventDeltaBuilder(ctx, p.gs, p.userID, p.containerID) p.builder = getEventDeltaBuilder(ctx, p.gs, p.userID, p.containerID)
} }
func (p *eventDeltaPager) ValidModTimes() bool {
return false
}
func (c Events) GetAddedAndRemovedItemIDs( func (c Events) GetAddedAndRemovedItemIDs(
ctx context.Context, ctx context.Context,
userID, containerID, prevDeltaLink string, userID, containerID, prevDeltaLink string,
immutableIDs bool, immutableIDs bool,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, DeltaUpdate, error) {
ctx = clues.Add( ctx = clues.Add(
ctx, ctx,
"data_category", path.EventsCategory, "data_category", path.EventsCategory,
@ -270,7 +279,7 @@ func (c Events) GetAddedAndRemovedItemIDs(
userID, userID,
containerID, containerID,
immutableIDs, immutableIDs,
idAnd()...) idAnd(lastModifiedDateTime)...)
return getAddedAndRemovedItemIDs[models.Eventable]( return getAddedAndRemovedItemIDs[models.Eventable](
ctx, ctx,
@ -278,5 +287,5 @@ func (c Events) GetAddedAndRemovedItemIDs(
deltaPager, deltaPager,
prevDeltaLink, prevDeltaLink,
canMakeDeltaQueries, canMakeDeltaQueries,
addedAndRemovedByAddtlData) addedAndRemovedByAddtlData[models.Eventable])
} }

View File

@ -52,6 +52,10 @@ type Resetter interface {
Reset(context.Context) Reset(context.Context)
} }
type ValidModTimer interface {
ValidModTimes() bool
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// common funcs // common funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -76,6 +80,7 @@ func NextAndDeltaLink(pl DeltaLinker) (string, string) {
type Pager[T any] interface { type Pager[T any] interface {
GetPager[NextLinkValuer[T]] GetPager[NextLinkValuer[T]]
SetNextLinker SetNextLinker
ValidModTimer
} }
func enumerateItems[T any]( func enumerateItems[T any](
@ -114,6 +119,7 @@ type DeltaPager[T any] interface {
GetPager[DeltaLinkValuer[T]] GetPager[DeltaLinkValuer[T]]
Resetter Resetter
SetNextLinker SetNextLinker
ValidModTimer
} }
func deltaEnumerateItems[T any]( func deltaEnumerateItems[T any](
@ -173,7 +179,7 @@ func deltaEnumerateItems[T any](
// shared enumeration runner funcs // shared enumeration runner funcs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type addedAndRemovedHandler[T any] func(items []T) ([]string, []string, error) type addedAndRemovedHandler[T any] func(items []T) (map[string]time.Time, []string, error)
func getAddedAndRemovedItemIDs[T any]( func getAddedAndRemovedItemIDs[T any](
ctx context.Context, ctx context.Context,
@ -182,16 +188,16 @@ func getAddedAndRemovedItemIDs[T any](
prevDeltaLink string, prevDeltaLink string,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
aarh addedAndRemovedHandler[T], aarh addedAndRemovedHandler[T],
) ([]string, []string, DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, DeltaUpdate, error) {
if canMakeDeltaQueries { if canMakeDeltaQueries {
ts, du, err := deltaEnumerateItems[T](ctx, deltaPager, prevDeltaLink) ts, du, err := deltaEnumerateItems[T](ctx, deltaPager, prevDeltaLink)
if err != nil && (!graph.IsErrInvalidDelta(err) || len(prevDeltaLink) == 0) { if err != nil && (!graph.IsErrInvalidDelta(err) || len(prevDeltaLink) == 0) {
return nil, nil, DeltaUpdate{}, graph.Stack(ctx, err) return nil, false, nil, DeltaUpdate{}, graph.Stack(ctx, err)
} }
if err == nil { if err == nil {
a, r, err := aarh(ts) a, r, err := aarh(ts)
return a, r, du, graph.Stack(ctx, err).OrNil() return a, deltaPager.ValidModTimes(), r, du, graph.Stack(ctx, err).OrNil()
} }
} }
@ -199,12 +205,12 @@ func getAddedAndRemovedItemIDs[T any](
ts, err := enumerateItems(ctx, pager) ts, err := enumerateItems(ctx, pager)
if err != nil { if err != nil {
return nil, nil, DeltaUpdate{}, graph.Stack(ctx, err) return nil, false, nil, DeltaUpdate{}, graph.Stack(ctx, err)
} }
a, r, err := aarh(ts) a, r, err := aarh(ts)
return a, r, du, graph.Stack(ctx, err).OrNil() return a, pager.ValidModTimes(), r, du, graph.Stack(ctx, err).OrNil()
} }
type getIDer interface { type getIDer interface {
@ -218,8 +224,15 @@ type getIDAndAddtler interface {
GetAdditionalData() map[string]any GetAdditionalData() map[string]any
} }
func addedAndRemovedByAddtlData[T any](items []T) ([]string, []string, error) { type getModTimer interface {
added, removed := []string{}, []string{} GetLastModifiedDateTime() *time.Time
}
func addedAndRemovedByAddtlData[T any](
items []T,
) (map[string]time.Time, []string, error) {
added := map[string]time.Time{}
removed := []string{}
for _, item := range items { for _, item := range items {
giaa, ok := any(item).(getIDAndAddtler) giaa, ok := any(item).(getIDAndAddtler)
@ -232,7 +245,13 @@ func addedAndRemovedByAddtlData[T any](items []T) ([]string, []string, error) {
// be 'changed' or 'deleted'. We don't really care about the cause: both // be 'changed' or 'deleted'. We don't really care about the cause: both
// cases are handled the same way in storage. // cases are handled the same way in storage.
if giaa.GetAdditionalData()[graph.AddtlDataRemoved] == nil { if giaa.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
added = append(added, ptr.Val(giaa.GetId())) var modTime time.Time
if mt, ok := giaa.(getModTimer); ok {
modTime = ptr.Val(mt.GetLastModifiedDateTime())
}
added[ptr.Val(giaa.GetId())] = modTime
} else { } else {
removed = append(removed, ptr.Val(giaa.GetId())) removed = append(removed, ptr.Val(giaa.GetId()))
} }
@ -248,8 +267,11 @@ type getIDAndDeletedDateTimer interface {
GetDeletedDateTime() *time.Time GetDeletedDateTime() *time.Time
} }
func addedAndRemovedByDeletedDateTime[T any](items []T) ([]string, []string, error) { func addedAndRemovedByDeletedDateTime[T any](
added, removed := []string{}, []string{} items []T,
) (map[string]time.Time, []string, error) {
added := map[string]time.Time{}
removed := []string{}
for _, item := range items { for _, item := range items {
giaddt, ok := any(item).(getIDAndDeletedDateTimer) giaddt, ok := any(item).(getIDAndDeletedDateTimer)
@ -259,7 +281,13 @@ func addedAndRemovedByDeletedDateTime[T any](items []T) ([]string, []string, err
} }
if giaddt.GetDeletedDateTime() == nil { if giaddt.GetDeletedDateTime() == nil {
added = append(added, ptr.Val(giaddt.GetId())) var modTime time.Time
if mt, ok := giaddt.(getModTimer); ok {
modTime = ptr.Val(mt.GetLastModifiedDateTime())
}
added[ptr.Val(giaddt.GetId())] = modTime
} else { } else {
removed = append(removed, ptr.Val(giaddt.GetId())) removed = append(removed, ptr.Val(giaddt.GetId()))
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"strings" "strings"
"testing" "testing"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -76,16 +77,19 @@ func (p *testPager) GetPage(ctx context.Context) (NextLinkValuer[any], error) {
func (p *testPager) SetNextLink(nextLink string) {} func (p *testPager) SetNextLink(nextLink string) {}
func (p testPager) ValidModTimes() bool { return true }
// mock id pager // mock id pager
var _ Pager[any] = &testIDsPager{} var _ Pager[any] = &testIDsPager{}
type testIDsPager struct { type testIDsPager struct {
t *testing.T t *testing.T
added []string added map[string]time.Time
removed []string removed []string
errorCode string errorCode string
needsReset bool needsReset bool
validModTimes bool
} }
func (p *testIDsPager) GetPage( func (p *testIDsPager) GetPage(
@ -103,10 +107,11 @@ func (p *testIDsPager) GetPage(
values := make([]any, 0, len(p.added)+len(p.removed)) values := make([]any, 0, len(p.added)+len(p.removed))
for _, a := range p.added { for a, modTime := range p.added {
// contact chosen arbitrarily, any exchange model should work // contact chosen arbitrarily, any exchange model should work
itm := models.NewContact() itm := models.NewContact()
itm.SetId(ptr.To(a)) itm.SetId(ptr.To(a))
itm.SetLastModifiedDateTime(ptr.To(modTime))
values = append(values, itm) values = append(values, itm)
} }
@ -132,14 +137,19 @@ func (p *testIDsPager) Reset(context.Context) {
p.errorCode = "" p.errorCode = ""
} }
func (p testIDsPager) ValidModTimes() bool {
return p.validModTimes
}
var _ DeltaPager[any] = &testIDsDeltaPager{} var _ DeltaPager[any] = &testIDsDeltaPager{}
type testIDsDeltaPager struct { type testIDsDeltaPager struct {
t *testing.T t *testing.T
added []string added map[string]time.Time
removed []string removed []string
errorCode string errorCode string
needsReset bool needsReset bool
validModTimes bool
} }
func (p *testIDsDeltaPager) GetPage( func (p *testIDsDeltaPager) GetPage(
@ -157,10 +167,11 @@ func (p *testIDsDeltaPager) GetPage(
values := make([]any, 0, len(p.added)+len(p.removed)) values := make([]any, 0, len(p.added)+len(p.removed))
for _, a := range p.added { for a, modTime := range p.added {
// contact chosen arbitrarily, any exchange model should work // contact chosen arbitrarily, any exchange model should work
itm := models.NewContact() itm := models.NewContact()
itm.SetId(ptr.To(a)) itm.SetId(ptr.To(a))
itm.SetLastModifiedDateTime(ptr.To(modTime))
values = append(values, itm) values = append(values, itm)
} }
@ -186,6 +197,10 @@ func (p *testIDsDeltaPager) Reset(context.Context) {
p.errorCode = "" p.errorCode = ""
} }
func (p testIDsDeltaPager) ValidModTimes() bool {
return p.validModTimes
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tests // Tests
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -252,11 +267,14 @@ func (suite *PagerUnitSuite) TestEnumerateItems() {
func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() { func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
type expected struct { type expected struct {
added []string added map[string]time.Time
removed []string removed []string
deltaUpdate DeltaUpdate deltaUpdate DeltaUpdate
validModTimes bool
} }
now := time.Now()
tests := []struct { tests := []struct {
name string name string
pagerGetter func( pagerGetter func(
@ -265,9 +283,10 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
deltaPagerGetter func( deltaPagerGetter func(
*testing.T, *testing.T,
) DeltaPager[any] ) DeltaPager[any]
prevDelta string prevDelta string
expect expected expect expected
canDelta bool canDelta bool
validModTimes bool
}{ }{
{ {
name: "no prev delta", name: "no prev delta",
@ -276,13 +295,46 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
}, },
deltaPagerGetter: func(t *testing.T) DeltaPager[any] { deltaPagerGetter: func(t *testing.T) DeltaPager[any] {
return &testIDsDeltaPager{ return &testIDsDeltaPager{
t: t, t: t,
added: []string{"uno", "dos"}, added: map[string]time.Time{
"uno": now.Add(time.Minute),
"dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
validModTimes: true,
}
},
expect: expected{
added: map[string]time.Time{
"uno": now.Add(time.Minute),
"dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
deltaUpdate: DeltaUpdate{Reset: true},
validModTimes: true,
},
canDelta: true,
},
{
name: "no prev delta invalid mod times",
pagerGetter: func(t *testing.T) Pager[any] {
return nil
},
deltaPagerGetter: func(t *testing.T) DeltaPager[any] {
return &testIDsDeltaPager{
t: t,
added: map[string]time.Time{
"uno": {},
"dos": {},
},
removed: []string{"tres", "quatro"}, removed: []string{"tres", "quatro"},
} }
}, },
expect: expected{ expect: expected{
added: []string{"uno", "dos"}, added: map[string]time.Time{
"uno": {},
"dos": {},
},
removed: []string{"tres", "quatro"}, removed: []string{"tres", "quatro"},
deltaUpdate: DeltaUpdate{Reset: true}, deltaUpdate: DeltaUpdate{Reset: true},
}, },
@ -295,16 +347,24 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
}, },
deltaPagerGetter: func(t *testing.T) DeltaPager[any] { deltaPagerGetter: func(t *testing.T) DeltaPager[any] {
return &testIDsDeltaPager{ return &testIDsDeltaPager{
t: t, t: t,
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
"dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
validModTimes: true,
} }
}, },
prevDelta: "delta", prevDelta: "delta",
expect: expected{ expect: expected{
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
deltaUpdate: DeltaUpdate{Reset: false}, "dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
deltaUpdate: DeltaUpdate{Reset: false},
validModTimes: true,
}, },
canDelta: true, canDelta: true,
}, },
@ -315,18 +375,26 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
}, },
deltaPagerGetter: func(t *testing.T) DeltaPager[any] { deltaPagerGetter: func(t *testing.T) DeltaPager[any] {
return &testIDsDeltaPager{ return &testIDsDeltaPager{
t: t, t: t,
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
errorCode: "SyncStateNotFound", "dos": now.Add(2 * time.Minute),
needsReset: true, },
removed: []string{"tres", "quatro"},
errorCode: "SyncStateNotFound",
needsReset: true,
validModTimes: true,
} }
}, },
prevDelta: "delta", prevDelta: "delta",
expect: expected{ expect: expected{
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
deltaUpdate: DeltaUpdate{Reset: true}, "dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
deltaUpdate: DeltaUpdate{Reset: true},
validModTimes: true,
}, },
canDelta: true, canDelta: true,
}, },
@ -334,18 +402,26 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
name: "delta not allowed", name: "delta not allowed",
pagerGetter: func(t *testing.T) Pager[any] { pagerGetter: func(t *testing.T) Pager[any] {
return &testIDsPager{ return &testIDsPager{
t: t, t: t,
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
"dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
validModTimes: true,
} }
}, },
deltaPagerGetter: func(t *testing.T) DeltaPager[any] { deltaPagerGetter: func(t *testing.T) DeltaPager[any] {
return nil return nil
}, },
expect: expected{ expect: expected{
added: []string{"uno", "dos"}, added: map[string]time.Time{
removed: []string{"tres", "quatro"}, "uno": now.Add(time.Minute),
deltaUpdate: DeltaUpdate{Reset: true}, "dos": now.Add(2 * time.Minute),
},
removed: []string{"tres", "quatro"},
deltaUpdate: DeltaUpdate{Reset: true},
validModTimes: true,
}, },
canDelta: false, canDelta: false,
}, },
@ -358,18 +434,19 @@ func (suite *PagerUnitSuite) TestGetAddedAndRemovedItemIDs() {
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
defer flush() defer flush()
added, removed, deltaUpdate, err := getAddedAndRemovedItemIDs[any]( added, validModTimes, removed, deltaUpdate, err := getAddedAndRemovedItemIDs[any](
ctx, ctx,
test.pagerGetter(t), test.pagerGetter(t),
test.deltaPagerGetter(t), test.deltaPagerGetter(t),
test.prevDelta, test.prevDelta,
test.canDelta, test.canDelta,
addedAndRemovedByAddtlData) addedAndRemovedByAddtlData[any])
require.NoErrorf(t, err, "getting added and removed item IDs: %+v", clues.ToCore(err)) require.NoErrorf(t, err, "getting added and removed item IDs: %+v", clues.ToCore(err))
require.EqualValues(t, test.expect.added, added, "added item IDs") assert.Equal(t, test.expect.added, added, "added item IDs and mod times")
require.EqualValues(t, test.expect.removed, removed, "removed item IDs") assert.Equal(t, test.expect.validModTimes, validModTimes, "valid mod times")
require.Equal(t, test.expect.deltaUpdate, deltaUpdate, "delta update") assert.EqualValues(t, test.expect.removed, removed, "removed item IDs")
assert.Equal(t, test.expect.deltaUpdate, deltaUpdate, "delta update")
}) })
} }
} }

View File

@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
@ -166,6 +167,10 @@ func (p *mailsPageCtrl) SetNextLink(nextLink string) {
p.builder = users.NewItemMailFoldersItemMessagesRequestBuilder(nextLink, p.gs.Adapter()) p.builder = users.NewItemMailFoldersItemMessagesRequestBuilder(nextLink, p.gs.Adapter())
} }
func (p *mailsPageCtrl) ValidModTimes() bool {
return true
}
func (c Mail) GetItemsInContainerByCollisionKey( func (c Mail) GetItemsInContainerByCollisionKey(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,
@ -279,12 +284,16 @@ func (p *mailDeltaPager) Reset(ctx context.Context) {
p.builder = getMailDeltaBuilder(ctx, p.gs, p.userID, p.containerID) p.builder = getMailDeltaBuilder(ctx, p.gs, p.userID, p.containerID)
} }
func (p *mailDeltaPager) ValidModTimes() bool {
return true
}
func (c Mail) GetAddedAndRemovedItemIDs( func (c Mail) GetAddedAndRemovedItemIDs(
ctx context.Context, ctx context.Context,
userID, containerID, prevDeltaLink string, userID, containerID, prevDeltaLink string,
immutableIDs bool, immutableIDs bool,
canMakeDeltaQueries bool, canMakeDeltaQueries bool,
) ([]string, []string, DeltaUpdate, error) { ) (map[string]time.Time, bool, []string, DeltaUpdate, error) {
ctx = clues.Add( ctx = clues.Add(
ctx, ctx,
"data_category", path.EmailCategory, "data_category", path.EmailCategory,
@ -296,12 +305,12 @@ func (c Mail) GetAddedAndRemovedItemIDs(
containerID, containerID,
prevDeltaLink, prevDeltaLink,
immutableIDs, immutableIDs,
idAnd()...) idAnd(lastModifiedDateTime)...)
pager := c.NewMailPager( pager := c.NewMailPager(
userID, userID,
containerID, containerID,
immutableIDs, immutableIDs,
idAnd()...) idAnd(lastModifiedDateTime)...)
return getAddedAndRemovedItemIDs[models.Messageable]( return getAddedAndRemovedItemIDs[models.Messageable](
ctx, ctx,
@ -309,5 +318,5 @@ func (c Mail) GetAddedAndRemovedItemIDs(
deltaPager, deltaPager,
prevDeltaLink, prevDeltaLink,
canMakeDeltaQueries, canMakeDeltaQueries,
addedAndRemovedByAddtlData) addedAndRemovedByAddtlData[models.Messageable])
} }

View File

@ -8,6 +8,11 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
) )
var (
_ api.Pager[any] = &Pager[any]{}
_ api.DeltaPager[any] = &DeltaPager[any]{}
)
type DeltaNextLinkValues[T any] struct { type DeltaNextLinkValues[T any] struct {
Next *string Next *string
Delta *string Delta *string
@ -61,7 +66,8 @@ func (p *Pager[T]) GetPage(
return &link, p.ToReturn[idx].Err return &link, p.ToReturn[idx].Err
} }
func (p *Pager[T]) SetNextLink(string) {} func (p *Pager[T]) SetNextLink(string) {}
func (p *Pager[T]) ValidModTimes() bool { return true }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// delta pager // delta pager
@ -94,3 +100,4 @@ func (p *DeltaPager[T]) GetPage(
func (p *DeltaPager[T]) SetNextLink(string) {} func (p *DeltaPager[T]) SetNextLink(string) {}
func (p *DeltaPager[T]) Reset(context.Context) {} func (p *DeltaPager[T]) Reset(context.Context) {}
func (p *DeltaPager[T]) ValidModTimes() bool { return true }