Skip to content
This repository has been archived by the owner on Oct 11, 2024. It is now read-only.

use delta queries for calendar events #2154

Merged
merged 4 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/internal/connector/data_collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
getSelector func(t *testing.T) selectors.Selector
}{
{
name: suite.user + " Email",
name: "Email",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.MailFolders([]string{exchange.DefaultMailFolder}, selectors.PrefixMatch()))
Expand All @@ -76,7 +76,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
},
},
{
name: suite.user + " Contacts",
name: "Contacts",
getSelector: func(t *testing.T) selectors.Selector {
sel := selectors.NewExchangeBackup(selUsers)
sel.Include(sel.ContactFolders([]string{exchange.DefaultContactFolder}, selectors.PrefixMatch()))
Expand All @@ -85,7 +85,7 @@ func (suite *ConnectorDataCollectionIntegrationSuite) TestExchangeDataCollection
},
},
// {
// name: suite.user + " Events",
// name: "Events",
// getSelector: func(t *testing.T) selectors.Selector {
// sel := selectors.NewExchangeBackup(selUsers)
// sel.Include(sel.EventCalendars([]string{exchange.DefaultCalendar}, selectors.PrefixMatch()))
Expand Down
61 changes: 44 additions & 17 deletions src/internal/connector/exchange/api/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,29 +144,25 @@ func (c Events) EnumerateContainers(
// item pager
// ---------------------------------------------------------------------------

type eventWrapper struct {
models.EventCollectionResponseable
}

func (ew eventWrapper) GetOdataDeltaLink() *string {
return nil
}

var _ itemPager = &eventPager{}

const (
eventBetaDeltaURLTemplate = "https://graph.microsoft.com/beta/users/%s/calendars/%s/events/delta"
)

type eventPager struct {
gs graph.Servicer
builder *users.ItemCalendarsItemEventsRequestBuilder
options *users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration
builder *users.ItemCalendarsItemEventsDeltaRequestBuilder
options *users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration
}

func (p *eventPager) getPage(ctx context.Context) (pageLinker, error) {
resp, err := p.builder.Get(ctx, p.options)
return eventWrapper{resp}, err
return resp, err
}

func (p *eventPager) setNext(nextLink string) {
p.builder = users.NewItemCalendarsItemEventsRequestBuilder(nextLink, p.gs.Adapter())
p.builder = users.NewItemCalendarsItemEventsDeltaRequestBuilder(nextLink, p.gs.Adapter())
}

func (p *eventPager) valuesIn(pl pageLinker) ([]getIDAndAddtler, error) {
Expand All @@ -182,23 +178,54 @@ func (c Events) GetAddedAndRemovedItemIDs(
return nil, nil, DeltaUpdate{}, err
}

var errs *multierror.Error
var (
resetDelta bool
errs *multierror.Error
)

options, err := optionsForEventsByCalendar([]string{"id"})
options, err := optionsForEventsByCalendarDelta([]string{"id"})
if err != nil {
return nil, nil, DeltaUpdate{}, err
}

builder := service.Client().UsersById(user).CalendarsById(calendarID).Events()
if len(oldDelta) > 0 {
builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(oldDelta, service.Adapter())
pgr := &eventPager{service, builder, options}

added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr)
// note: happy path, not the error condition
if err == nil {
return added, removed, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// on bad deltas we retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, DeltaUpdate{}, err
}

resetDelta = true
errs = nil
}

// Graph SDK only supports delta queries against events on the beta version, so we're
// manufacturing use of the beta version url to make the call instead.
// See: https://learn.microsoft.com/ko-kr/graph/api/event-delta?view=graph-rest-beta&tabs=http
// Note that the delta item body is skeletal compared to the actual event struct. Lucky
// for us, we only need the item ID. As a result, even though we hacked the version, the
Comment on lines +213 to +214
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you check that items don't get skipped accidentally due to us selecting the bare minimum fields? Tbh, I'm not sure if this is a feature or a "bug" in how Graph works, but using $select can sometimes cause a delta endpoint to skip returning results if they don't match what's in the $select. For example, not having isRead in the select for emails will cause emails where the isRead flag has been changed to be skipped because it doesn't update the mod time. This will take some playing around and is probably easiest to do with GraphExplorer/postman

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good call out. I'll do some manual testing. This might be especially interesting in this case, because the delta body for an event are only able to contain the id, startTime, and endTime. Hopefully the delta will catch changes on unrepresented properties, like renames and etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, two findings.

  1. everything works. Changes to name, desc, attendees, etc, are all picked up by the delta.
  2. we're not allowed to select values in the first place (see the error below). If anything, we're lucky that the client is tossing the use of options for us.
    "error": {
        "code": "ErrorInvalidUrlQuery",
        "message": "The following parameters are not supported with change tracking over the 'SyncEvents' resource: '$orderby, $filter, $select, $expand, $search'."
    }

// response body parses properly into the v1.0 structs and complies with our wanted interfaces.
// Likewise, the NextLink and DeltaLink odata tags carry our hack forward, so the rest of the code
// works as intended (until, at least, we want to _not_ call the beta anymore).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

rawURL := fmt.Sprintf(eventBetaDeltaURLTemplate, user, calendarID)
builder := users.NewItemCalendarsItemEventsDeltaRequestBuilder(rawURL, service.Adapter())
pgr := &eventPager{service, builder, options}

added, _, _, err := getItemsAddedAndRemovedFromContainer(ctx, pgr)
added, removed, deltaURL, err := getItemsAddedAndRemovedFromContainer(ctx, pgr)
if err != nil {
return nil, nil, DeltaUpdate{}, err
}

// Events don't have a delta endpoint so just return an empty string.
return added, nil, DeltaUpdate{}, errs.ErrorOrNil()
return added, removed, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
}

