crystalforce
Crystalforce is a Crystal shard for the Salesforce REST API. A Crystal port of Restforce.
Installation
Add this to your application's shard.yml:
dependencies:
crystalforce:
github: masak1yu/crystalforce
Then run:
shards install
Development
Build
shards build
Run tests
crystal spec
Check formatting
crystal tool format --check
Usage
require "crystalforce"
Initialization
Username/Password authentication
client = Crystalforce.new(
username: "foo",
password: "bar",
security_token: "security_token",
client_id: "client_id",
client_secret: "client_secret",
)
OAuth token refresh
client = Crystalforce.new(
refresh_token: "refresh_token",
client_id: "client_id",
client_secret: "client_secret",
)
JWT Bearer authentication
jwt_key = File.read("path/to/private_key.pem")
client = Crystalforce.new(
username: "foo",
client_id: "client_id",
jwt_key: jwt_key,
)
Client Credentials authentication
Requires the host to be set to your My Domain URL:
client = Crystalforce.new(
client_id: "client_id",
client_secret: "client_secret",
host: "yourdomain.my.salesforce.com",
)
Sandbox Orgs
You can connect to sandbox orgs by specifying a host. The default host is
login.salesforce.com:
client = Crystalforce.new(
host: "test.salesforce.com",
username: "foo",
password: "bar",
security_token: "security_token",
client_id: "client_id",
client_secret: "client_secret",
)
Options
API versions
By default, the shard uses version 34.0 of the Salesforce API.
You can change the api_version on a per-client basis:
client = Crystalforce.new(
api_version: "58.0",
username: "foo",
password: "bar",
client_id: "client_id",
client_secret: "client_secret",
)
Authentication retries
When an API call returns a 401 Unauthorized, the client automatically re-authenticates and retries the request. The default retry count is 3:
client = Crystalforce.new(
authentication_retries: 5,
# ... auth params
)
Authentication callback
You can provide a callback that is invoked after each successful authentication (including re-authentications on 401):
callback = Proc(Crystalforce::Client, Nil).new do |client|
puts "Authenticated! Token: #{client.access_token}"
end
client = Crystalforce.new(
authentication_callback: callback,
# ... auth params
)
Environment variables
Configuration can be provided via environment variables. Explicit parameters take precedence over environment variables:
| Variable | Config key |
|----------|-----------|
| SALESFORCE_USERNAME | username |
| SALESFORCE_PASSWORD | password |
| SALESFORCE_SECURITY_TOKEN | security_token |
| SALESFORCE_CLIENT_ID | client_id |
| SALESFORCE_CLIENT_SECRET | client_secret |
| SALESFORCE_HOST | host |
| SALESFORCE_API_VERSION | api_version |
| SALESFORCE_PROXY_URI | proxy_uri |
# Uses SALESFORCE_* environment variables as defaults
client = Crystalforce.new
Custom headers
Add custom headers to all requests:
client = Crystalforce.new(
request_headers: {"X-Custom-Header" => "value"},
# ... auth params
)
Logging
Crystalforce uses Crystal's standard Log module under the crystalforce source.
Configure the log level to see request/response details:
Log.setup("crystalforce", :debug)
GZIP compression
Enable GZIP compression for requests and responses:
client = Crystalforce.new(
compress: true,
# ... auth params
)
SSL configuration
Provide a custom SSL context:
ssl = OpenSSL::SSL::Context::Client.new
client = Crystalforce.new(
ssl: ssl,
# ... auth params
)
Proxy
Route requests through an HTTP proxy:
client = Crystalforce.new(
proxy_uri: "http://proxy.example.com:8080",
# ... auth params
)
Caching
Cache GET request responses using the built-in MemoryCache or a custom
implementation of the Crystalforce::Cache module:
cache = Crystalforce::MemoryCache.new
client = Crystalforce.new(
cache: cache,
# ... auth params
)
Query
accounts = client.query("select Id, Something__c from Account where Id = 'someid'")
Automatic pagination
Use query_with_pagination to get a Collection that automatically fetches
subsequent pages as you iterate:
collection = client.query_with_pagination("SELECT Id, Name FROM Account")
puts collection.total_size
collection.each { |record| puts record["Name"] }
query_all
accounts = client.query_all("select Id, Something__c from Account where isDeleted = true")
query_all allows you to include results from your query that Salesforce hides in the default query method. These include soft-deleted records and archived records (e.g. Task and Event records which are usually archived automatically after they are a year old).
search
results = client.search("FIND {Foobar Inc.}")
explain
plan = client.explain("select Id from Account")
find
account = client.find("Account", "0016000000MRatd")
# Find by external ID
account = client.find("Account", "12345", "External__c")
select
account = client.select("Account", "0016000000MRatd", ["Id", "Name", "Industry"])
create
client.create("Account", {:Name => "Foobar Inc."})
# Bang version raises on error and returns parsed response
result = client.create!("Account", {:Name => "Foobar Inc."})
puts result["id"]
update
client.update("Account", "0016000000MRatd", {:Name => "Whizbang Corp"})
# Bang version raises on error
client.update!("Account", "0016000000MRatd", {:Name => "Whizbang Corp"})
upsert
client.upsert("Account", "External__c", {"External__c" => "12", "Name" => "Foobar"})
# Bang version raises on error
client.upsert!("Account", "External__c", {"External__c" => "12", "Name" => "Foobar"})
destroy
client.destroy("Account", "0016000000MRatd")
# Bang version raises on error
client.destroy!("Account", "0016000000MRatd")
describe
# Describe all SObjects
all = client.describe
# Describe a specific SObject
account_desc = client.describe("Account")
describe_layouts
layouts = client.describe_layouts("Account")
list_sobjects
names = client.list_sobjects
limits
limits = client.limits
user_info
info = client.user_info
org_id
id = client.org_id
get_updated / get_deleted
updated = client.get_updated("Account", Time.utc - 1.day, Time.utc)
deleted = client.get_deleted("Account", Time.utc - 1.day, Time.utc)
recent
items = client.recent(10)
picklist_values
values = client.picklist_values("Account", "Industry")
# Dependent picklist (filtered by controlling field value)
values = client.picklist_values("MyObject__c", "SubType__c", valid_for: "TypeA")
Batch API
Execute up to 25 subrequests in a single call. Automatically chunks larger batches:
results = client.batch do |b|
b.create("Account", {:Name => "Batch1"})
b.create("Account", {:Name => "Batch2"})
b.update("Account", "001xx...", {:Name => "Updated"})
b.destroy("Account", "001xx...")
end
Composite API
Execute multiple dependent requests in a single call with reference IDs:
results = client.composite do |c|
c.create("Account", "newAccount", {:Name => "Composite1"})
c.find("Account", "findAccount", "001xx...")
c.update("Account", "updateAccount", "001xx...", {:Name => "Updated"})
c.destroy("Account", "deleteAccount", "001xx...")
end
Low-level HTTP
response = client.api_get("/sobjects/Account/describe")
response = client.api_post("/sobjects/Account", {:Name => "Test"})
response = client.api_patch("/sobjects/Account/001xx...", {:Name => "Updated"})
response = client.api_put("/some/path", {:key => "value"})
response = client.api_delete("/sobjects/Account/001xx...")
Tooling API
tooling = Crystalforce.tooling(
username: "foo",
client_id: "client_id",
jwt_key: jwt_key,
)
classes = tooling.query("SELECT Id, Name FROM ApexClass LIMIT 10")
desc = tooling.describe("ApexClass")
Canvas
Decode and verify a Force.com Canvas signed request:
result = Crystalforce::Canvas.decode_signed_request(signed_request, client_secret)
Streaming API
Subscribe to PushTopics or Platform Events via CometD long-polling:
streaming = client.streaming
# Subscribe to a PushTopic
streaming.subscribe("/topic/MyTopic") do |message|
puts message
end
# Subscribe with replay
streaming.subscribe("/event/MyEvent__e", replay_id: -2_i64) do |message|
puts message
end
Contributing
- Fork it ( https://github.com/masak1yu/crystalforce/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- masak1yu - creator, maintainer