Repository: amp-labs/connectors
Pull request #179 (2024-04-08 16:39:02)

feat: Deep connector MS Dynamics365 CRM (ListMetadata) 
Closes https://github.com/amp-labs/connectors/issues/196


Patch:
diff --git a/common/paramsbuilder/params.go b/common/paramsbuilder/params.go
index c14849d7..17ec169b 100644
--- a/common/paramsbuilder/params.go
+++ b/common/paramsbuilder/params.go
@@ -17,14 +17,14 @@ type ParamAssurance interface {
 }
 
 var (
-	ErrMissingClient    = errors.New("JSON http client not set")
+	ErrMissingClient    = errors.New("http client not set")
 	ErrMissingWorkspace = errors.New("missing workspace name")
 )
 
 // Client params sets up authenticated proxy HTTP client
 // This can be reused among other param builders by composition.
 type Client struct {
-	Caller *common.JSONHTTPClient
+	Caller *common.HTTPClient
 }
 
 func (p *Client) ValidateParams() error {
@@ -55,11 +55,9 @@ func (p *Client) WithClient(
 }
 
 func (p *Client) WithAuthenticatedClient(client common.AuthenticatedHTTPClient) {
-	p.Caller = &common.JSONHTTPClient{
-		HTTPClient: &common.HTTPClient{
-			Client:       client,
-			ErrorHandler: common.InterpretError,
-		},
+	p.Caller = &common.HTTPClient{
+		Client:       client,
+		ErrorHandler: common.InterpretError,
 	}
 }
 
diff --git a/common/types.go b/common/types.go
index 731f3b4e..3aa6f67e 100644
--- a/common/types.go
+++ b/common/types.go
@@ -43,6 +43,9 @@ var (
 	// ErrNotJSON is returned when a response is not JSON.
 	ErrNotJSON = errors.New("response is not JSON")
 
+	// ErrNotXML is returned when a response is not XML.
+	ErrNotXML = errors.New("response is not XML")
+
 	// ErrMissingOauthConfig is returned when the OAuth config is missing.
 	ErrMissingOauthConfig = errors.New("missing OAuth config")
 
diff --git a/common/xml.go b/common/xml.go
index 044bc52a..ba716a03 100644
--- a/common/xml.go
+++ b/common/xml.go
@@ -1,12 +1,17 @@
 package common
 
 import (
+	"bytes"
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
+	"mime"
+	"net/http"
 	"strings"
 
 	"github.com/go-playground/validator"
+	"github.com/subchen/go-xmldom"
 )
 
 const (
@@ -18,11 +23,93 @@ const (
 var (
 	//nolint:gochecknoglobals
 	validate          = validator.New()
+	ErrNoXMLRoot      = errors.New("xml document has no root")
 	ErrNotXMLChildren = errors.New("children must be of type 'XMLData' or 'XMLString'")
 	ErrNoSelfClosing  = errors.New("selfClosing cannot be true if children are not present")
 	ErrNoParens       = errors.New("value cannot contain < or >")
 )
 
+// XMLHTTPClient is an HTTP client that can parse XML response.
+type XMLHTTPClient struct {
+	HTTPClient *HTTPClient // underlying HTTP client. Required.
+}
+
+type XMLHTTPResponse struct {
+	// bodyBytes is the raw response body.
+	bodyBytes []byte
+
+	// Code is the HTTP status code of the response.
+	Code int
+
+	// Headers are the HTTP headers of the response.
+	Headers http.Header
+
+	// Body is the unmarshalled response body in XML form. Content is the same as bodyBytes
+	Body *xmldom.Document
+}
+
+func (r XMLHTTPResponse) GetRoot() (*xmldom.Node, error) {
+	if r.Body == nil || r.Body.Root == nil {
+		return nil, ErrNoXMLRoot
+	}
+
+	return r.Body.Root, nil
+}
+
+// Get makes a GET request to the given URL and returns the response body as a XML object.
+// If the response is not a 2xx, an error is returned. If the response is a 401, the caller should
+// refresh the access token and retry the request. If errorHandler is nil, then the default error
+// handler is used. If not, the caller can inject their own error handling logic.
+func (j *XMLHTTPClient) Get(ctx context.Context, url string, headers ...Header) (*XMLHTTPResponse, error) {
+	res, body, err := j.HTTPClient.Get(ctx, url, addAcceptXMLHeader(headers)) //nolint:bodyclose
+	if err != nil {
+		return nil, err
+	}
+
+	return parseXMLResponse(res, body)
+}
+
+// parseXMLResponse parses the given HTTP response and returns a XMLHTTPResponse.
+func parseXMLResponse(res *http.Response, body []byte) (*XMLHTTPResponse, error) {
+	if len(body) == 0 {
+		// Empty XML response is not allowed
+		return nil, ErrNotXML
+	}
+	// Ensure the response is XML
+	ct := res.Header.Get("Content-Type")
+	if len(ct) > 0 {
+		mimeType, _, err := mime.ParseMediaType(ct)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse content type: %w", err)
+		}
+
+		if mimeType != "application/xml" {
+			return nil, fmt.Errorf("%w: expected content type to be application/xml, got %s", ErrNotXML, mimeType)
+		}
+	}
+
+	// Unmarshall the response body into XML
+	xmlBody, err := xmldom.Parse(bytes.NewReader(body))
+	if err != nil {
+		return nil, NewHTTPStatusError(res.StatusCode, fmt.Errorf("failed to unmarshall response body into XML: %w", err))
+	}
+
+	return &XMLHTTPResponse{
+		bodyBytes: body,
+		Code:      res.StatusCode,
+		Headers:   res.Header,
+		Body:      xmlBody,
+	}, nil
+}
+
+func addAcceptXMLHeader(headers []Header) []Header {
+	if headers == nil {
+		headers = make([]Header, 0)
+	}
+
+	return append(headers, Header{Key: "Accept", Value: "application/xml"})
+}
+
 type XMLSchema interface {
 	String() string
 	Validate() error
@@ -166,12 +253,12 @@ func (x *XMLData) String() string {
 
 	end := x.endTag()
 
-	chilren := []string{}
+	children := make([]string, 0)
 	for _, child := range x.Children {
-		chilren = append(chilren, child.String())
+		children = append(children, child.String())
 	}
 
-	return fmt.Sprintf("%s%s%s", start, strings.Join(chilren, ""), end)
+	return fmt.Sprintf("%s%s%s", start, strings.Join(children, ""), end)
 }
 
 func (x *XMLData) startTag() string {
@@ -182,19 +269,19 @@ func (x *XMLData) startTag() string {
 
 	attrStr := strings.Join(attributes, " ")
 
-	var close string //nolint:predeclared
+	var closingTag string //nolint:predeclared
 
 	if x.SelfClosing {
-		close = closeParenWithSlash
+		closingTag = closeParenWithSlash
 	} else {
-		close = closeParen
+		closingTag = closeParen
 	}
 
 	if attrStr == "" {
-		return fmt.Sprintf("%s%s%s", openParen, x.XMLName, close)
+		return fmt.Sprintf("%s%s%s", openParen, x.XMLName, closingTag)
 	}
 
-	return fmt.Sprintf("%s%s %s%s", openParen, x.XMLName, attrStr, close)
+	return fmt.Sprintf("%s%s %s%s", openParen, x.XMLName, attrStr, closingTag)
 }
 
 func (x *XMLData) endTag() string {
diff --git a/microsoftdynamicscrm/connector.go b/microsoftdynamicscrm/connector.go
index 32abf6a8..82ee2d4b 100644
--- a/microsoftdynamicscrm/connector.go
+++ b/microsoftdynamicscrm/connector.go
@@ -16,9 +16,10 @@ var DefaultModuleCRM = paramsbuilder.APIModule{ // nolint: gochecknoglobals
 }
 
 type Connector struct {
-	BaseURL string
-	Module  string
-	Client  *common.JSONHTTPClient
+	BaseURL   string
+	Module    string
+	Client    *common.JSONHTTPClient
+	XMLClient *common.XMLHTTPClient
 }
 
 func NewConnector(opts ...Option) (conn *Connector, outErr error) {
@@ -39,9 +40,15 @@ func NewConnector(opts ...Option) (conn *Connector, outErr error) {
 		return nil, err
 	}
 
+	httpClient := params.Client.Caller
 	conn = &Connector{
 		Module: params.Module.Suffix,
-		Client: params.Client.Caller,
+		Client: &common.JSONHTTPClient{
+			HTTPClient: httpClient,
+		},
+		XMLClient: &common.XMLHTTPClient{
+			HTTPClient: httpClient,
+		},
 	}
 	// connector and its client must mirror base url and provide its own error parser
 	conn.setBaseURL(providerInfo.BaseURL)
diff --git a/microsoftdynamicscrm/metadata.go b/microsoftdynamicscrm/metadata.go
index 89e4ced3..beddf849 100644
--- a/microsoftdynamicscrm/metadata.go
+++ b/microsoftdynamicscrm/metadata.go
@@ -2,13 +2,126 @@ package microsoftdynamicscrm
 
 import (
 	"context"
+	"errors"
+	"fmt"
 
 	"github.com/amp-labs/connectors/common"
+	"github.com/subchen/go-xmldom"
 )
 
+var (
+	CRMMetadataSchemaName = "Microsoft.Dynamics.CRM" // nolint:gochecknoglobals
+
+	ErrMissingSchema      = fmt.Errorf("missing schema %v in response", CRMMetadataSchemaName)
+	ErrMetadataProcessing = errors.New("metadata couldn't be processed")
+	ErrObjectNotFound     = errors.New("object not found")
+)
+
+// Please note: MSDynamics API does not return proper display names for objects and fields,
+// so the ListObjectMetadataResult will have display names that look like "accountleads".
 func (c *Connector) ListObjectMetadata(
 	ctx context.Context, objectNames []string,
 ) (*common.ListObjectMetadataResult, error) {
-	// TODO implement me
-	panic("implement me")
+	// Ensure that objectNames is not empty
+	if len(objectNames) == 0 {
+		return nil, common.ErrMissingObjects
+	}
+
+	rsp, err := c.getXML(ctx, c.getURL("$metadata"))
+	if err != nil {
+		return nil, err
+	}
+
+	root, err := rsp.GetRoot()
+	if err != nil {
+		return nil, err
+	}
+
+	entities, err := extractEntities(root)
+	if err != nil {
+		return nil, err
+	}
+
+	result, err := convertEntitySetToMetadataSet(objectNames, entities)
+	if err != nil {
+		return nil, err
+	}
+
+	return &common.ListObjectMetadataResult{
+		Result: result,
+		Errors: nil,
+	}, nil
+}
+
+// collects field properties and groups them in entities, other data in XML is ignored.
+func extractEntities(root *xmldom.Node) (EntitySet, error) {
+	querySchema := fmt.Sprintf("/DataServices/Schema[@Namespace='%v']", CRMMetadataSchemaName)
+
+	schema := root.QueryOne(querySchema)
+	if schema == nil {
+		return nil, ErrMissingSchema
+	}
+
+	entities := NewEntitySet()
+	// List all field properties that exist for current schema
+	queryListAllSchemaProperties := fmt.Sprintf(
+		"/DataServices/Schema[@Namespace='%v']/EntityType[*]/Property/@Name", CRMMetadataSchemaName)
+	root.QueryEach(queryListAllSchemaProperties, func(index int, property *xmldom.Node) {
+		// parent of a property is an Entity.
+		// Entity may inherit properties from a parent
+		// We save entity name and the name of its parent, so later we can infer all properties by denormalisation
+		entityName := property.Parent.GetAttributeValue("Name")
+		parentName := property.Parent.GetAttributeValue("BaseType")
+		entity := entities.GetOrCreate(entityName, parentName)
+		propertyName := property.GetAttributeValue("Name")
+		entity.AddProperty(propertyName)
+	})
+
+	queryListAbstractEntities := fmt.Sprintf(
+		"/DataServices/Schema[@Namespace='%v']/EntityType[@Abstract='true']", CRMMetadataSchemaName)
+	root.QueryEach(queryListAbstractEntities, func(index int, abstractEntity *xmldom.Node) {
+		if len(abstractEntity.Children) == 0 {
+			// these entities were not included by previous query as they have no properties
+			// we programmatically find these special types, which are "primary values" but for structs
+			// Ex: crmbaseentity, crmmodelbaseentity,
+			entityName := abstractEntity.GetAttributeValue("Name")
+			// effectively only create
+			_ = entities.GetOrCreate(entityName, "")
+		}
+	})
+
+	// link every child with parent completing hierarchy
+	schemaAlias := schema.GetAttributeValue("Alias")
+	if err := entities.MatchParentsWithChildren(schemaAlias); err != nil {
+		return nil, errors.Join(ErrMetadataProcessing, err)
+	}
+
+	return entities, nil
+}
+
+// Select entities that match entity names of interest.
+// Every property has display identical to itself.
+func convertEntitySetToMetadataSet(names []string, entities EntitySet) (map[string]common.ObjectMetadata, error) {
+	result := map[string]common.ObjectMetadata{}
+
+	for _, name := range names {
+		entity, ok := entities[name]
+		if !ok {
+			return nil, fmt.Errorf("unknown entity %v %w", name, ErrObjectNotFound)
+		}
+
+		properties := entity.GetAllProperties()
+		fieldsMap := make(map[string]string)
+
+		for _, p := range properties {
+			fieldsMap[p] = p
+		}
+
+		result[name] = common.ObjectMetadata{
+			DisplayName: name,
+			FieldsMap:   fieldsMap,
+		}
+	}
+
+	return result, nil
 }
diff --git a/microsoftdynamicscrm/methods.go b/microsoftdynamicscrm/methods.go
index 1ee40c16..53c09376 100644
--- a/microsoftdynamicscrm/methods.go
+++ b/microsoftdynamicscrm/methods.go
@@ -47,6 +47,17 @@ func (c *Connector) delete(ctx context.Context, url string) (*common.JSONHTTPRes
 	return rsp, nil
 }
 
+func (c *Connector) getXML(ctx context.Context,
+	url string, headers ...common.Header,
+) (*common.XMLHTTPResponse, error) {
+	rsp, err := c.XMLClient.Get(ctx, url, headers...)
+	if err = handleError(err); err != nil {
+		return nil, err
+	}
+
+	return rsp, nil
+}
+
 func handleError(err error) error {
 	if err == nil {
 		return nil
diff --git a/microsoftdynamicscrm/models.go b/microsoftdynamicscrm/models.go
new file mode 100644
index 00000000..1b1a279f
--- /dev/null
+++ b/microsoftdynamicscrm/models.go
@@ -0,0 +1,92 @@
+package microsoftdynamicscrm
+
+import (
+	"errors"
+	"strings"
+)
+
+var ErrInvalidParentRef = errors.New("referenced parent type doesn't exist for schema")
+
+// EntitySet organised by Entity.Name.
+type EntitySet map[string]*Entity
+
+func NewEntitySet() EntitySet {
+	return make(map[string]*Entity)
+}
+
+func (s EntitySet) GetNames() []string {
+	names := make([]string, len(s))
+	i := 0
+
+	for name := range s {
+		names[i] = name
+		i++
+	}
+
+	return names
+}
+
+func (s EntitySet) GetOrCreate(name, parentName string) *Entity {
+	if _, ok := s[name]; !ok {
+		s[name] = &Entity{
+			Name:       name,
+			properties: make([]string, 0),
+			parentName: parentName,
+		}
+	}
+
+	return s[name]
+}
+
+// Entity represents schema of the data returned by Microsoft
+// fields are inherited from parents so there is a tree hierarchy that can be traversed.
+type Entity struct {
+	Name       string
+	properties []string
+	parentName string
+	parent     *Entity
+}
+
+func (e *Entity) AddProperty(property string) {
+	e.properties = append(e.properties, property)
+}
+
+// GetRawParentName parents that are defined under schema are prefixed with its alias
+// this strips the prefix.
+func (e *Entity) GetRawParentName(schemaAlias string) string {
+	// strip prefix if we are using local schema
+	name, _ := strings.CutPrefix(e.parentName, schemaAlias+".")
+
+	return name
+}
+
+// MatchParentsWithChildren this will link a parent entity for everybody
+// only top root entities will have parent null.
+func (s EntitySet) MatchParentsWithChildren(schemaAlias string) error {
+	for _, entity := range s {
+		name := entity.GetRawParentName(schemaAlias)
+		// if it has parent then match
+		if len(name) != 0 {
+			parent, ok := s[name]
+			if !ok {
+				return ErrInvalidParentRef
+			}
+
+			entity.parent = parent
+		}
+	}
+
+	return nil
+}
+
+// GetAllProperties recursive function that includes inherited fields from parents.
+func (e *Entity) GetAllProperties() []string {
+	if e.parent == nil {
+		// this is root
+		return e.properties
+	}
+
+	parentProperties := e.parent.GetAllProperties()
+
+	return append(e.properties, parentProperties...)
+}
diff --git a/providers/catalog.go b/providers/catalog.go
index f7824d14..dc274ca5 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -14,6 +14,7 @@ const (
 	Pipedrive                           Provider = "pipedrive"
 	Copper                              Provider = "copper"
 	ZohoCRM                             Provider = "zohoCRM"
+	Mural                               Provider = "mural"
 	Klaviyo                             Provider = "klaviyo"
 	Sellsy                              Provider = "sellsy"
 	Attio                               Provider = "attio"
@@ -297,6 +298,33 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		},
 	},
 
+	// Mural configuration
+	Mural: {
+		AuthType: Oauth2,
+		BaseURL:  "https://api.mural.co/api",
+		OauthOpts: OauthOpts{
+			AuthURL:                   "https://api.mural.co/oauth/authorize",
+			TokenURL:                  "https://api.mural.co/oauth/token",
+			ExplicitScopesRequired:    true,
+			ExplicitWorkspaceRequired: false,
+			TokenMetadataFields: TokenMetadataFields{
+				ScopesField: "scope",
+			},
+		},
+		Support: Support{
+			BulkWrite: BulkWriteSupport{
+				Insert: false,
+				Update: false,
+				Upsert: false,
+				Delete: false,
+			},
+			Proxy:     false,
+			Read:      false,
+			Subscribe: false,
+			Write:     false,
+		},
+	},
+
 	// Klaviyo configuration
 	Klaviyo: {
 		AuthType: Oauth2,
@@ -580,7 +608,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	// DocuSign configuration
 	// TODO: we don't have a good way to get the server string yet. Need to make API call to /oauth/userinfo.
 	// Leaving this connector commented out until that is unblocked.
-	//DocuSign: {
+	// DocuSign: {
 	//	AuthType: Oauth2,
 	//	BaseURL:  "https://{{.server}}.docusign.net",
 	//	OauthOpts: OauthOpts{
diff --git a/salesforce/errors.go b/salesforce/errors.go
index 036966ed..1e3b471a 100644
--- a/salesforce/errors.go
+++ b/salesforce/errors.go
@@ -5,8 +5,10 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
 
 	"github.com/amp-labs/connectors/common"
+	"golang.org/x/oauth2"
 )
 
 var (
@@ -72,6 +74,16 @@ func (c *Connector) interpretJSONError(res *http.Response, body []byte) error {
 }
 
 func (c *Connector) HandleError(err error) error {
+	var urlErr *url.Error
+	if errors.As(err, &urlErr) {
+		var oauthErr *oauth2.RetrieveError
+		if urlErr != nil && errors.As(urlErr.Err, &oauthErr) {
+			if oauthErr.ErrorCode == "invalid_grant" {
+				return errors.Join(common.ErrInvalidGrant, err)
+			}
+		}
+	}
+
 	switch {
 	case errors.Is(err, common.ErrAccessToken):
 		// Retryable, so just log and retry
diff --git a/scripts/oauth/token.go b/scripts/oauth/token.go
index 7120f071..06f9f519 100644
--- a/scripts/oauth/token.go
+++ b/scripts/oauth/token.go
@@ -170,12 +170,15 @@ func (a *OAuthApp) processCallback(writer http.ResponseWriter, request *http.Req
 		return
 	}
 
+	// Print the token which will also print raw metadata
+	fmt.Printf("%+v", tok)
+
 	writer.Header().Set("Content-Type", "application/json")
 	writer.Header().Set("Content-Length", strconv.FormatInt(int64(len(jsonBody)), 10))
 	writer.WriteHeader(http.StatusOK)
 
 	// All done
-	if _, err = writer.Write(jsonBody); err != nil { // nosemgrep
+	if _, err = writer.Write([]byte("Received a token, printed in the console")); err != nil { // nosemgrep
 		slog.Error("Error writing token", "error", err)
 
 		os.Exit(1)


Test patch:
diff --git a/microsoftdynamicscrm/metadata_test.go b/microsoftdynamicscrm/metadata_test.go
new file mode 100644
index 00000000..63c1b346
--- /dev/null
+++ b/microsoftdynamicscrm/metadata_test.go
@@ -0,0 +1,217 @@
+package microsoftdynamicscrm
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/interpreter"
+	"github.com/amp-labs/connectors/test/utils/mockutils"
+	"github.com/go-test/deep"
+)
+
+func TestListObjectMetadata(t *testing.T) { // nolint:funlen,gocognit,cyclop
+	t.Parallel()
+
+	fakeServerResp, err := mockutils.DataFromFile("metadata.xml")
+	if err != nil {
+		t.Fatalf("failed to start test, input file missing, %v", err)
+	}
+
+	tests := []struct {
+		name                string
+		input               []string
+		server              *httptest.Server
+		connector           Connector
+		expected            *common.ListObjectMetadataResult
+		expectedFieldsCount map[string]int // used instead of `expected` when response result is too big
+		expectedErrs        []error
+	}{
+		{
+			name:  "At least one object name must be queried",
+			input: nil,
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(http.StatusTeapot)
+			})),
+			expectedErrs: []error{common.ErrMissingObjects},
+		},
+		{
+			name:  "Mime response header expected",
+			input: []string{"msfp_surveyinvite"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(http.StatusTeapot)
+			})),
+			expectedErrs: []error{interpreter.ErrMissingContentType},
+		},
+		{
+			name:  "Missing XML response on status OK",
+			input: []string{"msfp_surveyinvite"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				writeBody(w, "")
+			})),
+			expectedErrs: []error{common.ErrNotXML},
+		},
+		{
+			name:  "Missing XML root",
+			input: []string{"msfp_surveyinvite"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				writeBody(w, `<?xml version="1.0" encoding="utf-8"?>`)
+			})),
+			expectedErrs: []error{common.ErrNoXMLRoot},
+		},
+		{
+			name:  "Server response without CRM Schema",
+			input: []string{"msfp_surveyinvite"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				writeBody(w, `
+<?xml version="1.0" encoding="utf-8"?>
+<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"></edmx:Edmx>`)
+			})),
+			expectedErrs: []error{ErrMissingSchema},
+		},
+		{
+			name:  "Object name cannot be found from server response",
+			input: []string{"msfp_surveyinvite"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				_, _ = w.Write(fakeServerResp)
+			})),
+			expectedErrs: []error{ErrObjectNotFound, errors.New("unknown entity msfp_surveyinvite")}, // nolint:goerr113
+		},
+		{
+			name:  "Correctly list metadata for account leads and invite contact",
+			input: []string{"accountleads", "adx_invitation_invitecontacts"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				_, _ = w.Write(fakeServerResp)
+			})),
+			expected: &common.ListObjectMetadataResult{
+				Result: map[string]common.ObjectMetadata{
+					"accountleads": {
+						DisplayName: "accountleads",
+						FieldsMap: map[string]string{
+							"accountid":                 "accountid",
+							"accountleadid":             "accountleadid",
+							"importsequencenumber":      "importsequencenumber",
+							"leadid":                    "leadid",
+							"name":                      "name",
+							"overriddencreatedon":       "overriddencreatedon",
+							"timezoneruleversionnumber": "timezoneruleversionnumber",
+							"utcconversiontimezonecode": "utcconversiontimezonecode",
+							"versionnumber":             "versionnumber",
+						},
+					},
+					"adx_invitation_invitecontacts": {
+						DisplayName: "adx_invitation_invitecontacts",
+						FieldsMap: map[string]string{
+							"adx_invitation_invitecontactsid": "adx_invitation_invitecontactsid",
+							"adx_invitationid":                "adx_invitationid",
+							"contactid":                       "contactid",
+							"versionnumber":                   "versionnumber",
+						},
+					},
+				},
+				Errors: nil,
+			},
+			expectedErrs: nil,
+		},
+		{
+			// In total phonecall will have 65 fields, where
+			// phonecall 		(has 7 fields) and inherits from
+			// activitypointer 	(has 58 fields), which in turn inherits from
+			// crmbaseentity 	(has 0 fields)
+			name:  "Correctly list metadata for phone calls including inherited fields",
+			input: []string{"phonecall", "activitypointer", "crmbaseentity"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/xml")
+				w.WriteHeader(http.StatusOK)
+				_, _ = w.Write(fakeServerResp)
+			})),
+			expectedFieldsCount: map[string]int{
+				"phonecall":       65,
+				"activitypointer": 58,
+				"crmbaseentity":   0,
+			},
+			expectedErrs: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		// nolint:varnamelen
+		tt := tt // rebind, omit loop side effects for parallel goroutine
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			defer tt.server.Close()
+
+			connector, err := NewConnector(
+				WithAuthenticatedClient(http.DefaultClient),
+				WithWorkspace("test-workspace"),
+			)
+			if err != nil {
+				t.Fatalf("%s: error in test while constructin connector %v", tt.name, err)
+			}
+
+			// for testing we want to redirect calls to our mock server
+			connector.setBaseURL(tt.server.URL)
+
+			// start of tests
+			output, err := connector.ListObjectMetadata(context.Background(), tt.input)
+			if err != nil {
+				if len(tt.expectedErrs) == 0 {
+					t.Fatalf("%s: expected no errors, got: (%v)", tt.name, err)
+				}
+			} else {
+				// check that missing error is what is expected
+				if len(tt.expectedErrs) != 0 {
+					t.Fatalf("%s: expected errors (%v), but got nothing", tt.name, tt.expectedErrs)
+				}
+			}
+
+			// check every error
+			for _, expectedErr := range tt.expectedErrs {
+				if !errors.Is(err, expectedErr) && !strings.Contains(err.Error(), expectedErr.Error()) {
+					t.Fatalf("%s: expected Error: (%v), got: (%v)", tt.name, expectedErr, err)
+				}
+			}
+
+			// compare desired output
+			// there are 2 modes we can compare, by exact field values or matching quantity
+			if len(tt.expectedFieldsCount) != 0 {
+				// we are comparing if number of fields match under ListObjectMetadataResult.Result
+				for entityName, count := range tt.expectedFieldsCount {
+					entity, ok := output.Result[entityName]
+					if !ok {
+						t.Fatalf("%s: expected entity was missing: (%v)", tt.name, entityName)
+					}
+
+					got := len(entity.FieldsMap)
+					if got != count {
+						t.Fatalf("%s: expected entity '%v' to have (%v) fields got: (%v)",
+							tt.name, entityName, count, got)
+					}
+				}
+			} else { // nolint:gocritic
+				// usual comparison of ListObjectMetadataResult
+				if !reflect.DeepEqual(output, tt.expected) {
+					diff := deep.Equal(output, tt.expected)
+					t.Fatalf("%s:, \nexpected: (%v), \ngot: (%v), \ndiff: (%v)",
+						tt.name, tt.expected, output, diff)
+				}
+			}
+		})
+	}
+}
diff --git a/microsoftdynamicscrm/test/metadata.xml b/microsoftdynamicscrm/test/metadata.xml
new file mode 100644
index 00000000..4308e0f7
--- /dev/null
+++ b/microsoftdynamicscrm/test/metadata.xml
@@ -0,0 +1,578 @@
+<?xml version="1.0" encoding="utf-8"?>
+<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
+    <edmx:Reference Uri="https://vocabularies.odata.org/OData.Community.Keys.V1.xml">
+        <edmx:Include Namespace="OData.Community.Keys.V1" Alias="Keys"/>
+        <edmx:IncludeAnnotations TermNamespace="OData.Community.Keys.V1"/>
+    </edmx:Reference>
+    <edmx:Reference Uri="https://vocabularies.odata.org/OData.Community.Display.V1.xml">
+        <edmx:Include Namespace="OData.Community.Display.V1" Alias="Display"/>
+        <edmx:IncludeAnnotations TermNamespace="OData.Community.Display.V1"/>
+    </edmx:Reference>
+    <edmx:DataServices>
+        <Schema Namespace="Microsoft.Dynamics.CRM" Alias="mscrm" xmlns="http://docs.oasis-open.org/odata/ns/edm">
+            <EntityType Name="crmbaseentity" Abstract="true"/>
+            <EntityType Name="accountleads" BaseType="mscrm.crmbaseentity">
+                <Key>
+                    <PropertyRef Name="accountleadid"/>
+                </Key>
+                <Property Name="importsequencenumber" Type="Edm.Int32"/>
+                <Property Name="accountleadid" Type="Edm.Guid"/>
+                <Property Name="name" Type="Edm.String" Unicode="false"/>
+                <Property Name="accountid" Type="Edm.Guid"/>
+                <Property Name="overriddencreatedon" Type="Edm.DateTimeOffset"/>
+                <Property Name="versionnumber" Type="Edm.Int64"/>
+                <Property Name="leadid" Type="Edm.Guid"/>
+                <Property Name="timezoneruleversionnumber" Type="Edm.Int32"/>
+                <Property Name="utcconversiontimezonecode" Type="Edm.Int32"/>
+                <NavigationProperty Name="accountleads_AsyncOperations" Type="Collection(mscrm.asyncoperation)"
+                                    Partner="regardingobjectid_accountleads"/>
+                <NavigationProperty Name="accountleads_MailboxTrackingFolders"
+                                    Type="Collection(mscrm.mailboxtrackingfolder)"
+                                    Partner="regardingobjectid_accountleads"/>
+                <NavigationProperty Name="accountleads_BulkDeleteFailures" Type="Collection(mscrm.bulkdeletefailure)"
+                                    Partner="regardingobjectid_accountleads"/>
+                <NavigationProperty Name="accountleads_PrincipalObjectAttributeAccesses"
+                                    Type="Collection(mscrm.principalobjectattributeaccess)"
+                                    Partner="objectid_accountleads"/>
+            </EntityType>
+            <EntityType Name="adx_invitation_invitecontacts" BaseType="mscrm.crmbaseentity">
+                <Key>
+                    <PropertyRef Name="adx_invitation_invitecontactsid"/>
+                </Key>
+                <Property Name="adx_invitationid" Type="Edm.Guid"/>
+                <Property Name="versionnumber" Type="Edm.Int64"/>
+                <Property Name="contactid" Type="Edm.Guid"/>
+                <Property Name="adx_invitation_invitecontactsid" Type="Edm.Guid"/>
+            </EntityType>
+            <EntityType Name="phonecall" BaseType="mscrm.activitypointer">
+                <Key>
+                    <PropertyRef Name="activityid"/>
+                </Key>
+                <Property Name="subcategory" Type="Edm.String" Unicode="false"/>
+                <Property Name="directioncode" Type="Edm.Boolean"/>
+                <Property Name="overriddencreatedon" Type="Edm.DateTimeOffset"/>
+                <Property Name="category" Type="Edm.String" Unicode="false"/>
+                <Property Name="subscriptionid" Type="Edm.Guid"/>
+                <Property Name="phonenumber" Type="Edm.String" Unicode="false"/>
+                <Property Name="importsequencenumber" Type="Edm.Int32"/>
+                <NavigationProperty Name="phonecall_PostRegardings" Type="Collection(mscrm.postregarding)"
+                                    Partner="regardingobjectid_phonecall"/>
+                <NavigationProperty Name="phonecall_PostFollows" Type="Collection(mscrm.postfollow)"
+                                    Partner="regardingobjectid_phonecall"/>
+                <NavigationProperty Name="regardingobjectid_knowledgebaserecord_phonecall"
+                                    Type="mscrm.knowledgebaserecord"
+                                    Nullable="false" Partner="KnowledgeBaseRecord_PhoneCalls"/>
+                <NavigationProperty Name="phonecall_actioncard" Type="Collection(mscrm.actioncard)"
+                                    Partner="regardingobjectid_phonecall_actioncard"/>
+                <NavigationProperty Name="transactioncurrencyid_phonecall" Type="mscrm.transactioncurrency"
+                                    Nullable="false"
+                                    Partner="TransactionCurrency_PhoneCall"/>
+                <NavigationProperty Name="regardingobjectid_contact_phonecall" Type="mscrm.contact" Nullable="false"
+                                    Partner="Contact_Phonecalls"/>
+                <NavigationProperty Name="sla_phonecall_sla" Type="mscrm.sla" Nullable="false"
+                                    Partner="manualsla_phonecall"/>
+                <NavigationProperty Name="PhoneCall_BulkDeleteFailures" Type="Collection(mscrm.bulkdeletefailure)"
+                                    Partner="regardingobjectid_phonecall"/>
+                <NavigationProperty Name="modifiedonbehalfby_phonecall" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_phonecall_modifiedonbehalfby"/>
+                <NavigationProperty Name="PhoneCall_AsyncOperations" Type="Collection(mscrm.asyncoperation)"
+                                    Partner="regardingobjectid_phonecall"/>
+                <NavigationProperty Name="regardingobjectid_account_phonecall" Type="mscrm.account" Nullable="false"
+                                    Partner="Account_Phonecalls"/>
+                <NavigationProperty Name="createdby_phonecall" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_phonecall_createdby"/>
+                <NavigationProperty Name="createdonbehalfby_phonecall" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_phonecall_createdonbehalfby"/>
+                <NavigationProperty Name="phonecall_activity_parties" Type="Collection(mscrm.activityparty)"
+                                    Partner="activityid_phonecall"/>
+                <NavigationProperty Name="PhoneCall_ProcessSessions" Type="Collection(mscrm.processsession)"
+                                    Partner="regardingobjectid_phonecall"/>
+                <NavigationProperty Name="owningbusinessunit_phonecall" Type="mscrm.businessunit" Nullable="false"
+                                    Partner="business_unit_phone_call_activities"/>
+                <NavigationProperty Name="PhoneCall_DuplicateMatchingRecord" Type="Collection(mscrm.duplicaterecord)"
+                                    Partner="duplicaterecordid_phonecall"/>
+                <NavigationProperty Name="regardingobjectid_knowledgearticle_phonecall" Type="mscrm.knowledgearticle"
+                                    Nullable="false" Partner="KnowledgeArticle_Phonecalls"/>
+                <NavigationProperty Name="stageid_processstage" Type="mscrm.processstage" Nullable="false"
+                                    Partner="processstage_phonecalls"/>
+                <NavigationProperty Name="PhoneCall_DuplicateBaseRecord" Type="Collection(mscrm.duplicaterecord)"
+                                    Partner="baserecordid_phonecall"/>
+                <NavigationProperty Name="phonecall_connections1" Type="Collection(mscrm.connection)"
+                                    Partner="record1id_phonecall"/>
+                <NavigationProperty Name="activityid_activitypointer" Type="mscrm.activitypointer" Nullable="false"
+                                    Partner="activity_pointer_phonecall"/>
+                <NavigationProperty Name="slakpiinstance_phonecall" Type="Collection(mscrm.slakpiinstance)"
+                                    Partner="regarding_phonecall"/>
+                <NavigationProperty Name="slainvokedid_phonecall_sla" Type="mscrm.sla" Nullable="false"
+                                    Partner="sla_phonecall"/>
+                <NavigationProperty Name="PhoneCall_SyncErrors" Type="Collection(mscrm.syncerror)"
+                                    Partner="regardingobjectid_phonecall_syncerror"/>
+                <NavigationProperty Name="modifiedby_phonecall" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_phonecall_modifiedby"/>
+                <NavigationProperty Name="owningteam_phonecall" Type="mscrm.team" Nullable="false"
+                                    Partner="team_phonecall"/>
+                <NavigationProperty Name="PhoneCall_QueueItem" Type="Collection(mscrm.queueitem)"
+                                    Partner="objectid_phonecall"/>
+                <NavigationProperty Name="PhoneCall_Annotation" Type="Collection(mscrm.annotation)"
+                                    Partner="objectid_phonecall"/>
+                <NavigationProperty Name="phonecall_connections2" Type="Collection(mscrm.connection)"
+                                    Partner="record2id_phonecall"/>
+                <NavigationProperty Name="owninguser_phonecall" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="user_phonecall"/>
+                <NavigationProperty Name="phonecall_principalobjectattributeaccess"
+                                    Type="Collection(mscrm.principalobjectattributeaccess)"
+                                    Partner="objectid_phonecall"/>
+                <NavigationProperty Name="ownerid_phonecall" Type="mscrm.principal" Nullable="false"
+                                    Partner="owner_phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_adx_invitation_phonecall" Type="mscrm.adx_invitation"
+                                    Nullable="false"
+                                    Partner="adx_invitation_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_adplacement_phonecall" Type="mscrm.mspp_adplacement"
+                                    Nullable="false" Partner="mspp_adplacement_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_pollplacement_phonecall"
+                                    Type="mscrm.mspp_pollplacement"
+                                    Nullable="false" Partner="mspp_pollplacement_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_publishingstatetransitionrule_phonecall"
+                                    Type="mscrm.mspp_publishingstatetransitionrule" Nullable="false"
+                                    Partner="mspp_publishingstatetransitionrule_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_redirect_phonecall" Type="mscrm.mspp_redirect"
+                                    Nullable="false"
+                                    Partner="mspp_redirect_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_shortcut_phonecall" Type="mscrm.mspp_shortcut"
+                                    Nullable="false"
+                                    Partner="mspp_shortcut_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_mspp_website_phonecall" Type="mscrm.mspp_website"
+                                    Nullable="false"
+                                    Partner="mspp_website_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_lead_phonecall" Type="mscrm.lead" Nullable="false"
+                                    Partner="Lead_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_bookableresourcebooking_phonecall"
+                                    Type="mscrm.bookableresourcebooking"
+                                    Nullable="false" Partner="bookableresourcebooking_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_bookableresourcebookingheader_phonecall"
+                                    Type="mscrm.bookableresourcebookingheader" Nullable="false"
+                                    Partner="bookableresourcebookingheader_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_bulkoperation_phonecall" Type="mscrm.bulkoperation"
+                                    Nullable="false"
+                                    Partner="BulkOperation_Phonecall"/>
+                <NavigationProperty Name="regardingobjectid_campaign_phonecall" Type="mscrm.campaign" Nullable="false"
+                                    Partner="Campaign_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_campaignactivity_phonecall" Type="mscrm.campaignactivity"
+                                    Nullable="false" Partner="CampaignActivity_Phonecalls"/>
+                <NavigationProperty Name="phonecall_campaignresponse" Type="Collection(mscrm.campaignresponse)"
+                                    Partner="originatingactivityid_phonecall"/>
+                <NavigationProperty Name="regardingobjectid_contract_phonecall" Type="mscrm.contract" Nullable="false"
+                                    Partner="Contract_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_entitlement_phonecall" Type="mscrm.entitlement"
+                                    Nullable="false"
+                                    Partner="entitlement_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_entitlementtemplate_phonecall"
+                                    Type="mscrm.entitlementtemplate"
+                                    Nullable="false" Partner="entitlementtemplate_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_incident_phonecall" Type="mscrm.incident" Nullable="false"
+                                    Partner="Incident_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_site_phonecall" Type="mscrm.site" Nullable="false"
+                                    Partner="site_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_customerasset_phonecall"
+                                    Type="mscrm.msdyn_customerasset"
+                                    Nullable="false" Partner="msdyn_customerasset_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_playbookinstance_phonecall"
+                                    Type="mscrm.msdyn_playbookinstance"
+                                    Nullable="false" Partner="msdyn_playbookinstance_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_invoice_phonecall" Type="mscrm.invoice" Nullable="false"
+                                    Partner="Invoice_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_opportunity_phonecall" Type="mscrm.opportunity"
+                                    Nullable="false"
+                                    Partner="Opportunity_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_quote_phonecall" Type="mscrm.quote" Nullable="false"
+                                    Partner="Quote_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_salesorder_phonecall" Type="mscrm.salesorder"
+                                    Nullable="false"
+                                    Partner="SalesOrder_Phonecalls"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_postalbum_phonecall" Type="mscrm.msdyn_postalbum"
+                                    Nullable="false"
+                                    Partner="msdyn_postalbum_PhoneCalls"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_swarm_phonecall" Type="mscrm.msdyn_swarm"
+                                    Nullable="false"
+                                    Partner="msdyn_swarm_PhoneCalls"/>
+                <NavigationProperty Name="msdyn_phonecall_msdyn_transcript_transcripttarget"
+                                    Type="Collection(mscrm.msdyn_transcript)"
+                                    Partner="msdyn_transcripttarget_phonecall"/>
+                <NavigationProperty Name="msdyn_phonecall_msdyn_ocrecording_recordingtarget"
+                                    Type="Collection(mscrm.msdyn_ocrecording)"
+                                    Partner="msdyn_recordingtarget_phonecall"/>
+                <NavigationProperty Name="msdyn_phonecall_msdyn_conversationinsight_conversationinsighttarget"
+                                    Type="Collection(mscrm.msdyn_conversationinsight)"
+                                    Partner="msdyn_conversationinsighttarget_phonecall"/>
+                <NavigationProperty Name="msdyn_msdyn_conversationactionitem_phonecall_msdyn_createdactivity"
+                                    Type="Collection(mscrm.msdyn_conversationactionitem)"
+                                    Partner="msdyn_createdactivity_phonecall"/>
+                <NavigationProperty Name="msdyn_msdyn_sciconversation_phonecall_msdyn_relatedactivity"
+                                    Type="Collection(mscrm.msdyn_sciconversation)"
+                                    Partner="msdyn_relatedactivity_phonecall"/>
+                <NavigationProperty Name="msdyn_phonecall_msdyn_recording_phone_call_activity"
+                                    Type="Collection(mscrm.msdyn_recording)" Partner="msdyn_phone_call_activity"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_salessuggestion_phonecall"
+                                    Type="mscrm.msdyn_salessuggestion"
+                                    Nullable="false" Partner="msdyn_salessuggestion_PhoneCalls"/>
+                <NavigationProperty Name="msdyn_msdyn_salescopilotinsight_phonecall_msdyn_activityid"
+                                    Type="Collection(mscrm.msdyn_salescopilotinsight)"
+                                    Partner="msdyn_activityid_phonecall"/>
+                <NavigationProperty Name="serviceid_phonecall" Type="mscrm.service" Nullable="false"
+                                    Partner="service_phonecalls"/>
+            </EntityType>
+            <EntityType Name="activitypointer" BaseType="mscrm.crmbaseentity" Abstract="true">
+                <Key>
+                    <PropertyRef Name="activityid"/>
+                </Key>
+                <Property Name="_transactioncurrencyid_value" Type="Edm.Guid"/>
+                <Property Name="instancetypecode" Type="Edm.Int32"/>
+                <Property Name="_modifiedby_value" Type="Edm.Guid"/>
+                <Property Name="_createdby_value" Type="Edm.Guid"/>
+                <Property Name="onholdtime" Type="Edm.Int32"/>
+                <Property Name="description" Type="Edm.String" Unicode="false"/>
+                <Property Name="seriesid" Type="Edm.Guid"/>
+                <Property Name="_ownerid_value" Type="Edm.Guid"/>
+                <Property Name="lastonholdtime" Type="Edm.DateTimeOffset"/>
+                <Property Name="_owningteam_value" Type="Edm.Guid"/>
+                <Property Name="activitytypecode" Type="Edm.String" Unicode="false"/>
+                <Property Name="deliverylastattemptedon" Type="Edm.DateTimeOffset"/>
+                <Property Name="exchangerate" Type="Edm.Decimal" Scale="Variable"/>
+                <Property Name="isbilled" Type="Edm.Boolean"/>
+                <Property Name="leftvoicemail" Type="Edm.Boolean"/>
+                <Property Name="postponeactivityprocessinguntil" Type="Edm.DateTimeOffset"/>
+                <Property Name="_owningbusinessunit_value" Type="Edm.Guid"/>
+                <Property Name="traversedpath" Type="Edm.String" Unicode="false"/>
+                <Property Name="scheduledstart" Type="Edm.DateTimeOffset"/>
+                <Property Name="descriptionblobid" Type="Edm.Binary"/>
+                <Property Name="activityadditionalparams" Type="Edm.String" Unicode="false"/>
+                <Property Name="subject" Type="Edm.String" Unicode="false"/>
+                <Property Name="scheduleddurationminutes" Type="Edm.Int32"/>
+                <Property Name="_serviceid_value" Type="Edm.Guid"/>
+                <Property Name="versionnumber" Type="Edm.Int64"/>
+                <Property Name="community" Type="Edm.Int32"/>
+                <Property Name="_createdonbehalfby_value" Type="Edm.Guid"/>
+                <Property Name="_slainvokedid_value" Type="Edm.Guid"/>
+                <Property Name="_regardingobjectid_value" Type="Edm.Guid"/>
+                <Property Name="actualdurationminutes" Type="Edm.Int32"/>
+                <Property Name="exchangeweblink" Type="Edm.String" Unicode="false"/>
+                <Property Name="senton" Type="Edm.DateTimeOffset"/>
+                <Property Name="isworkflowcreated" Type="Edm.Boolean"/>
+                <Property Name="scheduledend" Type="Edm.DateTimeOffset"/>
+                <Property Name="isregularactivity" Type="Edm.Boolean"/>
+                <Property Name="_slaid_value" Type="Edm.Guid"/>
+                <Property Name="_owninguser_value" Type="Edm.Guid"/>
+                <Property Name="ismapiprivate" Type="Edm.Boolean"/>
+                <Property Name="activityid" Type="Edm.Guid"/>
+                <Property Name="formattedscheduledend" Type="Edm.DateTimeOffset"/>
+                <Property Name="exchangeitemid" Type="Edm.String" Unicode="false"/>
+                <Property Name="processid" Type="Edm.Guid"/>
+                <Property Name="_sendermailboxid_value" Type="Edm.Guid"/>
+                <Property Name="stageid" Type="Edm.Guid"/>
+                <Property Name="modifiedon" Type="Edm.DateTimeOffset"/>
+                <Property Name="createdon" Type="Edm.DateTimeOffset"/>
+                <Property Name="formattedscheduledstart" Type="Edm.DateTimeOffset"/>
+                <Property Name="statuscode" Type="Edm.Int32"/>
+                <Property Name="statecode" Type="Edm.Int32"/>
+                <Property Name="actualend" Type="Edm.DateTimeOffset"/>
+                <Property Name="_modifiedonbehalfby_value" Type="Edm.Guid"/>
+                <Property Name="actualstart" Type="Edm.DateTimeOffset"/>
+                <Property Name="prioritycode" Type="Edm.Int32"/>
+                <Property Name="descriptionblobid_name" Type="Edm.String" Unicode="false"/>
+                <Property Name="deliveryprioritycode" Type="Edm.Int32"/>
+                <Property Name="sortdate" Type="Edm.DateTimeOffset"/>
+                <Property Name="timezoneruleversionnumber" Type="Edm.Int32"/>
+                <Property Name="utcconversiontimezonecode" Type="Edm.Int32"/>
+                <NavigationProperty Name="regardingobjectid_new_interactionforemail" Type="mscrm.interactionforemail"
+                                    Nullable="false"
+                                    Partner="new_interactionforemail_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="interactionforemailid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_knowledgebaserecord" Type="mscrm.knowledgebaserecord"
+                                    Nullable="false"
+                                    Partner="KnowledgeBaseRecord_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="knowledgebaserecordid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_account" Type="mscrm.account" Nullable="false"
+                                    Partner="Account_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="accountid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="createdby" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_activitypointer_createdby">
+                    <ReferentialConstraint Property="_createdby_value" ReferencedProperty="systemuserid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_contact" Type="mscrm.contact" Nullable="false"
+                                    Partner="Contact_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="contactid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_socialactivity" Type="Collection(mscrm.socialactivity)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_recurringappointmentmaster"
+                                    Type="Collection(mscrm.recurringappointmentmaster)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_email" Type="Collection(mscrm.email)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="sendermailboxid" Type="mscrm.mailbox" Nullable="false"
+                                    Partner="activitypointer_sendermailboxid_mailbox">
+                    <ReferentialConstraint Property="_sendermailboxid_value" ReferencedProperty="mailboxid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="transactioncurrencyid" Type="mscrm.transactioncurrency" Nullable="false"
+                                    Partner="TransactionCurrency_ActivityPointer">
+                    <ReferentialConstraint Property="_transactioncurrencyid_value"
+                                           ReferencedProperty="transactioncurrencyid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="ActivityPointer_QueueItem" Type="Collection(mscrm.queueitem)"
+                                    Partner="objectid_activitypointer"/>
+                <NavigationProperty Name="ownerid" Type="mscrm.principal" Nullable="false"
+                                    Partner="owner_activitypointers">
+                    <ReferentialConstraint Property="_ownerid_value" ReferencedProperty="ownerid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="owninguser" Type="mscrm.systemuser" Nullable="false" Partner="user_activity">
+                    <ReferentialConstraint Property="_owninguser_value" ReferencedProperty="systemuserid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="sla_activitypointer_sla" Type="mscrm.sla" Nullable="false"
+                                    Partner="manualsla_activitypointer">
+                    <ReferentialConstraint Property="_slaid_value" ReferencedProperty="slaid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="owningbusinessunit" Type="mscrm.businessunit" Nullable="false"
+                                    Partner="business_unit_activitypointer">
+                    <ReferentialConstraint Property="_owningbusinessunit_value" ReferencedProperty="businessunitid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_knowledgearticle" Type="mscrm.knowledgearticle"
+                                    Nullable="false"
+                                    Partner="KnowledgeArticle_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="knowledgearticleid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="modifiedonbehalfby" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_activitypointer_modifiedonbehalfby">
+                    <ReferentialConstraint Property="_modifiedonbehalfby_value" ReferencedProperty="systemuserid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_activity_mime_attachment"
+                                    Type="Collection(mscrm.activitymimeattachment)"
+                                    Partner="objectid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_fax" Type="Collection(mscrm.fax)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="createdonbehalfby" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_activitypointer_createdonbehalfby">
+                    <ReferentialConstraint Property="_createdonbehalfby_value" ReferencedProperty="systemuserid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="modifiedby" Type="mscrm.systemuser" Nullable="false"
+                                    Partner="lk_activitypointer_modifiedby">
+                    <ReferentialConstraint Property="_modifiedby_value" ReferencedProperty="systemuserid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_task" Type="Collection(mscrm.task)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_phonecall" Type="Collection(mscrm.phonecall)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_appointment" Type="Collection(mscrm.appointment)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_letter" Type="Collection(mscrm.letter)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activitypointer_connections2" Type="Collection(mscrm.connection)"
+                                    Partner="record2id_activitypointer"/>
+                <NavigationProperty Name="slakpiinstance_activitypointer" Type="Collection(mscrm.slakpiinstance)"
+                                    Partner="regarding_activitypointer"/>
+                <NavigationProperty Name="owningteam" Type="mscrm.team" Nullable="false" Partner="team_activity">
+                    <ReferentialConstraint Property="_owningteam_value" ReferencedProperty="teamid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="ActivityPointer_BulkDeleteFailures" Type="Collection(mscrm.bulkdeletefailure)"
+                                    Partner="regardingobjectid_activitypointer"/>
+                <NavigationProperty Name="slainvokedid_activitypointer_sla" Type="mscrm.sla" Nullable="false"
+                                    Partner="sla_activitypointer">
+                    <ReferentialConstraint Property="_slainvokedid_value" ReferencedProperty="slaid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activitypointer_connections1" Type="Collection(mscrm.connection)"
+                                    Partner="record1id_activitypointer"/>
+                <NavigationProperty Name="ActivityPointer_AsyncOperations" Type="Collection(mscrm.asyncoperation)"
+                                    Partner="regardingobjectid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_recurrencerule" Type="Collection(mscrm.recurrencerule)"
+                                    Partner="objectid"/>
+                <NavigationProperty Name="activitypointer_activity_parties" Type="Collection(mscrm.activityparty)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_chat" Type="Collection(mscrm.chat)"
+                                    Partner="activityid_chat"/>
+                <NavigationProperty Name="activitypointer_FileAttachments" Type="Collection(mscrm.fileattachment)"
+                                    Partner="objectid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_adx_invitation" Type="mscrm.adx_invitation" Nullable="false"
+                                    Partner="adx_invitation_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="adx_invitationid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_adx_inviteredemption"
+                                    Type="Collection(mscrm.adx_inviteredemption)"
+                                    Partner="activityid_adx_inviteredemption"/>
+                <NavigationProperty Name="activity_pointer_adx_portalcomment" Type="Collection(mscrm.adx_portalcomment)"
+                                    Partner="activityid_adx_portalcomment"/>
+                <NavigationProperty Name="regardingobjectid_mspp_adplacement" Type="mscrm.mspp_adplacement"
+                                    Nullable="false"
+                                    Partner="mspp_adplacement_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="mspp_adplacementid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_mspp_pollplacement" Type="mscrm.mspp_pollplacement"
+                                    Nullable="false"
+                                    Partner="mspp_pollplacement_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="mspp_pollplacementid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_mspp_publishingstatetransitionrule"
+                                    Type="mscrm.mspp_publishingstatetransitionrule" Nullable="false"
+                                    Partner="mspp_publishingstatetransitionrule_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="mspp_publishingstatetransitionruleid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_mspp_redirect" Type="mscrm.mspp_redirect" Nullable="false"
+                                    Partner="mspp_redirect_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="mspp_redirectid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_mspp_shortcut" Type="mscrm.mspp_shortcut" Nullable="false"
+                                    Partner="mspp_shortcut_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="mspp_shortcutid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_mspp_website" Type="mscrm.mspp_website" Nullable="false"
+                                    Partner="mspp_website_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="mspp_websiteid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_lead" Type="mscrm.lead" Nullable="false"
+                                    Partner="Lead_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="leadid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_bookableresourcebooking"
+                                    Type="mscrm.bookableresourcebooking"
+                                    Nullable="false" Partner="bookableresourcebooking_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="bookableresourcebookingid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_bookableresourcebookingheader"
+                                    Type="mscrm.bookableresourcebookingheader"
+                                    Nullable="false" Partner="bookableresourcebookingheader_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="bookableresourcebookingheaderid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_BulkOperation" Type="Collection(mscrm.bulkoperation)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_campaign" Type="mscrm.campaign" Nullable="false"
+                                    Partner="Campaign_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="campaignid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_campaignactivity" Type="Collection(mscrm.campaignactivity)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_campaignresponse" Type="Collection(mscrm.campaignresponse)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_BulkOperation_logs" Type="Collection(mscrm.bulkoperationlog)"
+                                    Partner="bulkoperationid_activitypointer"/>
+                <NavigationProperty Name="CreatedActivity_BulkOperationLogs" Type="Collection(mscrm.bulkoperationlog)"
+                                    Partner="createdobjectid_activitypointer"/>
+                <NavigationProperty Name="ActivityPointer_CampaignActivityItems"
+                                    Type="Collection(mscrm.campaignactivityitem)"
+                                    Partner="campaignactivityid"/>
+                <NavigationProperty Name="activity_campaignresponse" Type="Collection(mscrm.campaignresponse)"
+                                    Partner="originatingactivityid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_campaignactivity" Type="mscrm.campaignactivity"
+                                    Nullable="false"
+                                    Partner="CampaignActivity_ActivityPointers"/>
+                <NavigationProperty Name="regardingobjectid_bulkoperation" Type="mscrm.bulkoperation" Nullable="false"
+                                    Partner="BulkOperation_ActivityPointers"/>
+                <NavigationProperty Name="regardingobjectid_entitlement" Type="mscrm.entitlement" Nullable="false"
+                                    Partner="entitlement_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="entitlementid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_entitlementtemplate" Type="mscrm.entitlementtemplate"
+                                    Nullable="false"
+                                    Partner="entitlementtemplate_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="entitlementtemplateid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_site" Type="mscrm.site" Nullable="false"
+                                    Partner="site_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="siteid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_msdyn_customerasset" Type="mscrm.msdyn_customerasset"
+                                    Nullable="false"
+                                    Partner="msdyn_customerasset_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="msdyn_customerassetid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_msdyn_playbookinstance" Type="mscrm.msdyn_playbookinstance"
+                                    Nullable="false"
+                                    Partner="msdyn_playbookinstance_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="msdyn_playbookinstanceid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_invoice" Type="mscrm.invoice" Nullable="false"
+                                    Partner="Invoice_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="invoiceid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_opportunity" Type="mscrm.opportunity" Nullable="false"
+                                    Partner="Opportunity_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="opportunityid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_opportunity_close" Type="Collection(mscrm.opportunityclose)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_order_close" Type="Collection(mscrm.orderclose)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="activity_pointer_quote_close" Type="Collection(mscrm.quoteclose)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_quote" Type="mscrm.quote" Nullable="false"
+                                    Partner="Quote_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="quoteid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="regardingobjectid_salesorder" Type="mscrm.salesorder" Nullable="false"
+                                    Partner="SalesOrder_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="salesorderid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="msdyn_readtracker_poly_activitypointer"
+                                    Type="Collection(mscrm.msdyn_readtracker)"
+                                    Partner="msdyn_RegardingObjectId"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_postalbum" Type="mscrm.msdyn_postalbum"
+                                    Nullable="false"
+                                    Partner="msdyn_postalbum_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="msdyn_postalbumid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_msfp_alert" Type="Collection(mscrm.msfp_alert)"
+                                    Partner="activityid_msfp_alert"/>
+                <NavigationProperty Name="activity_pointer_msfp_surveyinvite" Type="Collection(mscrm.msfp_surveyinvite)"
+                                    Partner="activityid_msfp_surveyinvite"/>
+                <NavigationProperty Name="activity_pointer_msfp_surveyresponse"
+                                    Type="Collection(mscrm.msfp_surveyresponse)"
+                                    Partner="activityid_msfp_surveyresponse"/>
+                <NavigationProperty Name="activity_pointer_msdyn_copilottranscript"
+                                    Type="Collection(mscrm.msdyn_copilottranscript)"
+                                    Partner="activityid_msdyn_copilottranscript"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_swarm" Type="mscrm.msdyn_swarm" Nullable="false"
+                                    Partner="msdyn_swarm_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="msdyn_swarmid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_msdyn_ocliveworkitem"
+                                    Type="Collection(mscrm.msdyn_ocliveworkitem)"
+                                    Partner="activityid_msdyn_ocliveworkitem"/>
+                <NavigationProperty Name="activity_pointer_msdyn_ocsession" Type="Collection(mscrm.msdyn_ocsession)"
+                                    Partner="activityid_msdyn_ocsession"/>
+                <NavigationProperty Name="regardingobjectid_msdyn_salessuggestion" Type="mscrm.msdyn_salessuggestion"
+                                    Nullable="false"
+                                    Partner="msdyn_salessuggestion_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value"
+                                           ReferencedProperty="msdyn_salessuggestionid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_incident_resolution"
+                                    Type="Collection(mscrm.incidentresolution)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_incident" Type="mscrm.incident" Nullable="false"
+                                    Partner="Incident_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="incidentid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="serviceid" Type="mscrm.service" Nullable="false"
+                                    Partner="service_activity_pointers">
+                    <ReferentialConstraint Property="_serviceid_value" ReferencedProperty="serviceid"/>
+                </NavigationProperty>
+                <NavigationProperty Name="activity_pointer_service_appointment"
+                                    Type="Collection(mscrm.serviceappointment)"
+                                    Partner="activityid_activitypointer"/>
+                <NavigationProperty Name="regardingobjectid_contract" Type="mscrm.contract" Nullable="false"
+                                    Partner="Contract_ActivityPointers">
+                    <ReferentialConstraint Property="_regardingobjectid_value" ReferencedProperty="contractid"/>
+                </NavigationProperty>
+            </EntityType>
+        </Schema>
+    </edmx:DataServices>
+</edmx:Edmx>
\ No newline at end of file
diff --git a/providers/catalog_test.go b/providers/catalog_test.go
index afb16542..abd695dd 100644
--- a/providers/catalog_test.go
+++ b/providers/catalog_test.go
@@ -305,6 +305,37 @@ var testCases = []struct { // nolint
 		expectedErr: nil,
 	},
 
