Class: CollavreLinear::Client

Inherits:
Object
  • Object
show all
Defined in:
app/services/collavre_linear/client.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

DEFAULT_ENDPOINT =
"https://api.linear.app/graphql"
ISSUE_CREATE =

IssueCreateInput fields used: teamId, title, description, parentId, projectId, stateId, assigneeId, labelIds, priority

<<~GQL.freeze
  mutation IssueCreate($input: IssueCreateInput!) {
    issueCreate(input: $input) {
      success
      issue {
        id
        identifier
        updatedAt
      }
    }
  }
GQL
ISSUE_UPDATE =

IssueUpdateInput fields used: title, description, parentId, projectId, stateId, assigneeId, labelIds, priority (any subset passed as **fields)

<<~GQL.freeze
  mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
    issueUpdate(id: $id, input: $input) {
      success
      issue {
        id
        identifier
        updatedAt
      }
    }
  }
GQL
PROJECT_CREATE =

ProjectCreateInput fields used: name, teamIds

<<~GQL.freeze
  mutation ProjectCreate($input: ProjectCreateInput!) {
    projectCreate(input: $input) {
      success
      project {
        id
      }
    }
  }
GQL
PROJECT_UPDATE =

ProjectUpdateInput fields used: name (any subset passed as **fields)

<<~GQL.freeze
  mutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
    projectUpdate(id: $id, input: $input) {
      success
      project {
        id
      }
    }
  }
GQL
COMMENT_CREATE =

CommentCreateInput fields used: issueId, body

<<~GQL.freeze
  mutation CommentCreate($input: CommentCreateInput!) {
    commentCreate(input: $input) {
      success
      comment {
        id
        updatedAt
      }
    }
  }
GQL
COMMENT_UPDATE =

CommentUpdateInput fields used: body

<<~GQL.freeze
  mutation CommentUpdate($id: String!, $input: CommentUpdateInput!) {
    commentUpdate(id: $id, input: $input) {
      success
      comment {
        id
        updatedAt
      }
    }
  }
GQL
COMMENT_DELETE =
<<~GQL.freeze
  mutation CommentDelete($id: String!) {
    commentDelete(id: $id) {
      success
    }
  }
GQL
WEBHOOK_CREATE =

WebhookCreateInput fields used: url, secret, teamId, resourceTypes

<<~GQL.freeze
  mutation WebhookCreate($input: WebhookCreateInput!) {
    webhookCreate(input: $input) {
      success
      webhook {
        id
      }
    }
  }
GQL
ISSUE_ARCHIVE =

Archive (soft-delete) a Linear issue by id.

<<~GQL.freeze
  mutation IssueArchive($id: String!) {
    issueArchive(id: $id) {
      success
    }
  }
GQL
WEBHOOK_DELETE =

Delete (deregister) a Linear webhook by id.

<<~GQL.freeze
  mutation WebhookDelete($id: String!) {
    webhookDelete(id: $id) {
      success
    }
  }
GQL
VIEWER =

Viewer identity only. Linear's schema exposes no Query field for the OAuth app actor id of the current token (applicationWithAuthorization { appActor } is not valid — that field is absent and its type carries no appActor), so app_actor_id stays nil and EchoGuard no-ops. Loops are already prevented independently by InboundApplier (skip_linear_sync on inbound writes, IssueLink dedup on create, content_hash on update).

<<~GQL.freeze
  query Viewer {
    viewer {
      id
      organization {
        id
      }
    }
  }
GQL
TEAMS =

List the workspace teams the token can see (for the link-a-project picker).

<<~GQL.freeze
  query Teams {
    teams(first: 250) {
      nodes {
        id
        name
        key
      }
    }
  }
GQL
PROJECTS =

List projects with their owning team ids so the picker can scope projects to the chosen team (a Linear project may belong to more than one team).

<<~GQL.freeze
  query Projects {
    projects(first: 250) {
      nodes {
        id
        name
        teams {
          nodes { id }
        }
      }
    }
  }
GQL
WORKFLOW_STATES =

List workflow states with their owning team id and type so the link modal can offer a "done state" combobox scoped to the chosen team. type is Linear's state category (triage/backlog/unstarted/started/completed/ canceled) — the picker defaults to the team's "completed" state.

<<~GQL.freeze
  query WorkflowStates {
    workflowStates(first: 250) {
      nodes {
        id
        name
        type
        position
        team {
          id
        }
      }
    }
  }
