Exact.Client (exact_online v0.1.0)

Copy Markdown View Source

A configured Exact Online API client.

Building a client

With a token you manage yourself:

client = Exact.Client.new(access_token: token, division: 123_456)

With a token store, which is what you want in an application because it lets the client refresh and persist the rotating refresh token for you:

client =
  Exact.Client.new(
    division: 123_456,
    token_store: Exact.TokenStore.ETS,
    token_key: user_id,
    credentials: [
      client_id: client_id,
      client_secret: client_secret,
      redirect_uri: redirect_uri,
      region: :nl
    ]
  )

Paths

Paths are relative to the division, so "crm/Accounts" becomes /api/v1/123456/crm/Accounts. A path starting with /api is used as-is, which is how the division independent endpoints are reached ("/api/v1/current/Me"). A full URL is used as-is too, which is how stream/3 follows the __next cursor.

Return values

Every function returns {:ok, result} or {:error, %Exact.Error{}}. The bang variants raise the error instead. list/3 returns an Exact.Page, get/3 and post/4 return the entity as a plain map with the field names Exact Online uses, and put/4 and delete/3 return {:ok, nil} because the API answers those with 204 No Content.

Summary

Functions

Deletes an entity. Returns {:ok, nil}.

Same as delete/3, but raises Exact.Error on failure.

Fetches a single entity.

Same as get/3, but raises Exact.Error on failure.

Fetches one page of a collection.

Same as list/3, but raises Exact.Error on failure.

Builds a client.

Same as post/4, but raises Exact.Error on failure.

Updates an entity, sending only the fields you pass.

Same as put/4, but raises Exact.Error on failure.

Returns a copy of client scoped to division.

Runs a request and returns the raw Req.Response.

Same as request/2, but raises Exact.Error on failure.

Streams every record of a collection, following the __next cursor.

Types

t()

@type t() :: %Exact.Client{
  base_url: String.t(),
  division: integer() | String.t() | nil,
  region: Exact.Region.t() | nil,
  req: Req.Request.t()
}

Functions

delete(client, path, opts \\ [])

@spec delete(t(), String.t(), keyword()) :: {:ok, nil} | {:error, Exact.Error.t()}

Deletes an entity. Returns {:ok, nil}.

delete!(client, path, opts \\ [])

@spec delete!(t(), String.t(), keyword()) :: nil

Same as delete/3, but raises Exact.Error on failure.

get(client, path, opts \\ [])

@spec get(t(), String.t(), keyword()) ::
  {:ok, map() | nil} | {:error, Exact.Error.t()}

Fetches a single entity.

Accepts the Exact.Query options, most usefully :select and :expand.

id = "11111111-2222-3333-4444-555555555555"
Exact.Client.get(client, "crm/Accounts(" <> Exact.Query.guid(id) <> ")")

get!(client, path, opts \\ [])

@spec get!(t(), String.t(), keyword()) :: map() | nil

Same as get/3, but raises Exact.Error on failure.

list(client, path, opts \\ [])

@spec list(t(), String.t(), keyword()) ::
  {:ok, Exact.Page.t()} | {:error, Exact.Error.t()}

Fetches one page of a collection.

Exact Online returns 60 records per page by default and 1000 for the bulk and sync endpoints. Follow page.next or use stream/3.

{:ok, page} = Exact.Client.list(client, "crm/Accounts", select: ["ID", "Name"], top: 10)

list!(client, path, opts \\ [])

@spec list!(t(), String.t(), keyword()) :: Exact.Page.t()

Same as list/3, but raises Exact.Error on failure.

new(opts \\ [])

@spec new(keyword()) :: t()

Builds a client.

Options

  • :region - [:es, :be, :nl, :fr, :de, :uk, :us], defaults to :nl
  • :base_url - overrides :region
  • :division - the division code every relative path is scoped to. Discover it with Exact.System.Me.division/1
  • :access_token - use a token directly, without refreshing
  • :token_store - a module implementing Exact.TokenStore
  • :token_key - the key the token is stored under, defaults to :default
  • :credentials - the options Exact.OAuth.refresh/2 needs, required together with :token_store
  • :refresh_skew - refresh this many seconds before the token expires, defaults to 60
  • :req_options - merged into the underlying Req.Request, for things like receive_timeout: or a plug: stub in tests

post(client, path, body, opts \\ [])

@spec post(t(), String.t(), map(), keyword()) ::
  {:ok, map() | nil} | {:error, Exact.Error.t()}

Creates an entity.

The body is sent as JSON. Exact Online answers with the created entity, which is the only way to learn its ID.

{:ok, account} = Exact.Client.post(client, "crm/Accounts", %{"Name" => "Paradiso"})

post!(client, path, body, opts \\ [])

@spec post!(t(), String.t(), map(), keyword()) :: map() | nil

Same as post/4, but raises Exact.Error on failure.

put(client, path, body, opts \\ [])

@spec put(t(), String.t(), map(), keyword()) ::
  {:ok, map() | nil} | {:error, Exact.Error.t()}

Updates an entity, sending only the fields you pass.

Exact Online answers with 204 No Content, so this returns {:ok, nil}.

put!(client, path, body, opts \\ [])

@spec put!(t(), String.t(), map(), keyword()) :: map() | nil

Same as put/4, but raises Exact.Error on failure.

put_division(client, division)

@spec put_division(t(), integer() | String.t()) :: t()

Returns a copy of client scoped to division.

request(client, opts)

@spec request(t(), keyword()) :: {:ok, Req.Response.t()} | {:error, Exact.Error.t()}

Runs a request and returns the raw Req.Response.

Every other function funnels through here. Reach for it when you need response headers, or a method the wrappers do not cover.

request!(client, opts)

@spec request!(t(), keyword()) :: Req.Response.t()

Same as request/2, but raises Exact.Error on failure.

stream(client, path, opts \\ [])

@spec stream(t(), String.t(), keyword()) :: Enumerable.t()

Streams every record of a collection, following the __next cursor.

The stream is lazy and only requests a page when it is consumed, so it stays cheap to Stream.take/2 from it. It raises Exact.Error on failure, because a stream has nowhere to put an error tuple.

client
|> Exact.Client.stream("crm/Accounts", select: ["ID", "Name"])
|> Stream.map(& &1["Name"])
|> Enum.take(500)