+	{
+		provider:    Mural,
+		description: "Valid Mural provider config with no substitutions",
+		expected: &ProviderInfo{
+			Support: Support{
+				Read:  false,
+				Write: false,
+				BulkWrite: BulkWriteSupport{
+					Insert: false,
+					Update: false,
+					Upsert: false,
+					Delete: false,
+				},
+				Subscribe: false,
+				Proxy:     false,
+			},
+			AuthType: Oauth2,
+			OauthOpts: OauthOpts{
+				AuthURL:                   "https://api.mural.co/oauth/authorize",
+				TokenURL:                  "https://api.mural.co/oauth/token",
+				ExplicitScopesRequired:    true,
+				ExplicitWorkspaceRequired: false,
+				TokenMetadataFields: TokenMetadataFields{
+					ScopesField: "scope",
+				},
+			},
+			BaseURL: "https://api.mural.co/api",
+		},
+		expectedErr: nil,
+	},
+
 	{
 		provider:    Klaviyo,
 		description: "Valid Klaviyo provider config with no substitutions",
diff --git a/test/microsoftdynamicscrm/metadata/README.md b/test/microsoftdynamicscrm/metadata/README.md
new file mode 100644
index 00000000..4a59ac2a
--- /dev/null
+++ b/test/microsoftdynamicscrm/metadata/README.md
@@ -0,0 +1,14 @@
+# Script Description
+
+Listing object metadata for MS Dynamics 365 CRM will give huge XML document describing schema for entire service.
+A portion is propagated based on the list of desired Objects. This script is concerned about "account" structure.
+In order to validate that metadata is extracted as expected we compare against the response from connector's READ operation.
+
+The test ensures the following rules:
+* Every field returned by `GET ~/accounts` must be in schema description (except annotations)
+* No schema property is omitted
+
+If you would like to validate for other objects, here where you can find more information.
+Under this repo `sortedStructNamesFromSchema.txt` file lists all objects that are available.
+To find out which endpoint can serve these data PROXY call `~/api/data/v9.2/{yourObjectName_plural}`.
+> NOTE: making a GET request to the list resource should have the name pluralised.
diff --git a/test/microsoftdynamicscrm/metadata/main.go b/test/microsoftdynamicscrm/metadata/main.go
new file mode 100644
index 00000000..46f321a5
--- /dev/null
+++ b/test/microsoftdynamicscrm/metadata/main.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"os/signal"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/amp-labs/connectors/common"
+	msTest "github.com/amp-labs/connectors/test/microsoftdynamicscrm"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+var (
+	objectName       = "account"
+	objectNamePlural = "accounts"
+)
+
+// we want to compare fields returned by read and schema properties provided by metadata methods
+// they must match for all such objects
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	filePath := os.Getenv("MS_CRM_CRED_FILE")
+	if filePath == "" {
+		filePath = "./ms-crm-creds.json"
+	}
+
+	conn := msTest.GetMSDynamics365CRMConnector(ctx, filePath)
+	defer utils.Close(conn)
+
+	response, err := conn.Read(ctx, common.ReadParams{
+		ObjectName: objectNamePlural,
+	})
+	if err != nil {
+		utils.Fail("error reading from microsoft CRM", "error", err)
+	}
+
+	if response.Rows == 0 {
+		utils.Fail("expected to read at least one record", "error", err)
+	}
+
+	beforeCall := time.Now()
+
+	metadata, err := conn.ListObjectMetadata(ctx, []string{
+		objectName,
+	})
+	if err != nil {
+		utils.Fail("error listing metadata for microsoft CRM", "error", err)
+	}
+
+	fmt.Printf("ListObjectMetadata took %.2f seconds.\n", time.Since(beforeCall).Seconds())
+
+	fmt.Println("Compare object metadata against endpoint response:")
+
+	mismatchErr := compareFieldsMatch(metadata, response.Data[0].Raw, objectName)
+	if mismatchErr != nil {
+		utils.Fail("schema and payload response have mismatching fields", "error", mismatchErr)
+	} else {
+		fmt.Println("==> success fields match.")
+	}
+}
+
+func compareFieldsMatch(metadata *common.ListObjectMetadataResult, response map[string]any, objectName string) error {
+	fields := make(map[string]bool)
+
+	for field := range response {
+		// ignore all fields that are OData annotations
+		if !strings.Contains(field, "@") {
+			fields[field] = false
+		}
+	}
+
+	mismatch := make([]error, 0)
+
+	for _, displayName := range metadata.Result[objectName].FieldsMap {
+		if _, found := fields[displayName]; found {
+			fields[displayName] = true
+		} else {
+			mismatch = append(mismatch, fmt.Errorf("read payload doesn't have %v", displayName))
+		}
+	}
+
+	for name, found := range fields {
+		if !found {
+			mismatch = append(mismatch, fmt.Errorf("metadata schema is missing field %v", name))
+		}
+	}
+
+	return errors.Join(mismatch...)
+}
diff --git a/test/microsoftdynamicscrm/metadata/sortedStructNamesFromSchema.txt b/test/microsoftdynamicscrm/metadata/sortedStructNamesFromSchema.txt
new file mode 100644
index 00000000..4dbdb93e
--- /dev/null
+++ b/test/microsoftdynamicscrm/metadata/sortedStructNamesFromSchema.txt
@@ -0,0 +1,1167 @@
+AttributeMetadata
+BigIntAttributeMetadata
+BooleanAttributeMetadata
+BooleanOptionSetMetadata
+DateTimeAttributeMetadata
+DecimalAttributeMetadata
+DoubleAttributeMetadata
+EntityKeyMetadata
+EntityMetadata
+EntityNameAttributeMetadata
+EnumAttributeMetadata
+FileAttributeMetadata
+ImageAttributeMetadata
+IntegerAttributeMetadata
+LookupAttributeMetadata
+ManagedPropertyAttributeMetadata
+ManagedPropertyMetadata
+ManyToManyRelationshipMetadata
+MemoAttributeMetadata
+MetadataBase
+MoneyAttributeMetadata
+MultiSelectPicklistAttributeMetadata
+OneToManyRelationshipMetadata
+OptionSetMetadata
+OptionSetMetadataBase
+PicklistAttributeMetadata
+RelationshipMetadataBase
+StringAttributeMetadata
+aaduser
+account
+accountleads
+aciviewmapper
+actioncard
+actioncarduserstate
+activityfileattachment
+activitymimeattachment
+activitymonitor
+activityparty
+activitypointer
+adminsettingsentity
+adx_externalidentity
+adx_invitation
+adx_invitation_invitecontacts
+adx_invitation_mspp_webrole_powerpagecomponent
+adx_invitation_redeemedcontacts
+adx_inviteredemption
+adx_kbarticle_kbarticle
+adx_portalcomment
+adx_setting
+adx_webformsession
+aiplugin
+aipluginexternalschema
+aipluginexternalschemaproperty
+aiplugininstance
+aipluginoperation
+aipluginoperationparameter
+aipluginusersetting
+annotation
+annualfiscalcalendar
+appaction
+appaction_appactionrule_classicrules
+appactionmigration
+appactionrule
+appactionrule_webresource_scripts
+appconfig
+appconfiginstance
+appconfigmaster
+applicationuser
+applicationuserprofile
+applicationuserrole
+appmodule
+appmodulecomponent
+appmoduleroles
+appnotification
+appointment
+asyncoperation
+attachment
+attribute
+attributeimageconfig
+attributemaskingrule
+audit
+availabletimes
+availabletimesdatasource
+backgroundoperation
+bookableresource
+bookableresourcebooking
+bookableresourcebookingheader
+bookableresourcecategory
+bookableresourcecategoryassn
+bookableresourcecharacteristic
+bookableresourcegroup
+bookingstatus
+bot
+bot_botcomponent
+bot_botcontentpack
+bot_environmentvariabledefinition
+botcomponent
+botcomponent_aipluginoperation
+botcomponent_botcomponent
+botcomponent_connectionreference
+botcomponent_environmentvariabledefinition
+botcomponent_workflow
+botcontentpack
+bulkdeletefailure
+bulkdeleteoperation
+bulkoperation
+bulkoperationlog
+businessunit
+businessunitnewsarticle
+calendar
+calendarrule
+callbackregistration
+campaign
+campaignactivity
+campaignactivityitem
+campaignitem
+campaignresponse
+canvasapp
+card
+cardentityconnections
+cardstateitem
+catalog
+catalogassignment
+category
+channelaccessprofileentityaccesslevel
+characteristic
+chat
+columnmapping
+competitor
+competitoraddress
+competitorproduct
+competitorsalesliterature
+complexcontrol
+componentversion
+componentversiondatasource
+componentversionnrddatasource
+connection
+connectioninstance
+connectionreference
+connectionrole
+connectionroleassociation
+connector
+constraintbasedgroup
+contact
+contactinvoices
+contactleads
+contactorders
+contactquotes
+contract
+contractdetail
+contracttemplate
+conversationtranscript
+copilotexamplequestion
+credential
+crmbaseentity
+crmmodelbaseentity
+customapi
+customapirequestparameter
+customapiresponseproperty
+customcontrol
+customcontroldefaultconfig
+customcontrolresource
+customeraddress
+datalakefolder
+datalakefolderpermission
+datalakeworkspace
+datalakeworkspacepermission
+dataperformance
+dataprocessingconfiguration
+delegatedauthorization
+dependency
+desktopflowbinary
+desktopflowmodule
+discount
+discounttype
+displaystring
+documenttemplate
+duplicaterecord
+duplicaterule
+duplicaterulecondition
+dvfilesearch
+dvfilesearchattribute
+dvfilesearchentity
+dvtablesearch
+dvtablesearchattribute
+dvtablesearchentity
+dynamicproperty
+dynamicpropertyassociation
+dynamicpropertyinstance
+dynamicpropertyoptionsetitem
+elasticfileattachment
+email
+emailserverprofile
+entitlement
+entitlementchannel
+entitlementcontacts
+entitlemententityallocationtypemapping
+entitlementproducts
+entitlementtemplate
+entitlementtemplatechannel
+entitlementtemplateproducts
+entity
+entityanalyticsconfig
+entitydataprovider
+entityimageconfig
+entityindex
+entitykey
+entityrecordfilter
+entityrelationship
+environmentvariabledefinition
+environmentvariablevalue
+equipment
+eventexpanderbreadcrumb
+exchangesyncidmapping
+expiredprocess
+exportedexcel
+exportsolutionupload
+fax
+featurecontrolsetting
+feedback
+fieldpermission
+fieldsecurityprofile
+fileattachment
+fixedmonthlyfiscalcalendar
+flowcapacityassignment
+flowevent
+flowmachine
+flowmachinegroup
+flowmachineimage
+flowmachineimageversion
+flowmachinenetwork
+flowrun
+flowsession
+fxexpression
+goal
+goalrollupquery
+import
+importdata
+importentitymapping
+importfile
+importjob
+importlog
+importmap
+incident
+incidentknowledgebaserecord
+incidentresolution
+indexattributes
+interactionforemail
+invaliddependency
+invoice
+invoicedetail
+kbarticle
+kbarticlecomment
+kbarticletemplate
+keyvaultreference
+knowledgearticle
+knowledgearticleincident
+knowledgearticlescategories
+knowledgearticleviews
+knowledgebaserecord
+languagelocale
+languageprovisioningstate
+lead
+leadaddress
+leadcompetitors
+leadproduct
+leadtoopportunitysalesprocess
+letter
+list
+listmember
+listoperation
+lookupmapping
+mailbox
+mailboxtrackingfolder
+mailmergetemplate
+mainfewshot
+makerfewshot
+managedidentity
+managedproperty
+marketingformdisplayattributes
+maskingrule
+metadataforarchival
+metric
+mobileofflineprofile
+mobileofflineprofileextension
+mobileofflineprofileitem
+mobileofflineprofileitemassociation
+mobileofflineprofileitemfilter
+monthlyfiscalcalendar
+msdyn_accountkpiitem
+msdyn_actioncardactionstat
+msdyn_actioncardregarding
+msdyn_actioncardrolesetting
+msdyn_actioncardstataggregation
+msdyn_activeicdextension
+msdyn_activityanalysiscleanupstate
+msdyn_activityanalysisconfig
+msdyn_adaptivecardconfiguration
+msdyn_adjustmenthistory
+msdyn_adminappstate
+msdyn_agentcapacityprofileunit
+msdyn_agentcapacityupdatehistory
+msdyn_agentchannelstate
+msdyn_agentcopilotsetting
+msdyn_agentresourceforecasting
+msdyn_agentstatus
+msdyn_agentstatushistory
+msdyn_aibdataset
+msdyn_aibdatasetfile
+msdyn_aibdatasetrecord
+msdyn_aibdatasetscontainer
+msdyn_aibfeedbackloop
+msdyn_aibfile
+msdyn_aibfileattacheddata
+msdyn_aiconfiguration
+msdyn_aicontactsuggestion
+msdyn_aievent
+msdyn_aifptrainingdocument
+msdyn_aimodel
+msdyn_aiodimage
+msdyn_aiodlabel
+msdyn_aiodlabel_msdyn_aiconfiguration
+msdyn_aiodtrainingboundingbox
+msdyn_aiodtrainingimage
+msdyn_aitemplate
+msdyn_analysiscomponent
+msdyn_analysisjob
+msdyn_analysisoverride
+msdyn_analysisresult
+msdyn_analysisresultdetail
+msdyn_analytics
+msdyn_analyticsadminsettings
+msdyn_analyticsforcs
+msdyn_appconfig_msdyn_channelprovider
+msdyn_appconfiguration
+msdyn_appconfiguration_applicationextension
+msdyn_appconfiguration_sessiontemplate
+msdyn_appconfiguration_systemuser
+msdyn_appcopilotconfiguration
+msdyn_appinsightsmetadata
+msdyn_applicationextension
+msdyn_applicationtabtemplate
+msdyn_appprofilerolemapping
+msdyn_appstate
+msdyn_assetcategorytemplateassociation
+msdyn_assettemplateassociation
+msdyn_assignmentconfiguration
+msdyn_assignmentconfigurationstep
+msdyn_assignmentmap
+msdyn_assignmentrule
+msdyn_attribute
+msdyn_attributeinfluencestatistics
+msdyn_attributevalue
+msdyn_authenticationsettings
+msdyn_authsettingsentry
+msdyn_autocapturerule
+msdyn_autocapturesettings
+msdyn_bgjobledger
+msdyn_bookableresourcecapacityprofile
+msdyn_botsession
+msdyn_callablecontext
+msdyn_callablecontext_msdyn_playbooktemplate
+msdyn_cannedmessage
+msdyn_capacityprofile
+msdyn_caseenrichment
+msdyn_casesuggestion
+msdyn_casesuggestionrequestpayload
+msdyn_casesuggestionsdatasource
+msdyn_casetopic
+msdyn_casetopic_incident
+msdyn_casetopicsetting
+msdyn_casetopicsummary
+msdyn_cdsentityengagementctx
+msdyn_channelcapability
+msdyn_channeldefinition
+msdyn_channeldefinitionconsent
+msdyn_channeldefinitionlocale
+msdyn_channelinstance
+msdyn_channelinstanceaccount
+msdyn_channelmessageattachment
+msdyn_channelmessagecontextpart
+msdyn_channelmessagepart
+msdyn_channelprovider
+msdyn_ciprovider
+msdyn_ciprovider_systemuser
+msdyn_collabgraphresource
+msdyn_collabspaceteamassociation
+msdyn_componentlayer
+msdyn_componentlayerdatasource
+msdyn_connectordatasource
+msdyn_consoleapplicationnotificationfield
+msdyn_consoleapplicationnotificationtemplate
+msdyn_consoleapplicationsessiontemplate
+msdyn_consoleapplicationtemplate
+msdyn_consoleapplicationtemplateparameter
+msdyn_consoleapplicationtype
+msdyn_consoleappparameterdefinition
+msdyn_consumingapplication
+msdyn_contactkpiitem
+msdyn_contactsuggestionrule
+msdyn_contactsuggestionruleset
+msdyn_conversationaction
+msdyn_conversationactionitem
+msdyn_conversationactionlocale
+msdyn_conversationaggregatedinsights
+msdyn_conversationcomment
+msdyn_conversationdata
+msdyn_conversationinsight
+msdyn_conversationmessageblock
+msdyn_conversationparticipantinsights
+msdyn_conversationparticipantsentiment
+msdyn_conversationquestion
+msdyn_conversationsegmentsentiment
+msdyn_conversationsentiment
+msdyn_conversationsignal
+msdyn_conversationsubject
+msdyn_conversationsuggestionrequestpayload
+msdyn_conversationsummaryinteraction
+msdyn_conversationsummarysetting
+msdyn_conversationsummarysuggestion
+msdyn_conversationsystemtag
+msdyn_conversationtag
+msdyn_conversationtopic
+msdyn_conversationtopic_conversation
+msdyn_conversationtopicsetting
+msdyn_conversationtopicsummary
+msdyn_copilotagentpreference
+msdyn_copilotinteraction
+msdyn_copilotinteractiondata
+msdyn_copilotsummarizationsetting
+msdyn_copilottranscript
+msdyn_copilottranscriptdata
+msdyn_crmconnection
+msdyn_csadminconfig
+msdyn_cskeyvalueconfig
+msdyn_customapirulesetconfiguration
+msdyn_customcontrolextendedsettings
+msdyn_customerasset
+msdyn_customerassetattachment
+msdyn_customerassetcategory
+msdyn_customeremailcommunication
+msdyn_dailyaccountkpiitem
+msdyn_dailycontactkpiitem
+msdyn_dailyleadkpiitem
+msdyn_dailyopportunitykpiitem
+msdyn_dataanalyticscustomizedreport
+msdyn_dataanalyticsdataset
+msdyn_dataanalyticsreport
+msdyn_dataanalyticsreport_csrmanager
+msdyn_dataanalyticsreport_forecast
+msdyn_dataanalyticsreport_ksinsights
+msdyn_dataanalyticsreport_oc
+msdyn_dataanalyticsreport_oc_rt
+msdyn_dataanalyticsreport_ocvoice
+msdyn_dataanalyticsreport_sareporting
+msdyn_dataanalyticsreport_sutreporting
+msdyn_dataanalyticsworkspace
+msdyn_databaseversion
+msdyn_dataflow
+msdyn_dataflow_datalakefolder
+msdyn_dataflowconnectionreference
+msdyn_dataflowrefreshhistory
+msdyn_dataflowtemplate
+msdyn_datahygienesettinginfo
+msdyn_datainsightsandanalyticsfeature
+msdyn_datalakeds
+msdyn_dealmanageraccess
+msdyn_dealmanagersettings
+msdyn_decisioncontract
+msdyn_decisionruleset
+msdyn_defextendedchannelinstance
+msdyn_defextendedchannelinstanceaccount
+msdyn_deletedconversation
+msdyn_digitalsellingactivetask
+msdyn_digitalsellingcompletedtask
+msdyn_distributedlock
+msdyn_dmsrequest
+msdyn_dmsrequeststatus
+msdyn_dmssyncrequest
+msdyn_dmssyncstatus
+msdyn_duplicatedetectionpluginrun
+msdyn_duplicateleadmapping
+msdyn_effortpredictionresult
+msdyn_entityattachment
+msdyn_entityconfig
+msdyn_entitydeltachange
+msdyn_entitylinkchatconfiguration
+msdyn_entityrankingrule
+msdyn_entityrefreshhistory
+msdyn_entityroutingconfiguration
+msdyn_entityworkstreammap
+msdyn_extendedusersetting
+msdyn_favoriteknowledgearticle
+msdyn_federatedarticle
+msdyn_federatedarticleincident
+msdyn_fileupload
+msdyn_fileuploadstatustracker
+msdyn_flowcardtype
+msdyn_forecast
+msdyn_forecastconfiguration
+msdyn_forecastdefinition
+msdyn_forecastingcache
+msdyn_forecastinsight
+msdyn_forecastinstance
+msdyn_forecastpredictiondata
+msdyn_forecastpredictionstatus
+msdyn_forecastrecurrence
+msdyn_forecastsettingsandsummary
+msdyn_functionallocation
+msdyn_gdprdata
+msdyn_helppage
+msdyn_icdextension
+msdyn_icebreakersconfig
+msdyn_iermlmodel
+msdyn_iermltraining
+msdyn_inboxcardconfiguration
+msdyn_inboxconfiguration
+msdyn_inboxentityconfig
+msdyn_incident_msdyn_customerasset
+msdyn_insightsstorevirtualentity
+msdyn_integratedsearchprovider
+msdyn_intent
+msdyn_iotalert
+msdyn_iotdevice
+msdyn_iotdevicecategory
+msdyn_iotdevicecategorycommands
+msdyn_iotdevicecommand
+msdyn_iotdevicecommanddefinition
+msdyn_iotdevicecommandparameters
+msdyn_iotdevicedatahistory
+msdyn_iotdeviceproperty
+msdyn_iotdeviceregistrationhistory
+msdyn_iotdevicevisualizationconfiguration
+msdyn_iotfieldmapping
+msdyn_iotpropertydefinition
+msdyn_iotprovider
+msdyn_iotproviderinstance
+msdyn_iotsettings
+msdyn_iottocaseprocess
+msdyn_jobsstate
+msdyn_kalanguagesetting
+msdyn_kbattachment
+msdyn_kbenrichment
+msdyn_kbkeywordsdescsuggestionsetting
+msdyn_kmfederatedsearchconfig
+msdyn_kmpersonalizationsetting
+msdyn_knowledgearticleimage
+msdyn_knowledgearticlesuggestion
+msdyn_knowledgearticlesuggestiondatasource
+msdyn_knowledgearticletemplate
+msdyn_knowledgeconfiguration
+msdyn_knowledgeinteractioninsight
+msdyn_knowledgemanagementsetting
+msdyn_knowledgepersonalfilter
+msdyn_knowledgesearchfilter
+msdyn_knowledgesearchinsight
+msdyn_kpieventdata
+msdyn_kpieventdefinition
+msdyn_leadhygienesetting
+msdyn_leadkpiitem
+msdyn_leadmodelconfig
+msdyn_linkedentityattributevalidity
+msdyn_liveconversation
+msdyn_liveworkitemevent
+msdyn_liveworkstream
+msdyn_liveworkstreamcapacityprofile
+msdyn_macrosession
+msdyn_maskingrule
+msdyn_masterentityroutingconfiguration
+msdyn_migrationtracker
+msdyn_mobileapp
+msdyn_modelpreviewstatus
+msdyn_modulerundetail
+msdyn_mostcontacted
+msdyn_mostcontactedby
+msdyn_msdyn_attributevalue_systemuser
+msdyn_msdyn_cannedmessage_liveworkstream
+msdyn_msdyn_cannedmessage_msdyn_octag
+msdyn_msdyn_consoleapplicationnotificationtag
+msdyn_msdyn_consoleapplicationnotificationtem
+msdyn_msdyn_consoleapplicationsessiontemp_tag
+msdyn_msdyn_consoleapplicationsessiontemplate
+msdyn_msdyn_consoleapplicationtemplate_msdyn_
+msdyn_msdyn_consoleapplicationtemplate_tags
+msdyn_msdyn_consoleapplicationtype_msdyn_cons
+msdyn_msdyn_customerasset_knowledgearticl
+msdyn_msdyn_customerasset_knowledgebasere
+msdyn_msdyn_functionallocation_account
+msdyn_msdyn_kbattachment_knowledgearticle
+msdyn_msdyn_liveworkstream_systemuser
+msdyn_msdyn_ocliveworkitem_knowledgeartic
+msdyn_msdyn_ocliveworkitem_knowledgebaser
+msdyn_msdyn_ocrichobject_liveworkstream
+msdyn_msdyn_ocrichobject_msdyn_octag
+msdyn_msdyn_paneconfig_msdyn_appconfig
+msdyn_msdyn_personalmessage_msdyn_octag
+msdyn_msdyn_prod_actioninputparameter_msdyn_par
+msdyn_msdyn_prod_actionoutputparameter_msdyn_pa
+msdyn_msdyn_prod_agentscript_msdyn_sessiontempl
+msdyn_msteamssetting
+msdyn_msteamssettingsv2
+msdyn_nonrelationalds
+msdyn_notesanalysisconfig
+msdyn_notificationfield
+msdyn_notificationtemplate
+msdyn_notificationtemplate_notificationfield
+msdyn_oc_geolocationprovider
+msdyn_ocautoblockrule
+msdyn_ocbotchannelregistration
+msdyn_ocbotchannelregistrationsecret
+msdyn_occhannelapiconversationprivilege
+msdyn_occhannelapimessageprivilege
+msdyn_occhannelapimethodmapping
+msdyn_occhannelconfiguration
+msdyn_occhannelstateconfiguration
+msdyn_ocexternalcontext
+msdyn_ocflaggedspam
+msdyn_oclanguage
+msdyn_ocliveworkitem
+msdyn_ocliveworkitemcapacityprofile
+msdyn_ocliveworkitemcharacteristic
+msdyn_ocliveworkitemcontextitem
+msdyn_ocliveworkitemparticipant
+msdyn_ocliveworkitemsentiment
+msdyn_ocliveworkstreamcontextvariable
+msdyn_oclocalizationdata
+msdyn_ocpaymentprofile
+msdyn_ocprovisioningstate
+msdyn_ocrecording
+msdyn_ocrequest
+msdyn_ocrichobject
+msdyn_ocrichobjectmap
+msdyn_ocruleitem
+msdyn_ocsentimentdailytopic
+msdyn_ocsentimentdailytopickeyword
+msdyn_ocsentimentdailytopictrending
+msdyn_ocsession
+msdyn_ocsessioncharacteristic
+msdyn_ocsessionparticipantevent
+msdyn_ocsessionsentiment
+msdyn_ocsimltraining
+msdyn_ocsitdimportconfig
+msdyn_ocsitdskill
+msdyn_ocsitrainingdata
+msdyn_ocskillidentmlmodel
+msdyn_ocsystemmessage
+msdyn_octag
+msdyn_odatav4ds
+msdyn_odosfeaturemetadata
+msdyn_odosmetadata
+msdyn_omnichannelconfiguration
+msdyn_omnichannelpersonalization
+msdyn_omnichannelqueue
+msdyn_omnichannelsyncconfig
+msdyn_operatinghour
+msdyn_opportunitykpiitem
+msdyn_opportunitymodelconfig
+msdyn_orgchartnode
+msdyn_originatingqueue
+msdyn_overflowactionconfig
+msdyn_paneconfiguration
+msdyn_panetabconfiguration
+msdyn_panetoolconfiguration
+msdyn_personalmessage
+msdyn_personalsoundsetting
+msdyn_personasecurityrolemapping
+msdyn_playbookactivity
+msdyn_playbookactivityattribute
+msdyn_playbookcategory
+msdyn_playbookinstance
+msdyn_playbooktemplate
+msdyn_pmanalysishistory
+msdyn_pmbusinessruleautomationconfig
+msdyn_pmcalendar
+msdyn_pmcalendarversion
+msdyn_pminferredtask
+msdyn_pmprocessextendedmetadataversion
+msdyn_pmprocesstemplate
+msdyn_pmprocessusersettings
+msdyn_pmprocessversion
+msdyn_pmrecording
+msdyn_pmtemplate
+msdyn_pmview
+msdyn_postalbum
+msdyn_postconfig
+msdyn_postruleconfig
+msdyn_predictioncomputationoperation
+msdyn_predictionmodelstatus
+msdyn_predictionscheduledoperation
+msdyn_predictivemodelscore
+msdyn_predictivescore
+msdyn_predictivescoringsyncstatus
+msdyn_preferredagent
+msdyn_preferredagentcustomeridentity
+msdyn_preferredagentroutedentity
+msdyn_presence
+msdyn_product_knowledgearticle
+msdyn_product_knowledgebaserecord
+msdyn_productivityactioninputparameter
+msdyn_productivityactionoutputparameter
+msdyn_productivityagentscript
+msdyn_productivityagentscriptstep
+msdyn_productivitymacroactiontemplate
+msdyn_productivitymacroconnector
+msdyn_productivitymacrosolutionconfiguration
+msdyn_productivityparameterdefinition
+msdyn_property
+msdyn_propertyassetassociation
+msdyn_propertylog
+msdyn_propertytemplateassociation
+msdyn_provider
+msdyn_readtracker
+msdyn_readtrackingenabledinfo
+msdyn_realtimescoring
+msdyn_realtimescoringoperation
+msdyn_recomputetracker
+msdyn_recording
+msdyn_recurrence
+msdyn_recurringsalesaction
+msdyn_recurringsalesactionv2
+msdyn_relationshipanalyticsmetadata
+msdyn_relationshipinsightsunifiedconfig
+msdyn_reportbookmark
+msdyn_richtextfile
+msdyn_routingconfiguration
+msdyn_routingconfigurationstep
+msdyn_routingrequest
+msdyn_routingrulesetsetting
+msdyn_rulesetdependencymapping
+msdyn_sabackupdiagnostic
+msdyn_sabatchruninstance
+msdyn_salesaccelerationinsight
+msdyn_salesaccelerationsettings
+msdyn_salesassignmentsetting
+msdyn_salescopilotinsight
+msdyn_salescopilotorgsettings
+msdyn_salesinsightssettings
+msdyn_salesocmessage
+msdyn_salesocsmstemplate
+msdyn_salespremiumdemo
+msdyn_salesroutingdiagnostic
+msdyn_salesroutingrun
+msdyn_salessuggestion
+msdyn_salestag
+msdyn_salestag_msdyn_sequence
+msdyn_saruninstance
+msdyn_schedule
+msdyn_sciconversation
+msdyn_scicustomemailhighlight
+msdyn_scicustomhighlight
+msdyn_scicustompublisher
+msdyn_scienvironmentsettings
+msdyn_sciusersettings
+msdyn_searchconfiguration
+msdyn_segment
+msdyn_segmentationsetting
+msdyn_segmentattribute
+msdyn_segmentcatalogue
+msdyn_sentimentanalysis
+msdyn_sequence
+msdyn_sequencestat
+msdyn_sequencetarget
+msdyn_sequencetargetstep
+msdyn_sequencetemplate
+msdyn_serviceconfiguration
+msdyn_servicecopilotplugin
+msdyn_servicecopilotpluginrole
+msdyn_serviceoneprovisioningrequest
+msdyn_sessiondata
+msdyn_sessionevent
+msdyn_sessionparticipant
+msdyn_sessionparticipantdata
+msdyn_sessiontemplate
+msdyn_sessiontemplate_applicationtab
+msdyn_shareasconfiguration
+msdyn_siconfig
+msdyn_sikeyvalueconfig
+msdyn_similarentitiesfeatureimportance
+msdyn_skillattachmentruleitem
+msdyn_skillattachmenttarget
+msdyn_slakpi
+msdyn_smartassistconfig
+msdyn_smartassistconfig_msdyn_appconfig
+msdyn_solutioncomponentcountdatasource
+msdyn_solutioncomponentcountsummary
+msdyn_solutioncomponentdatasource
+msdyn_solutioncomponentsummary
+msdyn_solutionhealthrule
+msdyn_solutionhealthruleargument
+msdyn_solutionhealthruleset
+msdyn_solutionhistory
+msdyn_solutionhistorydatasource
+msdyn_soundfile
+msdyn_soundnotificationsetting
+msdyn_submodeldefinition
+msdyn_suggestedactivity
+msdyn_suggestedactivitydatasource
+msdyn_suggestedcontact
+msdyn_suggestedcontactsdatasource
+msdyn_suggestionassignmentrule
+msdyn_suggestioninteraction
+msdyn_suggestionprincipalobjectaccess
+msdyn_suggestionrequestpayload
+msdyn_suggestionsellerpriority
+msdyn_suggestionsmodelsummary
+msdyn_suggestionssetting
+msdyn_swarm
+msdyn_swarmparticipant
+msdyn_swarmparticipantrule
+msdyn_swarmrole
+msdyn_swarmskill
+msdyn_swarmtemplate
+msdyn_systemuser_msdyn_omnichannelqueue
+msdyn_taggedrecord
+msdyn_teamschatassociation
+msdyn_teamschatsuggestion
+msdyn_teamscollaboration
+msdyn_teamsdialeradminsettings
+msdyn_templateforproperties
+msdyn_templateparameter
+msdyn_templatetags
+msdyn_timelinepin
+msdyn_timespent
+msdyn_tour
+msdyn_trainingresult
+msdyn_transcript
+msdyn_unifiedroutingdiagnostic
+msdyn_unifiedroutingrun
+msdyn_unifiedroutingsetuptracker
+msdyn_untrackedappointment
+msdyn_upgraderun
+msdyn_upgradestep
+msdyn_upgradeversion
+msdyn_urnotificationtemplate
+msdyn_urnotificationtemplatemapping
+msdyn_usagemetric
+msdyn_usagereporting
+msdyn_usersetting
+msdyn_virtualtablecolumncandidate
+msdyn_visitorjourney
+msdyn_vivacustomerlist
+msdyn_vivaentitysetting
+msdyn_vivaorgextensioncred
+msdyn_vivaorgsetting
+msdyn_vivausersetting
+msdyn_wallsavedquery
+msdyn_wallsavedqueryusersettings
+msdyn_wkwcolleaguesforcompany
+msdyn_wkwcolleaguesforcontact
+msdyn_wkwconfig
+msdyn_workflowactionstatus
+msdyn_worklistsuggestion
+msdyn_worklistsuggestionsource
+msdyn_worklistviewconfiguration
+msdyn_workqueuerecord
+msdyn_workqueuestate
+msdyn_workqueueusersetting
+msdyn_wqdatasource
+msdynce_botcontent
+msdyncrm_addtocalendarstyle
+msdyncrm_basestyle
+msdyncrm_buttonstyle
+msdyncrm_codestyle
+msdyncrm_columnstyle
+msdyncrm_contentblockstyle
+msdyncrm_dividerstyle
+msdyncrm_generalstyles
+msdyncrm_imagestyle
+msdyncrm_layoutstyle
+msdyncrm_qrcodestyle
+msdyncrm_textstyle
+msdyncrm_videostyle
+msdynmkt_byoacschannelinstance
+msdynmkt_byoacschannelinstanceaccount
+msdynmkt_catalogeventstatusconfiguration
+msdynmkt_configuration
+msdynmkt_eventmetadata
+msdynmkt_eventmetadata_sdkmessageprocessingstep
+msdynmkt_eventparametermetadata
+msdynmkt_experimentv2
+msdynmkt_featureconfiguration
+msdynmkt_infobipchannelinstance
+msdynmkt_infobipchannelinstanceaccount
+msdynmkt_linkmobilitychannelinstance
+msdynmkt_linkmobilitychannelinstanceaccount
+msdynmkt_metadataentityrelationship
+msdynmkt_metadataitem
+msdynmkt_metadatastorestate
+msdynmkt_mocksmsproviderchannelinstance
+msdynmkt_mocksmsproviderchannelinstanceaccount
+msdynmkt_predefinedplaceholder
+msdynmkt_telesignchannelinstance
+msdynmkt_telesignchannelinstanceaccount
+msdynmkt_trackingcontext
+msdynmkt_twiliochannelinstance
+msdynmkt_twiliochannelinstanceaccount
+msdynmkt_vibeschannelinstance
+msdynmkt_vibeschannelinstanceaccount
+msfp_alert
+msfp_alertrule
+msfp_emailtemplate
+msfp_fileresponse
+msfp_localizedemailtemplate
+msfp_project
+msfp_question
+msfp_questionresponse
+msfp_satisfactionmetric
+msfp_survey
+msfp_surveyinvite
+msfp_surveyreminder
+msfp_surveyresponse
+msfp_unsubscribedrecipient
+msgraphresourcetosubscription
+mspcat_catalogsubmissionfiles
+mspcat_packagestore
+mspp_accesscontrolrule_publishingstate
+mspp_adplacement
+mspp_columnpermission
+mspp_columnpermissionprofile
+mspp_columnpermissionprofile_webrole
+mspp_contentsnippet
+mspp_entityform
+mspp_entityformmetadata
+mspp_entitylist
+mspp_entitypermission
+mspp_entitypermission_webrole
+mspp_pagetemplate
+mspp_pollplacement
+mspp_powerpagescoreentityds
+mspp_publishingstate
+mspp_publishingstatetransitionrule
+mspp_publishingstatetransitionrule_webrole
+mspp_redirect
+mspp_shortcut
+mspp_sitemarker
+mspp_sitesetting
+mspp_webfile
+mspp_webform
+mspp_webformmetadata
+mspp_webformstep
+mspp_weblink
+mspp_weblinkset
+mspp_webpage
+mspp_webpageaccesscontrolrule
+mspp_webpageaccesscontrolrule_webrole
+mspp_webrole
+mspp_website
+mspp_websiteaccess
+mspp_websiteaccess_webrole
+mspp_websitelanguage
+mspp_webtemplate
+navigationsetting
+newprocess
+nlsqregistration
+officegraphdocument
+opportunity
+opportunityclose
+opportunitycompetitors
+opportunityproduct
+opportunitysalesprocess
+optionset
+orderclose
+organization
+organizationdatasyncfnostate
+organizationdatasyncstate
+organizationdatasyncsubscription
+organizationdatasyncsubscriptionentity
+organizationdatasyncsubscriptionfnotable
+ownermapping
+package
+package_solution
+packagehistory
+personaldocumenttemplate
+phonecall
+phonetocaseprocess
+picklistmapping
+pluginassembly
+pluginpackage
+plugintracelog
+plugintype
+plugintypestatistic
+position
+post
+postcomment
+postfollow
+postlike
+postregarding
+powerbidataset
+powerbidatasetapdx
+powerbimashupparameter
+powerbireport
+powerbireportapdx
+powerfxrule
+powerpagecomponent
+powerpagecomponent_mspp_webrole_account
+powerpagecomponent_mspp_webrole_contact
+powerpagecomponent_powerpagecomponent
+powerpagecomponent_webrole_systemuser
+powerpagesite
+powerpagesitelanguage
+powerpagesitepublished
+pricelevel
+principal
+principalentitybusinessunitmap
+principalentitymap
+principalobjectaccess
+principalobjectattributeaccess
+privilege
+privilegesremovalsetting
+processsession
+processstage
+processstageparameter
+processtrigger
+product
+productassociation
+productpricelevel
+productsalesliterature
+productsubstitute
+provisionlanguageforuser
+publisher
+publisheraddress
+quarterlyfiscalcalendar
+queue
+queueitem
+queuemembership
+quote
+quoteclose
+quotedetail
+ratingmodel
+ratingvalue
+recentlyused
+recommendeddocument
+recordfilter
+recurrencerule
+recurringappointmentmaster
+recyclebinconfig
+relationship
+relationshipattribute
+report
+reportcategory
+resource
+resourcegroup
+resourcegroupdatasource
+resourcespec
+retaineddataexcel
+retentionconfig
+retentionfailuredetail
+retentionoperation
+retentionoperationdetail
+ribbonmetadatatoprocess
+role
+roleeditorlayout
+roleprivileges
+roletemplate
+roletemplateprivileges
+rollupfield
+runtimedependency
+salesliterature
+salesliteratureitem
+salesorder
+salesorderdetail
+savedquery
+savedqueryvisualization
+sdkmessage
+sdkmessagefilter
+sdkmessageprocessingstep
+sdkmessageprocessingstepimage
+sdkmessageprocessingstepsecureconfig
+searchattributesettings
+searchcustomanalyzer
+searchrelationshipsettings
+searchtelemetry
+semiannualfiscalcalendar
+service
+serviceappointment
+servicecontractcontacts
+serviceendpoint
+serviceplan
+serviceplanappmodules
+serviceplancustomcontrol
+serviceplanmapping
+sharedlinksetting
+sharedobject
+sharedworkspace
+sharedworkspaceaccesstoken
+sharedworkspacepool
+sharepointdocumentlocation
+sharepointsite
+similarityrule
+site
+sitemap
+sla
+slaitem
+slakpiinstance
+socialactivity
+socialprofile
+solution
+solutioncomponent
+solutioncomponentattributeconfiguration
+solutioncomponentbatchconfiguration
+solutioncomponentconfiguration
+solutioncomponentrelationshipconfiguration
+solutionhistorydata
+stagedentity
+stagedentityattribute
+stagedmetadataasyncoperation
+stagesolutionupload
+subject
+subscriptionmanuallytrackedobject
+subscriptionstatisticsoffline
+subscriptionstatisticsoutlook
+subscriptionsyncentryoffline
+subscriptionsyncentryoutlook
+supportusertable
+synapsedatabase
+synapselinkexternaltablestate
+synapselinkprofile
+synapselinkprofileentity
+synapselinkprofileentitystate
+synapselinkschedule
+syncerror
+systemform
+systemuser
+systemuserauthorizationchangetracker
+systemuserlicenses
+systemuserprofiles
+systemuserroles
+systemusersyncmappingprofiles
+task
+team
+teammembership
+teammobileofflineprofilemembership
+teamprofiles
+teamroles
+teamsyncattributemappingprofiles
+teamtemplate
+template
+territory
+theme
+timestampdatemapping
+timezonedefinition
+timezonelocalizedname
+timezonerule
+tracelog
+transactioncurrency
+transformationmapping
+transformationparametermapping
+translationprocess
+uom
+uomschedule
+userform
+usermapping
+usermobileofflineprofilemembership
+userquery
+userqueryvisualization
+userrating
+usersettings
+virtualentitymetadata
+virtualresourcegroupresource
+webresource
+webwizard
+workflow
+workflowbinary
+workflowcardconnections
+workflowlog
+workqueue
+workqueueitem