GQL
ISSUE_QUERY =

Fetch a single issue's current workflow state (for seeding the pre-done snapshot when a leaf is completed with no locally-known Linear state).

<<~GQL.freeze
  query Issue($id: String!) {
    issue(id: $id) {
      id
      state {
        id
        name
        type
      }
    }
  }
GQL

Instance Method Summary collapse

Constructor Details

#initialize(account) ⇒ Client

Returns a new instance of Client.



229
230
231
232
# File 'app/services/collavre_linear/client.rb', line 229

def initialize()
  @account = 
  @endpoint = resolve_endpoint
end

Instance Method Details

#archive_issue(id) ⇒ Boolean

Archive a Linear issue (soft-delete).

Parameters:

  • id (String)

    Linear issue UUID

Returns:

  • (Boolean)

    success



301
302
303
304
# File 'app/services/collavre_linear/client.rb', line 301

def archive_issue(id)
  data = post!(ISSUE_ARCHIVE, { id: id })
  data.dig("issueArchive", "success") == true
end

#create_comment(issue_id:, body:) ⇒ Hash

Create a comment on a Linear issue.

Returns:

  • (Hash)

    with :id



308
309
310
311
312
313
# File 'app/services/collavre_linear/client.rb', line 308

def create_comment(issue_id:, body:)
  input = { issueId: issue_id, body: body }
  data  = post!(COMMENT_CREATE, { input: input })
  node  = data.dig("commentCreate", "comment")
  symbolize(node)
end

#create_issue(team_id:, title:, description: nil, parent_id: nil, project_id: nil, state_id: nil, assignee_id: nil, label_ids: [], priority: nil) ⇒ Hash

Create a Linear issue.

Returns:

  • (Hash)

    with :id and :identifier



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'app/services/collavre_linear/client.rb', line 249

def create_issue(team_id:, title:, description: nil, parent_id: nil,
                 project_id: nil, state_id: nil, assignee_id: nil,
                 label_ids: [], priority: nil)
  input = { teamId: team_id, title: title }.tap do |h|
    h[:description] = description if description
    h[:parentId]    = parent_id   if parent_id
    h[:projectId]   = project_id  if project_id
    h[:stateId]     = state_id    if state_id
    h[:assigneeId]  = assignee_id if assignee_id
    h[:labelIds]    = label_ids   if label_ids.any?
    h[:priority]    = priority    if priority
  end

  data = post!(ISSUE_CREATE, { input: input })
  node = data.dig("issueCreate", "issue")
  symbolize(node)
end

#create_project(name:, team_ids:) ⇒ Hash

Create a Linear project.

Returns:

  • (Hash)

    with :id



280
281
282
283
284
285
# File 'app/services/collavre_linear/client.rb', line 280

def create_project(name:, team_ids:)
  input = { name: name, teamIds: team_ids }
  data  = post!(PROJECT_CREATE, { input: input })
  node  = data.dig("projectCreate", "project")
  symbolize(node)
end

#delete_comment(id) ⇒ Boolean

Delete a Linear comment.

Returns:

  • (Boolean)

    success



325
326
327
328
# File 'app/services/collavre_linear/client.rb', line 325

def delete_comment(id)
  data = post!(COMMENT_DELETE, { id: id })
  data.dig("commentDelete", "success") == true
end

#delete_webhook(id) ⇒ Boolean

Delete (deregister) a webhook from Linear.

Parameters:

  • id (String)

    Linear webhook UUID

Returns:

  • (Boolean)

    success



143
144
145
146
# File 'app/services/collavre_linear/client.rb', line 143

def delete_webhook(id)
  data = post!(WEBHOOK_DELETE, { id: id })
  data.dig("webhookDelete", "success") == true
end

#fetch_issue_state(id) ⇒ Hash?

Fetch a single Linear issue's current workflow state.

Parameters:

  • id (String)

    Linear issue UUID

Returns:

  • (Hash, nil)

    =>, "name" =>, "type" => (string keys, matching the shape stored under data["state"]), or nil when the issue or its state is absent.



239
240
241
242
243
244
245
# File 'app/services/collavre_linear/client.rb', line 239

def fetch_issue_state(id)
  data  = post!(ISSUE_QUERY, { id: id })
  state = data.dig("issue", "state")
  return nil if state.nil?

  state.slice("id", "name", "type")
end

#list_projectsArray<Hash>

