Repository: amp-labs/connectors
Pull request #1350 (2025-02-11 09:02:33)

[CON-1045] feta: Add ServiceNow Metadata Connector
Supports Table API Only.(ModuleTable)
Tests:
<img width="1202" alt="Screenshot 2025-02-11 at 13 10 45" src="https://github.com/user-attachments/assets/ecd8c319-e6b1-4d26-9ea4-30a5754c1819" />



RajatPawar: > Okay, Let me look into it. My Initial thought was the metadata Component for introspective endpoints or querying-data approach was not ready.

The implementation is still something we'd have to do - we can over time find ways to abstract it out, but just curious if this even fits well

Patch:
diff --git a/providers/servicenow/connector.go b/providers/servicenow/connector.go
new file mode 100644
index 00000000..bb928018
--- /dev/null
+++ b/providers/servicenow/connector.go
@@ -0,0 +1,48 @@
+package servicenow
+
+import (
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/components/operations"
+	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/providers"
+)
+
+const (
+	restAPIPrefix = "api"
+)
+
+type Connector struct {
+	// Basic connector
+	*components.Connector
+
+	// Require authenticated client
+	common.RequireAuthenticatedClient
+	// Require workspace
+	common.RequireWorkspace
+
+	// Supported operations
+	components.SchemaProvider
+}
+
+func NewConnector(params common.Parameters) (*Connector, error) {
+	return components.Initialize(providers.ServiceNow, params, constructor)
+}
+
+func constructor(base *components.Connector) (*Connector, error) {
+	connector := &Connector{Connector: base}
+
+	// Set the metadata provider for the connector
+	connector.SchemaProvider = schema.NewObjectSchemaProvider(
+		connector.HTTPClient().Client,
+		schema.FetchModeSerial,
+		operations.SingleObjectMetadataHandlers{
+			// Retrieving metadata using individual object calls can lead to rate limiting issues.
+			// Additionally, the rate limits may vary depending on the caller's roles.
+			BuildRequest:  connector.buildSingleObjectMetadataRequest,
+			ParseResponse: connector.parseSingleObjectMetadataResponse,
+		},
+	)
+
+	return connector, nil
+}
diff --git a/providers/servicenow/handlers.go b/providers/servicenow/handlers.go
new file mode 100644
index 00000000..90fab5d2
--- /dev/null
+++ b/providers/servicenow/handlers.go
@@ -0,0 +1,52 @@
+package servicenow
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/naming"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+)
+
+type responseData struct {
+	Result []map[string]any `json:"result"`
+	// Other fields
+}
+
+func (c *Connector) buildSingleObjectMetadataRequest(ctx context.Context, objectName string) (*http.Request, error) {
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, objectName)
+	if err != nil {
+		return nil, err
+	}
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseSingleObjectMetadataResponse(
+	ctx context.Context,
+	objectName string,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.ObjectMetadata, error) {
+	objectMetadata := common.ObjectMetadata{
+		FieldsMap:   make(map[string]string),
+		DisplayName: naming.CapitalizeFirstLetterEveryWord(objectName),
+	}
+
+	res, err := common.UnmarshalJSON[responseData](response)
+	if err != nil {
+		return nil, common.ErrFailedToUnmarshalBody
+	}
+
+	if len(res.Result) == 0 {
+		return nil, common.ErrMissingExpectedValues
+	}
+
+	// Using the first result data to generate the metadata.
+	for k := range res.Result[0] {
+		objectMetadata.FieldsMap[k] = k
+	}
+
+	return &objectMetadata, nil
+}


Test patch:
diff --git a/test/servicenow/connector.go b/test/servicenow/connector.go
new file mode 100644
index 00000000..18cb7830
--- /dev/null
+++ b/test/servicenow/connector.go
@@ -0,0 +1,55 @@
+package servicenow
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/providers/servicenow"
+	"github.com/amp-labs/connectors/test/utils"
+	"golang.org/x/oauth2"
+)
+
+func GetServiceNowConnector(ctx context.Context) *servicenow.Connector {
+	filePath := credscanning.LoadPath(providers.ServiceNow)
+	reader := utils.MustCreateProvCredJSON(filePath, true, true)
+
+	client, err := common.NewOAuthHTTPClient(ctx,
+		common.WithOAuthClient(http.DefaultClient),
+		common.WithOAuthConfig(getConfig(reader)),
+		common.WithOAuthToken(reader.GetOauthToken()),
+	)
+	if err != nil {
+		utils.Fail(err.Error())
+	}
+
+	conn, err := servicenow.NewConnector((common.Parameters{
+		AuthenticatedClient: client,
+		Workspace:           reader.Get(credscanning.Fields.Workspace),
+	}))
+	if err != nil {
+		utils.Fail("create servicenow connector", "error: ", err)
+	}
+
+	return conn
+}
+
+func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config {
+	workspace := reader.Get(credscanning.Fields.Workspace)
+
+	cfg := &oauth2.Config{
+		ClientID:     reader.Get(credscanning.Fields.ClientId),
+		ClientSecret: reader.Get(credscanning.Fields.ClientSecret),
+		RedirectURL:  "https://dev-api.withampersand.com/callbacks/v1/oauth",
+		Endpoint: oauth2.Endpoint{
+			AuthURL:   fmt.Sprintf("https://%s.service-now.com/oauth_auth.do", workspace),
+			TokenURL:  fmt.Sprintf("https://%s.service-now.com/oauth_token.do", workspace),
+			AuthStyle: oauth2.AuthStyleInParams,
+		},
+	}
+
+	return cfg
+}
diff --git a/test/servicenow/metadata/metadata.go b/test/servicenow/metadata/metadata.go
new file mode 100644
index 00000000..df03a5b9
--- /dev/null
+++ b/test/servicenow/metadata/metadata.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/amp-labs/connectors/test/servicenow"
+)
+
+func main() {
+	if err := run(); err != nil {
+		slog.Error(err.Error())
+		os.Exit(1)
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+
+	conn := servicenow.GetServiceNowConnector(ctx)
+
+	m, err := conn.ListObjectMetadata(ctx, []string{"now/table/incident", "now/contact", "now/consumer"})
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	fmt.Println("Results: ", m.Result)
+	fmt.Println("Errors: ", m.Errors)
+
+	return nil
+}