README.md

# Linear

Elixir client for the [Linear](https://linear.app) GraphQL API.

Provides a clean, functional interface for querying and mutating Linear data -- issues, comments, projects, workflow states, teams, and more. Includes cursor-based auto-pagination and supports both API key and OAuth authentication.

## Installation

```elixir
def deps do
  [
    {:linear_client, "~> 0.1.0"}
  ]
end
```

## Quick Start

```elixir
client = Linear.client(api_key: "lin_api_...")

# Fetch the authenticated user
{:ok, viewer} = Linear.viewer(client)
IO.puts(viewer["name"])

# List issues for a team
{:ok, issues} = Linear.list_issues(client, team_id: "TEAM-UUID")
Enum.each(issues, &IO.puts(&1["title"]))

# Create an issue
{:ok, issue} = Linear.create_issue(client, %{
  "teamId" => "TEAM-UUID",
  "title" => "Fix login bug",
  "description" => "Users can't log in with SSO"
})

# Transition an issue by state name
{:ok, _} = Linear.transition_issue(client, "ABC-123", "In Progress")
```

## Authentication

Two modes are supported:

```elixir
# Personal API key (Authorization: <key>)
client = Linear.client(api_key: "lin_api_...")

# OAuth bearer token (Authorization: Bearer <token>)
client = Linear.client(access_token: "oauth_access_token")
```

Create API keys at [Linear Settings > Security & Access](https://linear.app/settings/account/security).

## API Reference

### Queries

```elixir
# Current user
{:ok, viewer} = Linear.viewer(client)

# Teams
{:ok, teams} = Linear.list_teams(client)

# Issues (auto-paginated)
{:ok, issues} = Linear.list_issues(client,
  team_id: "...",
  state_names: ["In Progress", "Todo"],
  assignee_id: "...",
  project_id: "...",
  first: 50,
  include_archived: false
)

# Single issue (UUID or shorthand like "ABC-123")
{:ok, issue} = Linear.get_issue(client, "ABC-123")

# Workflow states
{:ok, states} = Linear.list_workflow_states(client, team_id: "...")

# Projects (auto-paginated)
{:ok, projects} = Linear.list_projects(client)
```

### Mutations

```elixir
# Create issue
{:ok, issue} = Linear.create_issue(client, %{
  "teamId" => "...",
  "title" => "New feature",
  "description" => "Markdown description",
  "assigneeId" => "...",
  "priority" => 2,
  "labelIds" => ["label-uuid"]
})

# Update issue
{:ok, issue} = Linear.update_issue(client, "ABC-123", %{
  "title" => "Updated title",
  "priority" => 1
})

# Create comment
{:ok, comment} = Linear.create_comment(client, "issue-uuid", "This is fixed in PR #42")

# Transition issue state (resolves state name to ID automatically)
{:ok, issue} = Linear.transition_issue(client, "ABC-123", "Done")
```

### Raw GraphQL

For queries not covered by the built-in helpers:

```elixir
{:ok, data} = Linear.query(client,
  "query($id: String!) { issue(id: $id) { title state { name } } }",
  variables: %{"id" => "ABC-123"}
)

{:ok, data} = Linear.mutate(client,
  "mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success } }",
  variables: %{"id" => "ABC-123", "input" => %{"title" => "New title"}}
)
```

### Auto-Pagination

`list_issues/2` and `list_projects/2` automatically follow cursors. You can also use pagination directly:

```elixir
{:ok, all_nodes} = Linear.paginate(fn cursor ->
  Linear.query(client, my_query, variables: %{"after" => cursor, "first" => 50})
  |> case do
    {:ok, %{"myConnection" => connection}} -> {:ok, connection}
    error -> error
  end
end)
```

## Error Handling

All functions return `{:ok, result}` or `{:error, reason}`:

```elixir
case Linear.get_issue(client, "ABC-999") do
  {:ok, issue} ->
    IO.puts(issue["title"])

  {:error, {:graphql_errors, errors, _data}} ->
    IO.puts("GraphQL error: #{hd(errors)["message"]}")

  {:error, {:http_status, 401}} ->
    IO.puts("Authentication failed")

  {:error, {:request_failed, reason}} ->
    IO.puts("Network error: #{inspect(reason)}")

  {:error, {:state_not_found, name}} ->
    IO.puts("No workflow state named #{name}")
end
```

## Configuration Options

| Option | Default | Description |
|--------|---------|-------------|
| `:api_key` | `nil` | Linear personal API key |
| `:access_token` | `nil` | OAuth2 bearer token |
| `:endpoint` | `"https://api.linear.app/graphql"` | GraphQL API endpoint |
| `:timeout` | `30_000` | HTTP timeout in milliseconds |

## Architecture

```
Linear (public API facade)
├── Linear.Client     — HTTP transport, auth, request/response handling
├── Linear.Queries    — Pre-built read queries (viewer, teams, issues, etc.)
├── Linear.Mutations  — Pre-built write mutations (create, update, transition)
└── Linear.Pagination — Cursor-based auto-pagination for connections
```

## Development

```bash
mix deps.get
mix test
mix credo --strict
```

## License

MIT — see [LICENSE](LICENSE).