List projects for the link picker, each with the ids of its owning teams so the UI can scope projects to the selected team.

Returns:

  • (Array<Hash>)

    each with :id, :name, :team_ids



354
355
356
357
358
359
360
361
362
363
364
# File 'app/services/collavre_linear/client.rb', line 354

def list_projects
  data  = post!(PROJECTS, {})
  nodes = data.dig("projects", "nodes") || []
  nodes.map do |n|
    {
      id:       n["id"],
      name:     n["name"],
      team_ids: (n.dig("teams", "nodes") || []).map { |t| t["id"] }
    }
  end
end

#list_teamsArray<Hash>

List workspace teams for the link picker.

Returns:

  • (Array<Hash>)

    each with :id, :name, :key



345
346
347
348
349
# File 'app/services/collavre_linear/client.rb', line 345

def list_teams
  data  = post!(TEAMS, {})
  nodes = data.dig("teams", "nodes") || []
  nodes.map { |n| symbolize(n) }
end

#list_workflow_statesArray<Hash>

List workflow states for the "done state" picker, each with its owning team id and category type so the UI can scope states to the selected team and default to the completed one.

Returns:

  • (Array<Hash>)

    each with :id, :name, :type, :position, :team_id



370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'app/services/collavre_linear/client.rb', line 370

def list_workflow_states
  data  = post!(WORKFLOW_STATES, {})
  nodes = data.dig("workflowStates", "nodes") || []
  nodes.map do |n|
    {
      id:       n["id"],
      name:     n["name"],
      type:     n["type"],
      position: n["position"],
      team_id:  n.dig("team", "id")
    }
  end
end

#register_webhook(url:, secret:, team_id:, resource_types:) ⇒ Hash

Register a webhook with Linear. WebhookCreateInput fields: url, secret, teamId, resourceTypes

Returns:

  • (Hash)

    with :id



387
388
389
390
391
392
# File 'app/services/collavre_linear/client.rb', line 387

def register_webhook(url:, secret:, team_id:, resource_types:)
  input = { url: url, secret: secret, teamId: team_id, resourceTypes: resource_types }
  data  = post!(WEBHOOK_CREATE, { input: input })
  node  = data.dig("webhookCreate", "webhook")
  symbolize(node)
end

#update_comment(id:, body:) ⇒ Hash

Edit an existing Linear comment's body.

Returns:

  • (Hash)

    with :id



317
318
319
320
321
# File 'app/services/collavre_linear/client.rb', line 317

def update_comment(id:, body:)
  data = post!(COMMENT_UPDATE, { id: id, input: { body: body } })
  node = data.dig("commentUpdate", "comment")
  symbolize(node)
end

#update_issue(id, **fields) ⇒ Hash

Update a Linear issue by id.

Parameters:

  • id (String)

    Linear issue UUID

  • fields (Hash)

    keyword args mapping to IssueUpdateInput fields

Returns:

  • (Hash)

    with :id and :identifier



271
272
273
274
275
276
# File 'app/services/collavre_linear/client.rb', line 271

def update_issue(id, **fields)
  input = camelize_keys(fields)
  data  = post!(ISSUE_UPDATE, { id: id, input: input })
  node  = data.dig("issueUpdate", "issue")
  symbolize(node)
end

#update_project(id, **fields) ⇒ Hash

Update a Linear project by id.

Parameters:

  • id (String)

    Linear project UUID

  • fields (Hash)

    keyword args mapping to ProjectUpdateInput fields

Returns:

  • (Hash)

    with :id



291
292
293
294
295
296
# File 'app/services/collavre_linear/client.rb', line 291

def update_project(id, **fields)
  input = camelize_keys(fields)
  data  = post!(PROJECT_UPDATE, { id: id, input: input })
  node  = data.dig("projectUpdate", "project")
  symbolize(node)
end

#viewer_and_app_actorHash

Fetch the authenticated viewer identity. app_actor_id is intentionally nil (no Linear query exposes it for the current token); EchoGuard degrades to a no-op — see VIEWER above.

Returns:

  • (Hash)

    with :user_id, :app_actor_id, :organization_id



334
335
336
337
338
339
340
341
# File 'app/services/collavre_linear/client.rb', line 334

def viewer_and_app_actor
  data = post!(VIEWER, {})
  {
    user_id:         data.dig("viewer", "id"),
    organization_id: data.dig("viewer", "organization", "id"),
    app_actor_id:    nil
  }
end