Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deep connector MS Dynamics365 Sales (Read) #157

Merged
merged 3 commits into from
Apr 16, 2024

Conversation

Cobalt0s
Copy link
Collaborator

@Cobalt0s Cobalt0s commented Apr 3, 2024

Closes #186

@RajatPawar RajatPawar self-requested a review April 4, 2024 07:25
@Cobalt0s Cobalt0s force-pushed the deep_ms_sales branch 4 times, most recently from a1de53f to 779f196 Compare April 4, 2024 19:57
Copy link
Collaborator Author

@Cobalt0s Cobalt0s left a comment

Choose a reason for hiding this comment

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

@RajatPawar, here is an initial explanation and questions to existing/future code changes

@@ -31,3 +31,14 @@ func InterpretError(res *http.Response, body []byte) error {

return NewHTTPStatusError(res.StatusCode, fmt.Errorf("%w: %s", ErrUnknown, string(body)))
}

func PanicRecovery(wrapup func(cause error)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Every connector captures panic errors and returns it as normal error. Wrapup is a callback after recovery

}

if h.JSON != nil && mediaType == "application/json" {
return h.JSON(res, body)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Depending on a ContentType a connector can parse error response in JSON, XML, HTML whatever impl we want to provide.
Every connector understands JSON structure in their own way. Other media types could be added.

// empty response body should not be parsed as JSON since it will cause ajson to err
if len(body) == 0 {
return nil, nil //nolint:nilnil
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it looks like every CRUD operation does this check (except GET, could be a bug), pulled it to this method since all of them call it at the end.

// ParamAssurance checks that param data is valid
// Every param instance must implement it.
type ParamAssurance interface {
ValidateParams() error
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are several parameters with single responsibilities: Client/Workspace/Module. There could be more of these shared parameters.
Every parameter must determine if its value is required/optional by implementing ParamAssurance.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very cool package, thanks for introducing it!

common/parse.go Outdated

// JSONManager is a helpful wrapper of ajson library that adds errors when querying JSON payload
// and provides common conversion methods.
JSONManager = jsonManager{}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Querying JSON content deals with finding "keys" and validating expected data type of the "value".
ErrNotArray, ErrNotObject errors can be universally shared for all JSON parsers

Copy link
Contributor

Choose a reason for hiding this comment

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

great idea!

opt(params)
}

return params, params.ValidateParams()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a construcotr of connector parameters from Option functions with required field validation

// * singular record
// * list of records
// * hybrid, list of records with extra fields describing list.
func getMarshaledData(records []map[string]interface{}, fields []string) ([]common.ReadResultRow, error) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can we marshal singular record in case of
GET domain/cats/<catID> vs GET domain/cats
Also LIST of cats could return additional parameters stored outside of array, How those fields are returned?

Copy link
Contributor

Choose a reason for hiding this comment

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

For now, our connector only does LIST and that's ok. We don't need to support the GET use case yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will remove the FIXME comment. I guess for future I was wondering if fields could perform queries like: [*].name, [*].meta.createdAt, listType, expectedNumRecords.

msdsales/read.go Outdated
// $filter = query functions, comparisons
// TODO pagination
// $top = <int> of entries to return (ignored if header <Prefer: odata.maxpagesize>)
// $count = counts all existing rows (@odata.count)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Microsoft's Read is very flexible, see options. Should we support all query capabilities?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for pointing this out @Cobalt0s! For now, we will just support the basic functionality of querying by field names, but this is really great to know. Can you replace the comments on line 13 to 24 with something like:

// Microsoft API supports other capabilities like filtering, grouping, and sorting which we can potentially tap into later.
// See https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api#odata-query-options

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, bottom line is I have described these options in docs.

msdsales/read.go Outdated Show resolved Hide resolved
}

res, err = conn.Read(ctx, common.ReadParams{
NextPage: string(res.NextPage), // TODO should Read params have NextPageToken type?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ReadParams.NextPage(string) <- doesn't match type with -> ReadResult.NextPage(NextPageToken)

Copy link
Contributor

Choose a reason for hiding this comment

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

Great catch, can you update the ReadParams struct in common/types.go to have NextPage be of the type NextPageToken token?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed.
In places where it was expected to use as string instead of direct conversion I added a method. Let me know if it is not ok.

type NextPageToken string

func (t NextPageToken) String() string {
	return string(t)
}

"github.com/amp-labs/connectors/test/utils"
)

func main() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

// Strategy is configuration of how a connector handles faulty requests
// connector will create Retry based on this config every time it attempts to make API call.
type Strategy interface {
Start() Retry
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

type parameters struct {
paramsbuilder.Client
paramsbuilder.Workspace
paramsbuilder.Module
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Cobalt0s Cobalt0s marked this pull request as ready for review April 8, 2024 14:38
@Cobalt0s Cobalt0s changed the title feat: Deep connector MS Dynamics365 Sales feat: Deep connector MS Dynamics365 Sales (Read) Apr 9, 2024
common/parse.go Outdated

// JSONManager is a helpful wrapper of ajson library that adds errors when querying JSON payload
// and provides common conversion methods.
JSONManager = jsonManager{}
Copy link
Contributor

Choose a reason for hiding this comment

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

great idea!

// Strategy is configuration of how a connector handles faulty requests
// connector will create Retry based on this config every time it attempts to make API call.
type Strategy interface {
Start() Retry
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey @Cobalt0s while this is a really good idea, we actually already handle retries in the server. We're almost done with building a sophisticated rate limiter that handles retries and employs different retry intervals according to the provider and other overrides that can be builder or consumer configurable. The overall design principle we're using is that the Connectors library handles making API calls & result + error parsing, while orchestration is handled by the server. So can you please remove the reqrepeater package?

msdsales/read_test.go Outdated Show resolved Hide resolved
"garbage": {}
}`)
})),
expectedErrs: []error{errors.New("wrong request: wrong key 'value'")},
Copy link
Contributor

Choose a reason for hiding this comment

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

Hard coding a particular error string to expect leads to a fragile test case because if we modified our error message later, this test will break. The test case right above is OK since the mock server returns a specific error string, so it's ok to hardcode that in expectedErrs

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is a smell code. There was another test where I used type comparison. To be more general I added reflection check. For this test checking that error comes from ajson lib should be enough.

expectedErrs - used when you know exact error
expectedErrTypes - used less frequently and relies on type rather than content

msdsales/read_test.go Outdated Show resolved Hide resolved
}

res, err = conn.Read(ctx, common.ReadParams{
NextPage: string(res.NextPage), // TODO should Read params have NextPageToken type?
Copy link
Contributor

Choose a reason for hiding this comment

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

Great catch, can you update the ReadParams struct in common/types.go to have NextPage be of the type NextPageToken token?

test/msdsales/paginate/main.go Outdated Show resolved Hide resolved
// ParamAssurance checks that param data is valid
// Every param instance must implement it.
type ParamAssurance interface {
ValidateParams() error
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very cool package, thanks for introducing it!

msdsales/errors_test.go Outdated Show resolved Hide resolved
msdsales/methods.go Outdated Show resolved Hide resolved
Copy link
Contributor

@laurenzlong laurenzlong left a comment

Choose a reason for hiding this comment

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

Thanks so much!

@laurenzlong laurenzlong merged commit 0e98118 into amp-labs:main Apr 16, 2024
4 checks passed
@Cobalt0s Cobalt0s deleted the deep_ms_sales branch April 16, 2024 19:15
@Cobalt0s Cobalt0s self-assigned this May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add MS Dynamics365 Sales read connector
2 participants