Packages

Elixir client for the Microsoft Graph API

Current section

Files

Jump to
keen_microsoft_graphapi lib graph_api batch.ex
Raw

lib/graph_api/batch.ex

defmodule GraphApi.Batch do
@moduledoc """
JSON batch requests for Microsoft Graph API.
Allows sending up to 20 requests in a single HTTP call via `POST /$batch`.
Build batch requests using `_query` functions from resource modules.
## Examples
alias GraphApi.{Batch, OData, Users, Groups}
alias GraphApi.Schema.{User, Group}
query = OData.new() |> OData.select(["id", "displayName"]) |> OData.top(5)
{:ok, responses} =
Batch.new()
|> Batch.add("1", Users.list_query(query: query, as: User))
|> Batch.add("2", Groups.get_query("group-id", as: Group))
|> Batch.add("3", Users.create_query(%{"displayName" => "New"}, as: User))
|> Batch.execute(client: client)
%{status: 200, body: %{"value" => users}} = Batch.get(responses, "1")
"""
alias GraphApi.OData
alias GraphApi.Resource
defmodule Request do
@moduledoc """
A deferred Graph API request, created by `_query` functions on resource modules.
"""
defstruct [:method, :url, :body, :headers, :as, query: nil, depends_on: nil]
@type t :: %__MODULE__{
method: String.t(),
url: String.t(),
body: map() | binary() | nil,
headers: map() | nil,
query: OData.t() | map() | nil,
as: module() | nil,
depends_on: [String.t()] | nil
}
end
defmodule Entry do
@moduledoc false
defstruct [:id, :request, depends_on: nil]
end
@type t :: %__MODULE__{entries: [Entry.t()]}
defstruct entries: []
@max_requests 20
@doc """
Creates a new empty batch.
"""
@spec new() :: t()
def new, do: %__MODULE__{}
@doc """
Adds a request to the batch with the given ID.
## Options
* `:depends_on` - List of request IDs that must complete before this one.
## Examples
Batch.new()
|> Batch.add("1", Users.list_query(as: User))
|> Batch.add("2", Users.create_query(attrs, as: User), depends_on: ["1"])
"""
@spec add(t(), String.t(), Request.t(), keyword()) :: t()
def add(%__MODULE__{} = batch, id, %Request{} = request, add_opts \\ []) do
depends_on = Keyword.get(add_opts, :depends_on) || request.depends_on
entry = %Entry{id: id, request: request, depends_on: depends_on}
%{batch | entries: batch.entries ++ [entry]}
end
@doc """
Executes the batch, sending all requests in a single `POST /$batch` call.
Returns `{:ok, [response]}` where each response has `:id`, `:status`, `:headers`, and `:body`.
Response bodies are automatically cast using the `:as` schema specified on each request.
## Options
Same as other resource functions: `:client`, `:access_token`, `:api_version`
"""
@spec execute(t(), keyword()) :: {:ok, [map()]} | {:error, term()}
def execute(%__MODULE__{entries: entries}, opts \\ []) do
count = length(entries)
cond do
count == 0 ->
{:ok, []}
count > @max_requests ->
{:error, "Batch limited to #{@max_requests} requests, got #{count}"}
true ->
as_map = Map.new(entries, fn %Entry{id: id, request: req} -> {id, req.as} end)
batch_body = %{
"requests" => Enum.map(entries, &serialize_entry/1)
}
case Resource.post("/$batch", batch_body, opts) do
{:ok, %{"responses" => responses}} ->
cast_responses =
responses
|> Enum.sort_by(& &1["id"])
|> Enum.map(&normalize_response(&1, as_map))
{:ok, cast_responses}
{:ok, other} ->
{:error, "Unexpected batch response: #{inspect(other)}"}
{:error, _} = err ->
err
end
end
end
@doc """
Finds a response by its request ID.
## Examples
{:ok, responses} = Batch.execute(batch, client: client)
%{status: 200, body: user} = Batch.get(responses, "1")
"""
@spec get([map()], String.t()) :: map() | nil
def get(responses, id) when is_list(responses) do
Enum.find(responses, &(&1.id == id))
end
# -- Serialization --
defp serialize_entry(%Entry{id: id, request: req, depends_on: depends_on}) do
url = build_url(req.url, req.query)
entry = %{"id" => id, "method" => req.method, "url" => url}
entry =
if req.body do
Map.put(entry, "body", req.body)
else
entry
end
entry =
if req.headers do
Map.put(entry, "headers", req.headers)
else
entry
end
entry =
if depends_on do
Map.put(entry, "dependsOn", depends_on)
else
entry
end
entry
end
defp build_url(url, nil), do: url
defp build_url(url, %OData{} = query) do
params = OData.to_params(query)
build_url(url, params)
end
defp build_url(url, params) when is_map(params) and map_size(params) == 0, do: url
defp build_url(url, params) when is_map(params) do
qs =
params
|> Enum.sort_by(fn {k, _} -> k end)
|> Enum.map(fn {k, v} -> "#{URI.encode_www_form(k)}=#{URI.encode_www_form(to_string(v))}" end)
|> Enum.join("&")
"#{url}?#{qs}"
end
# -- Response normalization --
defp normalize_response(resp, as_map) do
id = resp["id"]
status = resp["status"]
headers = resp["headers"] || %{}
body = resp["body"]
as_module = Map.get(as_map, id)
body = maybe_cast_body(body, status, as_module)
%{id: id, status: status, headers: headers, body: body}
end
defp maybe_cast_body(body, status, as_module)
when is_map(body) and status >= 200 and status < 300 and not is_nil(as_module) do
Resource.maybe_cast({:ok, body}, as_module)
|> case do
{:ok, cast} -> cast
_ -> body
end
end
defp maybe_cast_body(body, _status, _as_module), do: body
end