diff --git a/providers/salesforce/internal/crm/core/errors.go b/providers/salesforce/internal/crm/core/errors.go index afc4dbb1da..ae798a61bf 100644 --- a/providers/salesforce/internal/crm/core/errors.go +++ b/providers/salesforce/internal/crm/core/errors.go @@ -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" @@ -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 { @@ -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) diff --git a/providers/salesforce/write_test.go b/providers/salesforce/write_test.go index d6165ffd15..974a5ace53 100644 --- a/providers/salesforce/write_test.go +++ b/providers/salesforce/write_test.go @@ -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'."), }, }, {