defmodule Linear do @moduledoc """ Elixir client for the [Linear](https://linear.app) GraphQL API. ## Quick start client = Linear.client(api_key: "lin_api_...") {:ok, viewer} = Linear.viewer(client) IO.inspect(viewer["name"]) {:ok, issues} = Linear.list_issues(client, team_id: "TEAM-UUID") Enum.each(issues, &IO.puts(&1["title"])) ## Raw GraphQL {:ok, data} = Linear.query(client, "query { viewer { id name email } }") {:ok, data} = Linear.query(client, "query($id: String!) { issue(id: $id) { title state { name } } }", variables: %{"id" => "ABC-123"} ) ## Authentication Two auth modes are supported: - **API key**: `Linear.client(api_key: "lin_api_...")` -- sends `Authorization: ` - **OAuth bearer**: `Linear.client(access_token: "oauth_token")` -- sends `Authorization: Bearer ` """ alias Linear.Client @type client :: Client.t() @doc """ Create a new Linear API client. ## Options * `:api_key` - Linear personal API key * `:access_token` - OAuth2 access token (bearer auth) * `:endpoint` - API endpoint (default `"https://api.linear.app/graphql"`) * `:timeout` - HTTP request timeout in ms (default `30_000`) """ @spec client(keyword()) :: client() def client(opts \\ []) do Client.new(opts) end @doc """ Execute a raw GraphQL query or mutation. ## Options * `:variables` - GraphQL variables map (default `%{}`) * `:operation_name` - optional GraphQL operation name """ @spec query(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} def query(client, query_string, opts \\ []) do Client.query(client, query_string, opts) end @doc """ Execute a GraphQL mutation. Alias for `query/3` with clearer intent. """ @spec mutate(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} def mutate(client, mutation, opts \\ []) do Client.query(client, mutation, opts) end @doc """ Fetch the authenticated user. Returns the `viewer` object with `id`, `name`, `email`, and `displayName`. """ @spec viewer(client(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate viewer(client, opts \\ []), to: Linear.Queries @doc """ List teams in the workspace. Returns a list of team nodes. """ @spec list_teams(client(), keyword()) :: {:ok, [map()]} | {:error, term()} defdelegate list_teams(client, opts \\ []), to: Linear.Queries @doc """ List issues, optionally filtered by team or project. ## Options * `:team_id` - filter by team UUID * `:project_id` - filter by project UUID * `:state_names` - filter by workflow state names (list of strings) * `:assignee_id` - filter by assignee UUID * `:first` - page size (default `50`) * `:after` - pagination cursor * `:include_archived` - include archived issues (default `false`) """ @spec list_issues(client(), keyword()) :: {:ok, [map()]} | {:error, term()} defdelegate list_issues(client, opts \\ []), to: Linear.Queries @doc """ Fetch a single issue by ID (UUID or shorthand like `"ABC-123"`). """ @spec get_issue(client(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate get_issue(client, issue_id, opts \\ []), to: Linear.Queries @doc """ Create an issue. ## Required fields in `attrs` * `"teamId"` - team UUID * `"title"` - issue title ## Optional fields * `"description"` - markdown body * `"assigneeId"` - assignee UUID * `"stateId"` - workflow state UUID * `"priority"` - integer priority (0=none, 1=urgent, 4=low) * `"labelIds"` - list of label UUIDs * `"projectId"` - project UUID """ @spec create_issue(client(), map(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate create_issue(client, attrs, opts \\ []), to: Linear.Mutations @doc """ Update an existing issue. `issue_id` can be a UUID or shorthand like `"ABC-123"`. `attrs` is a map of fields to update (same keys as `create_issue/2`). """ @spec update_issue(client(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate update_issue(client, issue_id, attrs, opts \\ []), to: Linear.Mutations @doc """ Create a comment on an issue. """ @spec create_comment(client(), String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate create_comment(client, issue_id, body, opts \\ []), to: Linear.Mutations @doc """ Update an issue's workflow state by state name. Resolves the state name to a state ID via the issue's team, then applies the update. """ @spec transition_issue(client(), String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()} defdelegate transition_issue(client, issue_id, state_name, opts \\ []), to: Linear.Mutations @doc """ List workflow states, optionally filtered by team. ## Options * `:team_id` - filter by team UUID """ @spec list_workflow_states(client(), keyword()) :: {:ok, [map()]} | {:error, term()} defdelegate list_workflow_states(client, opts \\ []), to: Linear.Queries @doc """ List projects in the workspace. ## Options * `:first` - page size (default `50`) * `:after` - pagination cursor * `:include_archived` - include archived (default `false`) """ @spec list_projects(client(), keyword()) :: {:ok, [map()]} | {:error, term()} defdelegate list_projects(client, opts \\ []), to: Linear.Queries @doc """ Auto-paginate a query that returns a connection with `nodes` and `pageInfo`. Calls `query_fn` repeatedly, following `endCursor`, and accumulates all nodes. `query_fn` receives an `after` cursor (or `nil` for the first page) and must return `{:ok, %{"nodes" => [...], "pageInfo" => %{...}}}` or `{:error, reason}`. """ @spec paginate((String.t() | nil -> {:ok, map()} | {:error, term()})) :: {:ok, [map()]} | {:error, term()} defdelegate paginate(query_fn), to: Linear.Pagination end