From 2a71b2b426e01074e9d698190b8a3fff6585c9c9 Mon Sep 17 00:00:00 2001 From: "Christian G. Warden" Date: Mon, 15 Jan 2024 09:24:08 -0600 Subject: [PATCH] Add Salesforce Connector (#3747) * Add Salesforce Connector Add Salesforce connector supporting the FileIterator interface using the Bulk API with CSV jobs. Each source requires a SOQL Query and SObject name. Credentials can be provided for the source or the connector. Username/password and JWT authentication is supported. Each bulk job batch result is retrieved as a single CSV file for rill to ingest. * Improve Readability With Whitespace * Lowercase Error Strings Update error strings to be lowercase for consistency with existing convention. * Error If No Client Id When Authenticating Update salesforce.authenticate method to return an error if there is no Connected App client id defined. * Remove Authenticable Interface Remove Authenticable interface and forceProvider implementation. For force functions directly. * Fix Call to force.JwtAssertionForEndpoint JwtAssertionForEndpoint takes a file containing the signing key for creating a JWT assertion. Write the configured key to a temp file. * Simplify Missing Username Error Handling * Add Tests for endpoint Function * Remove Forceable Interface Use force.Force directly. * Replace Deprecated iotuil.TempFile with os.CreateTemp * Simplify endpoint() Short-circuit and return default endpoint if no endpoint is defined. Make code clearer when scheme is missing from the endpoint. * Return Error If Bulk Query Fails If the query is invalid and the initial PK chunking batch fails, return an error. * Add Documentation For Configuring Salesforce Credentials --- cli/cmd/runtime/start.go | 1 + docs/docs/deploy/credentials/credentials.md | 1 + docs/docs/deploy/credentials/salesforce.md | 50 +++ go.mod | 57 ++-- go.sum | 51 ++-- runtime/drivers/salesforce/authentication.go | 99 ++++++ .../drivers/salesforce/authentication_test.go | 25 ++ runtime/drivers/salesforce/bulk.go | 284 ++++++++++++++++++ runtime/drivers/salesforce/pk_chunking.go | 59 ++++ runtime/drivers/salesforce/salesforce.go | 213 +++++++++++++ runtime/drivers/salesforce/sql_store.go | 175 +++++++++++ .../icons/connectors/Salesforce.svelte | 34 +++ .../sources/modal/AddSourceModal.svelte | 3 + .../src/features/sources/modal/yupSchemas.ts | 5 + 14 files changed, 1018 insertions(+), 39 deletions(-) create mode 100644 docs/docs/deploy/credentials/salesforce.md create mode 100644 runtime/drivers/salesforce/authentication.go create mode 100644 runtime/drivers/salesforce/authentication_test.go create mode 100644 runtime/drivers/salesforce/bulk.go create mode 100644 runtime/drivers/salesforce/pk_chunking.go create mode 100644 runtime/drivers/salesforce/salesforce.go create mode 100644 runtime/drivers/salesforce/sql_store.go create mode 100644 web-common/src/components/icons/connectors/Salesforce.svelte diff --git a/cli/cmd/runtime/start.go b/cli/cmd/runtime/start.go index 55167643c26..48fef9040ba 100644 --- a/cli/cmd/runtime/start.go +++ b/cli/cmd/runtime/start.go @@ -38,6 +38,7 @@ import ( _ "github.com/rilldata/rill/runtime/drivers/https" _ "github.com/rilldata/rill/runtime/drivers/postgres" _ "github.com/rilldata/rill/runtime/drivers/s3" + _ "github.com/rilldata/rill/runtime/drivers/salesforce" _ "github.com/rilldata/rill/runtime/drivers/snowflake" _ "github.com/rilldata/rill/runtime/drivers/sqlite" _ "github.com/rilldata/rill/runtime/reconcilers" diff --git a/docs/docs/deploy/credentials/credentials.md b/docs/docs/deploy/credentials/credentials.md index 5bf27f19489..ae1c27ff159 100644 --- a/docs/docs/deploy/credentials/credentials.md +++ b/docs/docs/deploy/credentials/credentials.md @@ -18,3 +18,4 @@ For instructions on how to create a service account and set credentials in Rill - [Amazon Athena](./athena.md) - [BigQuery](./bigquery.md) - [Snowflake](./snowflake.md) +- [Salesforce](./salesforce.md) diff --git a/docs/docs/deploy/credentials/salesforce.md b/docs/docs/deploy/credentials/salesforce.md new file mode 100644 index 00000000000..cc01f12565b --- /dev/null +++ b/docs/docs/deploy/credentials/salesforce.md @@ -0,0 +1,50 @@ +--- +title: Salesforce +description: Connect to data in a Salesforce org using the Bulk API +sidebar_label: Salesforce +sidebar_position: 80 +--- + + + +## How to configure credentials in Rill + +Rill utilizes a Salesforce username along with a password (and token, depending +on org configuration) to authenticate against a Salesforce org. + +### Configure credentials for local development + +When working on a local project, you have the option to specify credentials when running Rill using the `--env` flag. +An example of using this syntax in terminal: +``` +rill start --env connector.salesforce.username="user@example.com" --env connector.salesforce.password="MyPasswordMyToken" +``` + +Alternatively, you can include the credentials string directly in the source code by adding the `username` and `password` parameters. +An example of a source using this approach: +``` +type: "salesforce" +endpoint: "login.salesforce.com" +username: "user@example.com" +password: "MyPasswordMyToken" +soql: "SELECT Id, Name, CreatedDate FROM Opportunity" +sobject: "Opportunity" +``` +This approach is less recommended because it places the connection string (which may contain sensitive information like passwords) in the source file, which is committed to Git. For more information, please refer to the documentation on [sources](../../reference/project-files/index.md). + +### Configure credentials for deployments on Rill Cloud + +Once a project having a Salesforce source has been deployed using `rill deploy`, Rill requires you to explicitly provide the credentials using following command: +``` +rill env configure +``` +Note that you must `cd` into the Git repository that your project was deployed from before running `rill env configure`. + +Leave `key` and `client_id` blank unless using JWT as described below. + +### JWT + +Authentication using JWT instead of a password is also supported by setting +`client_id` to the Client Id (also known as Consumer Key) of the Connected App +to use, and setting `key` to contain the PEM-formatted private key to use for +signing. diff --git a/go.mod b/go.mod index 228eb53cb2d..d3483274f37 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 + github.com/ForceCLI/force v1.0.5-0.20231227180521-1b251cf1a8b0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/MicahParks/keyfunc v1.9.0 github.com/NYTimes/gziphandler v1.1.1 @@ -65,7 +66,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.9.0 github.com/snowflakedb/gosnowflake v1.7.0 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/testcontainers/testcontainers-go v0.27.0 @@ -88,12 +89,12 @@ require ( gocloud.dev v0.34.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/oauth2 v0.13.0 - golang.org/x/sync v0.4.0 + golang.org/x/sync v0.5.0 golang.org/x/sys v0.15.0 google.golang.org/api v0.149.0 google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 - google.golang.org/grpc v1.60.0 - google.golang.org/protobuf v1.31.0 + google.golang.org/grpc v1.60.1 + google.golang.org/protobuf v1.32.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -114,6 +115,32 @@ require ( github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect + github.com/apache/arrow/go/v12 v12.0.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/jackc/pgtype v1.12.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect +) + +require ( + github.com/ForceCLI/inflect v0.0.0-20130829110746-cc00b5ad7a6a // indirect + github.com/ViViDboarder/gotifier v0.0.0-20140619195515-0f19f3d7c54c // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/onsi/gomega v1.20.1 // indirect +) + +require ( + github.com/ForceCLI/config v0.0.0-20230217143549-9149d42a3c99 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect @@ -123,7 +150,6 @@ require ( github.com/acomagu/bufpipe v1.0.4 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/andybalholm/brotli v1.0.5 // indirect - github.com/apache/arrow/go/v12 v12.0.0 // indirect github.com/apache/thrift v0.18.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.11 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.7 // indirect @@ -147,19 +173,15 @@ require ( github.com/containerd/containerd v1.7.11 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect - github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v24.0.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect @@ -169,7 +191,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -183,19 +204,16 @@ require ( github.com/google/wire v0.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/icholy/digest v0.1.22 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.12.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect @@ -210,15 +228,13 @@ require ( github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect @@ -229,13 +245,11 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/mtibben/percent v0.2.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.7 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect @@ -263,7 +277,7 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/term v0.15.0 // indirect @@ -271,7 +285,6 @@ require ( golang.org/x/tools v0.14.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect lukechampine.com/uint128 v1.3.0 // indirect diff --git a/go.sum b/go.sum index e8a3d07d92a..dc508bfd970 100644 --- a/go.sum +++ b/go.sum @@ -649,6 +649,12 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkM github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ForceCLI/config v0.0.0-20230217143549-9149d42a3c99 h1:H2axnitaP3Dw+tocMHPQHjM2wJ/+grF8sOIQGaJeEsg= +github.com/ForceCLI/config v0.0.0-20230217143549-9149d42a3c99/go.mod h1:WHFXv3VIHldTnYGmWAXAxsu4O754A9Zakq4DedI8PSA= +github.com/ForceCLI/force v1.0.5-0.20231227180521-1b251cf1a8b0 h1:XPYvEs+GpfNekTXPfOfkUWpbRYpOVorykDs6IPzlax8= +github.com/ForceCLI/force v1.0.5-0.20231227180521-1b251cf1a8b0/go.mod h1:qxApCXCTLnYtf2TRZbhnX3dcyEca29wss6YI8qZNMiI= +github.com/ForceCLI/inflect v0.0.0-20130829110746-cc00b5ad7a6a h1:mMd54YgLoeupNpbph3KdwvF58O0lZ72RQaJ2cFPOFDE= +github.com/ForceCLI/inflect v0.0.0-20130829110746-cc00b5ad7a6a/go.mod h1:DGKmCfb9oo5BivGO+szHk2ZvlqPDTlW4AYVpRBIVbms= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -703,6 +709,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/ViViDboarder/gotifier v0.0.0-20140619195515-0f19f3d7c54c h1:qLWjxZGLdzxp0Gc4Sf6f4w15D+wNKZ28HhkV9y5cAhw= +github.com/ViViDboarder/gotifier v0.0.0-20140619195515-0f19f3d7c54c/go.mod h1:/nH+y85gO3ta3b6JtRWGA5hPIH35XJr/ZHXlfrBRx3A= github.com/XSAM/otelsql v0.27.0 h1:i9xtxtdcqXV768a5C6SoT/RkG+ue3JTOgkYInzlTOqs= github.com/XSAM/otelsql v0.27.0/go.mod h1:0mFB3TvLa7NCuhm/2nU7/b2wEtsczkj8Rey8ygO7V+A= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= @@ -1016,7 +1024,7 @@ github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoY github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1038,6 +1046,7 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -1467,8 +1476,8 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -1664,11 +1673,11 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= @@ -1698,6 +1707,7 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -1750,6 +1760,7 @@ github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ github.com/networkplumbing/go-nft v0.2.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -1765,6 +1776,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= @@ -1779,6 +1791,8 @@ github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDs github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1893,6 +1907,8 @@ github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEt github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rgalanakis/golangal v0.0.0-20210923203926-e36008487518 h1:WBe84QNQwWtC92BctlbCj//ftrBWyjulKxzyWaFFuT0= +github.com/rgalanakis/golangal v0.0.0-20210923203926-e36008487518/go.mod h1:/7+rdrUijPGl0ZorwYZjZg8jwNXSGfQWTHS+sPE+Plc= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= @@ -1982,8 +1998,8 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2213,8 +2229,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2406,8 +2422,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2961,8 +2977,8 @@ google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k= -google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2981,8 +2997,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -3011,6 +3027,7 @@ gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/runtime/drivers/salesforce/authentication.go b/runtime/drivers/salesforce/authentication.go new file mode 100644 index 00000000000..423c7872366 --- /dev/null +++ b/runtime/drivers/salesforce/authentication.go @@ -0,0 +1,99 @@ +package salesforce + +import ( + "errors" + "fmt" + "net/url" + "os" + + force "github.com/ForceCLI/force/lib" +) + +const defaultEndpoint = "https://login.salesforce.com" + +type authenticationOptions struct { + Endpoint string + Username string + Password string + JWT string + ConnectedApp string +} + +func authenticate(options authenticationOptions) (*force.Force, error) { + if options.ConnectedApp == "" { + return nil, fmt.Errorf("connected app client id is required") + } + force.ClientId = options.ConnectedApp + + if options.Username == "" { + return nil, fmt.Errorf("username missing") + } + + isJWTSelected := len(options.JWT) > 0 + isSOAPSelected := len(options.Password) > 0 + + endpoint, err := endpoint(options) + if err != nil { + return nil, err + } + + switch { + case isJWTSelected: + return jwtLogin(endpoint, options) + case isSOAPSelected: + return soapLoginAtEndpoint(endpoint, options.Username, options.Password) + } + return nil, fmt.Errorf("unable to authenticate") +} + +func endpoint(options authenticationOptions) (endpoint string, err error) { + isEndpointSelected := len(options.Endpoint) > 0 + + if !isEndpointSelected { + return defaultEndpoint, nil + } + + // URL needs to have scheme lest the force cli lib chokes + uri, err := url.Parse(options.Endpoint) + if err != nil { + return defaultEndpoint, errors.New("unable to parse endpoint: " + options.Endpoint) + } + + if uri.Scheme == "" { + uri.Scheme = "https" + } + + return uri.String(), nil +} + +func jwtLogin(endpoint string, options authenticationOptions) (*force.Force, error) { + tempfile, err := os.CreateTemp("", "") + if err != nil { + return nil, fmt.Errorf("creating tempfile to write rsa key failed: %w", err) + } + defer os.Remove(tempfile.Name()) + + if _, err = tempfile.WriteString(options.JWT); err != nil { + return nil, fmt.Errorf("writing rsa key to tempfile failed: %w", err) + } + + assertion, err := force.JwtAssertionForEndpoint(endpoint, options.Username, tempfile.Name(), options.ConnectedApp) + if err != nil { + return nil, err + } + session, err := force.JWTLoginAtEndpoint(endpoint, assertion) + if err != nil { + return nil, fmt.Errorf("JWT authentication failed: %w", err) + } + + return force.NewForce(&session), nil +} + +func soapLoginAtEndpoint(endpoint, username, password string) (*force.Force, error) { + session, err := force.ForceSoapLoginAtEndpoint(endpoint, username, password) + if err != nil { + return nil, fmt.Errorf("SOAP authentication failed: %w", err) + } + + return force.NewForce(&session), nil +} diff --git a/runtime/drivers/salesforce/authentication_test.go b/runtime/drivers/salesforce/authentication_test.go new file mode 100644 index 00000000000..21ee5864589 --- /dev/null +++ b/runtime/drivers/salesforce/authentication_test.go @@ -0,0 +1,25 @@ +package salesforce + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEndpoint(t *testing.T) { + e, err := endpoint(authenticationOptions{Endpoint: "login.salesforce.com"}) + require.NoError(t, err) + require.Equal(t, "https://login.salesforce.com", e) + + e, err = endpoint(authenticationOptions{Endpoint: "example.my.salesforce.com"}) + require.NoError(t, err) + require.Equal(t, "https://example.my.salesforce.com", e) + + e, err = endpoint(authenticationOptions{Endpoint: "https://login.salesforce.com"}) + require.NoError(t, err) + require.Equal(t, "https://login.salesforce.com", e) + + e, err = endpoint(authenticationOptions{Endpoint: "https://example.my.salesforce.com"}) + require.NoError(t, err) + require.Equal(t, "https://example.my.salesforce.com", e) +} diff --git a/runtime/drivers/salesforce/bulk.go b/runtime/drivers/salesforce/bulk.go new file mode 100644 index 00000000000..d14748726cc --- /dev/null +++ b/runtime/drivers/salesforce/bulk.go @@ -0,0 +1,284 @@ +package salesforce + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "time" + + force "github.com/ForceCLI/force/lib" + "github.com/ForceCLI/force/lib/record_reader" + "go.uber.org/zap" +) + +type batchResult struct { + batch *force.BatchInfo + resultID string +} + +type bulkJob struct { + session *force.Force + objectName string + query string + job force.JobInfo + jobID string + batchID string + logger *zap.Logger + // pkChunking automatically splits large data sets into smaller batches of pkChunkSize, which we can query concurrently later on + pkChunkSize int + results []batchResult + nextResult int + tempFilePath string +} + +func (j *bulkJob) RecordReader(in io.Reader) record_reader.RecordReader { + return record_reader.NewCsv(in, &record_reader.Options{GroupSize: 100}) +} + +func makeBulkJob(session *force.Force, objectName, query string, queryAll bool, logger *zap.Logger) *bulkJob { + pkChunkSize := 100000 + contentType := force.JobContentTypeCsv + operation := "query" + + if queryAll { + operation = "queryAll" + } + + return &bulkJob{ + session: session, + objectName: objectName, + query: query, + pkChunkSize: pkChunkSize, + logger: logger, + job: force.JobInfo{ + Operation: operation, + Object: objectName, + ContentType: string(contentType), + }, + } +} + +func (c *connection) startJob(ctx context.Context, j *bulkJob) error { + session := j.session + + jobInfo, err := session.CreateBulkJobWithContext(ctx, j.job, func(request *http.Request) { + if isPKChunkingEnabled(j) { + pkChunkHeader := "chunkSize=" + strconv.Itoa(j.pkChunkSize) + parent := parentObject(j.objectName) + + if len(parent) > 0 { + pkChunkHeader += "; parent=" + parent + } + + request.Header.Add("Sforce-Enable-PKChunking", pkChunkHeader) + } + }) + if err != nil { + if errors.Is(err, force.InvalidBulkObject) { + return errors.New("object is not supported by Bulk API") + } + return err + } + result, err := session.BulkQueryWithContext(ctx, j.query, jobInfo.Id, j.job.ContentType) + if err != nil { + return errors.New("bulk query failed with " + err.Error()) + } + batchID := result.Id + // wait for chunking to complete + if isPKChunkingEnabled(j) { + for { + batchInfo, err := session.GetBatchInfoWithContext(ctx, jobInfo.Id, batchID) + if err != nil { + return errors.New("bulk job status failed with " + err.Error()) + } + + if batchInfo.State == "NotProcessed" { + // batches have been created + break + } + if batchInfo.State == "Failed" { + return errors.New("bulk query failed: " + batchInfo.StateMessage) + } + c.logger.Info("Waiting for pk chunking to complete") + select { + case <-time.After(2 * time.Second): + case <-ctx.Done(): + return fmt.Errorf("startJob cancelled: %w", ctx.Err()) + } + } + } + + jobInfo, err = session.CloseBulkJobWithContext(ctx, jobInfo.Id) + if err != nil { + return err + } + var status force.JobInfo + + for { + status, err = session.GetJobInfoWithContext(ctx, jobInfo.Id) + if err != nil { + return errors.New("bulk job status failed with " + err.Error()) + } + if status.NumberBatchesCompleted+status.NumberBatchesFailed == status.NumberBatchesTotal { + break + } + c.logger.Info("Waiting for bulk export to complete") + select { + case <-time.After(2 * time.Second): + case <-ctx.Done(): + return fmt.Errorf("startJob cancelled: %w", ctx.Err()) + } + } + + j.job = status + j.jobID = jobInfo.Id + j.batchID = batchID + + return nil +} + +func (j *bulkJob) getBatches(ctx context.Context) error { + if j.jobID == "" { + return fmt.Errorf("Invalid job: no job id") + } + + var batches []force.BatchInfo + var err error + errorMessage := "Could not retrieve job result. Reason: " + + if isPKChunkingEnabled(j) { + var allBatches []force.BatchInfo + allBatches, err = j.session.GetBatchesWithContext(ctx, j.job.Id) + // for pk chunking enabled jobs the first batch has no results + if allBatches != nil { + if allBatches[0].State == "Failed" { + return fmt.Errorf("Batch failed with: %s", allBatches[0].StateMessage) + } + + for _, b := range allBatches { + if b.State != "NotProcessed" && b.NumberRecordsProcessed > 0 { + batches = append(batches, b) + } + } + } + } else { + batch, berr := j.session.GetBatchInfoWithContext(ctx, j.jobID, j.batchID) + err = berr + batches = []force.BatchInfo{batch} + } + if err != nil { + return fmt.Errorf("%s %w", errorMessage+"batch status failed with ", err) + } + for _, b := range batches { + results, err := getBatchResults(ctx, j.session, j.job, b) + if err != nil { + return fmt.Errorf("%s %w", errorMessage+"batch results failed with ", err) + } + j.results = append(j.results, results...) + } + return nil +} + +func (j *bulkJob) retrieveJobResult(ctx context.Context, result int) (string, error) { + batchResult := j.results[result] + writer, err := os.CreateTemp("", "batchResult-"+batchResult.resultID+"-*.csv") + if err != nil { + return "", err + } + defer func() { + writer.Close() + }() + + httpBody := fetchBatchResult(ctx, j, batchResult, j.logger) + err = readAndWriteBody(ctx, j, httpBody, writer) + if closer, ok := httpBody.(io.ReadCloser); ok { + closer.Close() + } + if err != nil { + return "", err + } + return writer.Name(), nil +} + +func fetchBatchResult(ctx context.Context, j *bulkJob, resultInfo batchResult, logger *zap.Logger) io.Reader { + errorMessage := "Could not fetch job result. Reason: " + + if resultInfo.batch.State == "Failed" { + logger.Error(errorMessage + "batch failed with " + resultInfo.batch.StateMessage) + return bytes.NewReader(nil) + } + if resultInfo.batch.NumberRecordsProcessed == 0 { + logger.Debug("No records found for query") + return bytes.NewReader(nil) + } + var result io.Reader + err := j.session.RetrieveBulkJobQueryResultsWithCallbackWithContext(ctx, j.job, resultInfo.batch.Id, resultInfo.resultID, func(r *http.Response) error { + result = r.Body + return nil + }) + if err != nil { + logger.Error(errorMessage + "batch failed with " + err.Error()) + return bytes.NewReader(nil) + } + return result +} + +func readAndWriteBody(ctx context.Context, j *bulkJob, httpBody io.Reader, w io.Writer) error { + recReader := j.RecordReader(httpBody) + for { + records, err := recReader.Next() + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + return err + } + if _, err := io.Copy(w, bytes.NewReader(records.Bytes)); err != nil { + return fmt.Errorf("write failed: %w", err) + } + select { + case <-ctx.Done(): + return fmt.Errorf("readAndWriteBody cancelled: %w", ctx.Err()) + default: + } + } +} + +// Get all of the results for a batch. Most batches have one results, but +// large batches can be split into multiple result files. +func getBatchResults(ctx context.Context, session *force.Force, job force.JobInfo, batch force.BatchInfo) ([]batchResult, error) { + var resultIDs []string + var results []batchResult + jobInfo, err := session.RetrieveBulkQueryWithContext(ctx, job.Id, batch.Id) + if err != nil { + return nil, err + } + + jct, err := job.JobContentType() + if err != nil { + return nil, err + } + if jct == force.JobContentTypeJson { + err = json.Unmarshal(jobInfo, &resultIDs) + } else { + var resultList struct { + Results []string `xml:"result"` + } + err = xml.Unmarshal(jobInfo, &resultList) + resultIDs = resultList.Results + } + if err != nil { + return nil, err + } + for _, r := range resultIDs { + results = append(results, batchResult{batch: &batch, resultID: r}) + } + + return results, err +} diff --git a/runtime/drivers/salesforce/pk_chunking.go b/runtime/drivers/salesforce/pk_chunking.go new file mode 100644 index 00000000000..b99c3f4dd9a --- /dev/null +++ b/runtime/drivers/salesforce/pk_chunking.go @@ -0,0 +1,59 @@ +package salesforce + +import ( + "regexp" + "sort" + "strings" +) + +func isPKChunkingEnabled(bulkJob *bulkJob) bool { + return bulkJob.pkChunkSize > 0 && isPKChunkingEnabledObject(bulkJob.objectName) +} + +// pk chunking only works for certain standard objects, custom objects and share/history of those +func isPKChunkingEnabledObject(objectName string) bool { + standardObjectPKChunkingEnabled := []string{"account", "accounthistory", "accountshare", "campaign", "campaignhistory", "campaignmember", "campaignmemberhistory", "campaignmembershare", "campaignshare", "case", "casehistory", "caseshare", "contact", "contacthistory", "contactshare", "event", "eventhistory", "eventrelation", "eventrelationhistory", "eventrelationshare", "eventshare", "lead", "leadhistory", "leadshare", "opportunity", "opportunityhistory", "opportunityshare", "task", "taskhistory", "taskshare", "user", "userhistory", "usershare"} + + isCustomObject, err := regexp.MatchString("__c$", objectName) + if err != nil { + panic("Regex errored out with " + err.Error()) + } + isShareHistoryCustomObject, err := regexp.MatchString("(__Share|__History)$", objectName) + if err != nil { + panic("Regex errored out with " + err.Error()) + } + isHistoricalTrendingObject, err := regexp.MatchString("_hd$", objectName) + if err != nil { + panic("Regex errored out with " + err.Error()) + } + + return contains(standardObjectPKChunkingEnabled, objectName) || isCustomObject || isShareHistoryCustomObject || isHistoricalTrendingObject +} + +// performs a binary search for a given string +func contains(values []string, val string) bool { + val = strings.ToLower(val) + index := sort.SearchStrings(values, val) + + return index < len(values) && values[index] == val +} + +// if a object has Share or History (__Share, __History for custom) suffix, it likely has a parent object, which should be queried when using pk chunking +func parentObject(objectName string) string { + var parent string + regex := regexp.MustCompile("(__Share|Share|__History|History)$") + indexes := regex.FindStringIndex(objectName) + + if indexes != nil { + start, end := indexes[0], indexes[1] + suffix := objectName[start:end] + isCustomObject := suffix[0:2] == "__" + + parent = objectName[:start] + if isCustomObject { + parent += "__c" + } + } + + return parent +} diff --git a/runtime/drivers/salesforce/salesforce.go b/runtime/drivers/salesforce/salesforce.go new file mode 100644 index 00000000000..ea25eef1f04 --- /dev/null +++ b/runtime/drivers/salesforce/salesforce.go @@ -0,0 +1,213 @@ +package salesforce + +import ( + "context" + + force "github.com/ForceCLI/force/lib" + "github.com/rilldata/rill/runtime/drivers" + "github.com/rilldata/rill/runtime/pkg/activity" + "go.uber.org/zap" +) + +func init() { + drivers.Register("salesforce", driver{}) + drivers.RegisterAsConnector("salesforce", driver{}) + force.Log = silentLogger{} +} + +type silentLogger struct{} + +func (silentLogger) Info(args ...any) { +} + +var spec = drivers.Spec{ + DisplayName: "Salesforce", + Description: "Connect to Salesforce.", + SourceProperties: []drivers.PropertySchema{ + { + Key: "soql", + Type: drivers.StringPropertyType, + Required: true, + DisplayName: "SOQL", + Description: "SOQL Query to extract data from Salesforce.", + Placeholder: "SELECT Id, CreatedDate, Name FROM Opportunity", + }, + { + Key: "sobject", + Type: drivers.StringPropertyType, + Required: true, + DisplayName: "SObject", + Description: "SObject to query in Salesforce.", + Placeholder: "Opportunity", + }, + { + Key: "queryAll", + Type: drivers.BooleanPropertyType, + Required: false, + DisplayName: "Query All", + Description: "Include deleted and archived records", + }, + { + Key: "username", + DisplayName: "Salesforce Username", + Type: drivers.StringPropertyType, + Required: false, + Placeholder: "user@example.com", + Hint: "Either set this or pass --env connector.salesforce.username=... to rill start", + }, + { + Key: "password", + DisplayName: "Salesforce Password", + Type: drivers.StringPropertyType, + Required: false, + Hint: "Either set this or pass --env connector.salesforce.password=... to rill start", + }, + { + Key: "key", + DisplayName: "JWT Key for Authentication", + Type: drivers.StringPropertyType, + Required: false, + Hint: "Either set this or pass --env connector.salesforce.key=... to rill start", + }, + { + Key: "endpoint", + DisplayName: "Login Endpoint", + Type: drivers.StringPropertyType, + Required: false, + Default: "login.salesforce.com", + Placeholder: "login.salesforce.com", + Hint: "Either set this or pass --env connector.salesforce.endpoint=... to rill start", + }, + { + Key: "client_id", + DisplayName: "Connected App Client Id", + Type: drivers.StringPropertyType, + Required: false, + Default: defaultClientID, + Hint: "Either set this or pass --env connector.salesforce.client_id=... to rill start", + }, + }, + ConfigProperties: []drivers.PropertySchema{ + { + Key: "username", + Secret: false, + }, + { + Key: "password", + Secret: true, + }, + { + Key: "key", + Secret: true, + }, + { + Key: "endpoint", + Secret: false, + }, + { + Key: "client_id", + Secret: false, + }, + }, +} + +type driver struct{} + +func (d driver) Open(config map[string]any, shared bool, client activity.Client, logger *zap.Logger) (drivers.Handle, error) { + // actual db connection is opened during query + return &connection{ + config: config, + logger: logger, + }, nil +} + +func (d driver) Drop(config map[string]any, logger *zap.Logger) error { + return drivers.ErrDropNotSupported +} + +func (d driver) Spec() drivers.Spec { + return spec +} + +func (d driver) HasAnonymousSourceAccess(ctx context.Context, src map[string]any, logger *zap.Logger) (bool, error) { + return false, nil +} + +func (d driver) TertiarySourceConnectors(ctx context.Context, src map[string]any, logger *zap.Logger) ([]string, error) { + return nil, nil +} + +type connection struct { + config map[string]any + logger *zap.Logger +} + +// Migrate implements drivers.Connection. +func (c *connection) Migrate(ctx context.Context) (err error) { + return nil +} + +// MigrationStatus implements drivers.Handle. +func (c *connection) MigrationStatus(ctx context.Context) (current, desired int, err error) { + return 0, 0, nil +} + +// Driver implements drivers.Connection. +func (c *connection) Driver() string { + return "salesforce" +} + +// Config implements drivers.Connection. +func (c *connection) Config() map[string]any { + return c.config +} + +// Close implements drivers.Connection. +func (c *connection) Close() error { + return nil +} + +// AsRegistry implements drivers.Connection. +func (c *connection) AsRegistry() (drivers.RegistryStore, bool) { + return nil, false +} + +// AsCatalogStore implements drivers.Connection. +func (c *connection) AsCatalogStore(instanceID string) (drivers.CatalogStore, bool) { + return nil, false +} + +// AsRepoStore implements drivers.Connection. +func (c *connection) AsRepoStore(instanceID string) (drivers.RepoStore, bool) { + return nil, false +} + +// AsAdmin implements drivers.Handle. +func (c *connection) AsAdmin(instanceID string) (drivers.AdminService, bool) { + return nil, false +} + +// AsOLAP implements drivers.Connection. +func (c *connection) AsOLAP(instanceID string) (drivers.OLAPStore, bool) { + return nil, false +} + +// AsObjectStore implements drivers.Connection. +func (c *connection) AsObjectStore() (drivers.ObjectStore, bool) { + return nil, false +} + +// AsTransporter implements drivers.Connection. +func (c *connection) AsTransporter(from, to drivers.Handle) (drivers.Transporter, bool) { + return nil, false +} + +// AsFileStore implements drivers.Connection. +func (c *connection) AsFileStore() (drivers.FileStore, bool) { + return nil, false +} + +// AsSQLStore implements drivers.Connection. +func (c *connection) AsSQLStore() (drivers.SQLStore, bool) { + return c, true +} diff --git a/runtime/drivers/salesforce/sql_store.go b/runtime/drivers/salesforce/sql_store.go new file mode 100644 index 00000000000..fbb63cd2f4b --- /dev/null +++ b/runtime/drivers/salesforce/sql_store.go @@ -0,0 +1,175 @@ +package salesforce + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/mitchellh/mapstructure" + "github.com/rilldata/rill/runtime/drivers" +) + +const defaultClientID = "3MVG9KsVczVNcM8y6w3Kjszy.DW9gMzcYDHT97WIX3NYNYA35UvITypEhtYc6FDY8qqcDEIQc_qJgZErv6Q_d" + +// Query implements drivers.SQLStore +func (c *connection) Query(ctx context.Context, props map[string]any) (drivers.RowIterator, error) { + return nil, drivers.ErrNotImplemented +} + +// QueryAsFiles implements drivers.SQLStore +func (c *connection) QueryAsFiles(ctx context.Context, props map[string]any, opt *drivers.QueryOption, p drivers.Progress) (drivers.FileIterator, error) { + srcProps, err := parseSourceProperties(props) + if err != nil { + return nil, err + } + + var username, password, endpoint, key, clientID string + if srcProps.Username != "" { // get from src properties + username = srcProps.Username + } else if u, ok := c.config["username"].(string); ok && u != "" { // get from driver configs + username = u + } else { + return nil, fmt.Errorf("the property 'username' is required for Salesforce. Provide 'username' in the YAML properties or pass '--env connector.salesforce.username=...' to 'rill start'") + } + + if srcProps.Endpoint != "" { // get from src properties + endpoint = srcProps.Endpoint + } else if e, ok := c.config["endpoint"].(string); ok && e != "" { // get from driver configs + endpoint = e + } else { + return nil, fmt.Errorf("the property 'endpoint' is required for Salesforce. Provide 'endpoint' in the YAML properties or pass '--env connector.salesforce.endpoint=...' to 'rill start'") + } + + if srcProps.ClientID != "" { // get from src properties + clientID = srcProps.ClientID + } else if c, ok := c.config["client_id"].(string); ok && c != "" { // get from driver configs + clientID = c + } else { + clientID = defaultClientID + } + + if srcProps.Password != "" { // get from src properties + password = srcProps.Password + } else if p, ok := c.config["password"].(string); ok && p != "" { // get from driver configs + password = p + } + + if srcProps.Key != "" { // get from src properties + key = srcProps.Key + } else if k, ok := c.config["key"].(string); ok && k != "" { // get from driver configs + key = k + } + + if password == "" && key == "" { + return nil, fmt.Errorf("the property 'password' or property 'key' is required for Salesforce. Provide 'password' or 'key' in the YAML properties or pass '--env connector.salesforce.password=...' or '--env connector.salesforce.key=...' to 'rill start'") + } + + authOptions := authenticationOptions{ + Username: username, + Password: password, + JWT: key, + Endpoint: endpoint, + ConnectedApp: clientID, + } + + session, err := authenticate(authOptions) + if err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + + job := makeBulkJob(session, srcProps.SObject, srcProps.SOQL, srcProps.QueryAll, c.logger) + + err = c.startJob(ctx, job) + if err != nil { + return nil, err + } + + err = job.getBatches(ctx) + if err != nil { + return nil, err + } + + return job, nil +} + +func (j *bulkJob) Format() string { + return "csv" +} + +// Close implements drivers.RowIterator. +func (j *bulkJob) Close() error { + if j.tempFilePath != "" { + err := os.Remove(j.tempFilePath) + j.tempFilePath = "" + if err != nil { + return fmt.Errorf("failed to delete temp file: %w", err) + } + } + return nil +} + +// Next implements drivers.RowIterator. +func (j *bulkJob) Next() ([]string, error) { + if j.jobID == "" { + return nil, fmt.Errorf("invalid job: no job id") + } + if j.job.NumberRecordsProcessed == 0 { + return nil, io.EOF + } + if j.tempFilePath != "" { + err := os.Remove(j.tempFilePath) + j.tempFilePath = "" + if err != nil { + return nil, fmt.Errorf("failed to delete temp file: %w", err) + } + } + if j.nextResult == len(j.results) { + return nil, io.EOF + } + tempFile, err := j.retrieveJobResult(context.Background(), j.nextResult) + if err != nil { + return nil, fmt.Errorf("failed to retrieve batch: %w", err) + } + j.tempFilePath = tempFile + j.nextResult++ + return []string{j.tempFilePath}, nil +} + +// Size implements drivers.RowIterator. +func (j *bulkJob) Size(unit drivers.ProgressUnit) (int64, bool) { + switch unit { + case drivers.ProgressUnitRecord: + return int64(j.job.NumberRecordsProcessed), true + case drivers.ProgressUnitFile: + return int64(len(j.results)), true + default: + return 0, false + } +} + +type sourceProperties struct { + SOQL string `mapstructure:"soql"` + SObject string `mapstructure:"sobject"` + QueryAll bool `mapstructure:"queryAll"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Key string `mapstructure:"key"` + Endpoint string `mapstructure:"endpoint"` + ClientID string `mapstructure:"client_id"` +} + +func parseSourceProperties(props map[string]any) (*sourceProperties, error) { + conf := &sourceProperties{} + err := mapstructure.Decode(props, conf) + if err != nil { + return nil, err + } + if conf.SOQL == "" { + return nil, fmt.Errorf("property 'soql' is mandatory for connector \"salesforce\"") + } + if conf.SObject == "" { + return nil, fmt.Errorf("property 'sobject' is mandatory for connector \"salesforce\"") + } + return conf, err +} diff --git a/web-common/src/components/icons/connectors/Salesforce.svelte b/web-common/src/components/icons/connectors/Salesforce.svelte new file mode 100644 index 00000000000..8bbf0d197fe --- /dev/null +++ b/web-common/src/components/icons/connectors/Salesforce.svelte @@ -0,0 +1,34 @@ + + Salesforce.com logo + A cloud computing company based in San Francisco, California, United States + + + + + + + + + + + + diff --git a/web-common/src/features/sources/modal/AddSourceModal.svelte b/web-common/src/features/sources/modal/AddSourceModal.svelte index ed0aa333c23..7b8cc835e2c 100644 --- a/web-common/src/features/sources/modal/AddSourceModal.svelte +++ b/web-common/src/features/sources/modal/AddSourceModal.svelte @@ -14,6 +14,7 @@ import MicrosoftAzureBlobStorage from "../../../components/icons/connectors/MicrosoftAzureBlobStorage.svelte"; import DuckDB from "../../../components/icons/connectors/DuckDB.svelte"; import Postgres from "../../../components/icons/connectors/Postgres.svelte"; + import Salesforce from "../../../components/icons/connectors/Salesforce.svelte"; import Snowflake from "../../../components/icons/connectors/Snowflake.svelte"; import SQLite from "../../../components/icons/connectors/SQLite.svelte"; import { appScreen } from "../../../layout/app-store"; @@ -44,6 +45,7 @@ "postgres", "sqlite", "snowflake", + "salesforce", "local_file", "https", ]; @@ -59,6 +61,7 @@ postgres: Postgres, sqlite: SQLite, snowflake: Snowflake, + salesforce: Salesforce, local_file: LocalFile, https: Https, }; diff --git a/web-common/src/features/sources/modal/yupSchemas.ts b/web-common/src/features/sources/modal/yupSchemas.ts index 7bd8985473b..c7c3842b2dc 100644 --- a/web-common/src/features/sources/modal/yupSchemas.ts +++ b/web-common/src/features/sources/modal/yupSchemas.ts @@ -117,6 +117,11 @@ export function getYupSchema(connector: V1ConnectorSpec) { .required("Source name is required"), dsn: yup.string(), }); + case "salesforce": + return yup.object().shape({ + soql: yup.string().required("soql is required"), + sobject: yup.string().required("sobject is required"), + }); case "athena": return yup.object().shape({ sql: yup.string().required("sql is required"),