// ---------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions src/internal/connector/exchange/api/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,19 @@ func optionsForContactFoldersItemDelta(
}

// optionsForEvents ensures a valid option inputs for `exchange.Events` when selected from within a Calendar
func optionsForEventsByCalendar(
func optionsForEventsByCalendarDelta(
moreOps []string,
) (*users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration, error) {
) (*users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForEvents)
if err != nil {
return nil, err
}

requestParameters := &users.ItemCalendarsItemEventsRequestBuilderGetQueryParameters{
requestParameters := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetQueryParameters{
Select: selecting,
}

options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{
options := &users.ItemCalendarsItemEventsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}

Expand Down
7 changes: 7 additions & 0 deletions src/internal/connector/exchange/data_collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ func (suite *DataCollectionsIntegrationSuite) TestDelta() {
selectors.PrefixMatch(),
)[0],
},
{
name: "Events",
scope: selectors.NewExchangeBackup(users).EventCalendars(
[]string{DefaultCalendar},
selectors.PrefixMatch(),
)[0],
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
Expand Down
44 changes: 44 additions & 0 deletions src/internal/operations/backup_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,12 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
)
}

eventDBF := func(id, timeStamp, subject, body string) []byte {
return mockconnector.GetMockEventWith(
suite.user, subject, body, body,
now, now, false)
}

dataset := map[path.CategoryType]struct {
dbf dataBuilderFunc
dests map[string]contDeets
Expand All @@ -706,6 +712,13 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
container2: {},
},
},
path.EventsCategory: {
dbf: eventDBF,
dests: map[string]contDeets{
container1: {},
container2: {},
},
},
}

for category, gen := range dataset {
Expand Down Expand Up @@ -820,6 +833,11 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
t,
cli.ContactFoldersById(containerID).Delete(ctx, nil),
"deleting a contacts folder")
case path.EventsCategory:
require.NoError(
t,
cli.CalendarsById(containerID).Delete(ctx, nil),
"deleting a calendar")
}
}
},
Expand Down Expand Up @@ -897,6 +915,16 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
body.SetDisplayName(&containerRename)
_, err = ccf.Patch(ctx, body, nil)
require.NoError(t, err, "updating contact folder name")

case path.EventsCategory:
ccf := cli.CalendarsById(containerID)

body, err := ccf.Get(ctx, nil)
require.NoError(t, err, "getting calendar")

body.SetName(&containerRename)
_, err = ccf.Patch(ctx, body, nil)
require.NoError(t, err, "updating calendar name")
}
}
},
Expand Down Expand Up @@ -926,6 +954,14 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {

_, err = cli.ContactFoldersById(containerID).Contacts().Post(ctx, body, nil)
require.NoError(t, err, "posting contact item")

case path.EventsCategory:
_, itemData := generateItemData(t, category, suite.user, eventDBF)
body, err := support.CreateEventFromBytes(itemData)
require.NoError(t, err, "transforming event bytes to eventable")

_, err = cli.CalendarsById(containerID).Events().Post(ctx, body, nil)
require.NoError(t, err, "posting events item")
}
}
},
Expand Down Expand Up @@ -955,6 +991,14 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {

err = cli.ContactsById(ids[0]).Delete(ctx, nil)
require.NoError(t, err, "deleting contact item: %s", support.ConnectorStackErrorTrace(err))

case path.EventsCategory:
ids, _, _, err := ac.Events().GetAddedAndRemovedItemIDs(ctx, suite.user, containerID, "")
require.NoError(t, err, "getting event ids")
require.NotEmpty(t, ids, "event ids in folder")

err = cli.CalendarsById(ids[0]).Delete(ctx, nil)
require.NoError(t, err, "deleting calendar: %s", support.ConnectorStackErrorTrace(err))
}
}
},
Expand Down