Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions providers/salesforce/internal/crm/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/amp-labs/connectors/common"
"github.com/amp-labs/connectors/common/interpreter"
Expand All @@ -30,6 +32,59 @@ func createError(baseErr error, sfErr jsonError, res *http.Response) error {
return baseErr
}

// noSuchColumnRe extracts the field and object names from a Salesforce
// "No such column" error message. Salesforce phrases the object differently
// depending on the API path: reads use "on entity 'Contact'" while writes
// use "on sobject of type Contact" (unquoted).
var noSuchColumnRe = regexp.MustCompile(
`No such column '([^']*)' on (?:entity '([^']*)'|sobject of type (\w+))`)

// fieldNotFoundGuidance explains the two causes of a "No such column" error
// (incorrect field name, or missing field-level visibility) and how to fix
// each. It is appended to every formatted field-not-found message.
//
//nolint:lll
const fieldNotFoundGuidance = " This usually means either the field name is incorrect (custom field names must end in '__c'), or the connected Salesforce user lacks field-level visibility for this field. To resolve it, verify the field's API name, or grant the user 'Visible' access to the field via their profile or a permission set."

// formatFieldNotFoundMessage turns a Salesforce "No such column" error into a
// customer-facing message: it restates the problem in Salesforce admin
// vocabulary (field/object) and appends actionable guidance. When the field
// and object names cannot be extracted, it falls back to Salesforce's own
// sentence with its trailing noise stripped.
func formatFieldNotFoundMessage(msg string) string {
if m := noSuchColumnRe.FindStringSubmatch(msg); m != nil {
object := m[2]
if object == "" {
object = m[3]
}

return fmt.Sprintf("Field '%s' was not found or is not accessible on object '%s'.%s",
m[1], object, fieldNotFoundGuidance)
}

if i := strings.Index(msg, "No such column"); i >= 0 {
msg = msg[i:]
}

msg = strings.TrimSpace(msg)
msg = strings.TrimSuffix(msg,
" Please reference your WSDL or the describe call for the appropriate names.")
msg = strings.TrimSuffix(strings.TrimSpace(msg),
" If you are attempting to use a custom field, be sure to append the '__c' after the custom field name.")

return strings.TrimSpace(msg) + fieldNotFoundGuidance
}

// fieldNotFoundError wraps common.ErrBadRequest for errors.Is matching but
// renders only the supplied message — bypassing the "bad request:" prefix
// and "(HTTP status N)" suffix that createError would otherwise add.
type fieldNotFoundError struct {
msg string
}

func (e *fieldNotFoundError) Error() string { return e.msg }
func (e *fieldNotFoundError) Unwrap() error { return common.ErrBadRequest }

func interpretJSONError(res *http.Response, body []byte) error { // nolint:cyclop
var errs []jsonError
if err := json.Unmarshal(body, &errs); err != nil {
Expand Down Expand Up @@ -59,6 +114,12 @@ func interpretJSONError(res *http.Response, body []byte) error { // nolint:cyclo
case "FIELD_INTEGRITY_EXCEPTION":
fallthrough
case "INVALID_FIELD":
if strings.Contains(sfErr.Message, "No such column") {
return &fieldNotFoundError{
msg: formatFieldNotFoundMessage(sfErr.Message),
}
}

return createError(common.ErrBadRequest, sfErr, res)
case "INVALID_QUERY_LOCATOR":
return createError(common.ErrCursorGone, sfErr, res)
Expand Down
2 changes: 1 addition & 1 deletion providers/salesforce/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestWrite(t *testing.T) { // nolint:funlen,cyclop
}.Server(),
ExpectedErrs: []error{
common.ErrBadRequest,
testutils.StringError("No such column 'AccountNumer' on sobject of type Account"),
testutils.StringError("Field 'AccountNumer' was not found or is not accessible on object 'Account'."),
},
},
{
Expand Down
Loading