Packages

Elixir client for the Linear GraphQL API.

Current section

Files

Jump to
linear_client lib linear client.ex
Raw

lib/linear/client.ex

defmodule Linear.Client do
@moduledoc """
HTTP transport for the Linear GraphQL API.
Handles authentication, request construction, and response parsing.
Not typically used directly -- see `Linear` for the public API.
"""
require Logger
@default_endpoint "https://api.linear.app/graphql"
@default_timeout 30_000
@max_error_body_bytes 1_000
@type t :: %__MODULE__{
endpoint: String.t(),
headers: [{String.t(), String.t()}],
timeout: pos_integer()
}
@enforce_keys [:endpoint, :headers, :timeout]
defstruct [:endpoint, :headers, :timeout]
@spec new(keyword()) :: t()
def new(opts \\ []) do
endpoint = Keyword.get(opts, :endpoint, @default_endpoint)
timeout = Keyword.get(opts, :timeout, @default_timeout)
auth_header =
cond do
key = Keyword.get(opts, :api_key) ->
{"Authorization", key}
token = Keyword.get(opts, :access_token) ->
{"Authorization", "Bearer #{token}"}
true ->
nil
end
headers =
[auth_header, {"Content-Type", "application/json"}]
|> Enum.reject(&is_nil/1)
%__MODULE__{endpoint: endpoint, headers: headers, timeout: timeout}
end
@spec query(t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def query(%__MODULE__{} = client, query_string, opts \\ []) do
variables = Keyword.get(opts, :variables, %{})
operation_name = Keyword.get(opts, :operation_name)
request_fn = Keyword.get(opts, :request_fn, &default_request/3)
payload =
%{"query" => query_string, "variables" => variables}
|> maybe_put("operationName", operation_name)
case request_fn.(client, payload, opts) do
{:ok, %{status: 200, body: %{"errors" => errors, "data" => data}}} when is_list(errors) and errors != [] ->
{:error, {:graphql_errors, errors, data}}
{:ok, %{status: 200, body: %{"errors" => errors}}} when is_list(errors) and errors != [] ->
{:error, {:graphql_errors, errors, nil}}
{:ok, %{status: 200, body: %{"data" => data}}} ->
{:ok, data}
{:ok, %{status: status} = response} ->
Logger.error("Linear API returned status=#{status}#{error_context(payload, response)}")
{:error, {:http_status, status}}
{:error, reason} ->
Logger.error("Linear API request failed: #{inspect(reason)}")
{:error, {:request_failed, reason}}
end
end
defp default_request(%__MODULE__{} = client, payload, _opts) do
Req.post(client.endpoint,
headers: client.headers,
json: payload,
connect_options: [timeout: client.timeout]
)
end
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value) when is_binary(value) and value != "", do: Map.put(map, key, value)
defp maybe_put(map, _key, _value), do: map
defp error_context(payload, response) do
op =
case Map.get(payload, "operationName") do
name when is_binary(name) and name != "" -> " operation=#{name}"
_ -> ""
end
body =
response
|> Map.get(:body)
|> summarize_body()
"#{op} body=#{body}"
end
defp summarize_body(body) when is_binary(body) do
body
|> String.replace(~r/\s+/, " ")
|> String.trim()
|> String.slice(0, @max_error_body_bytes)
|> inspect()
end
defp summarize_body(body) do
body
|> inspect(limit: 20, printable_limit: @max_error_body_bytes)
|> String.slice(0, @max_error_body_bytes)
end
end