feat: Add APIs for linear integration (#9346)

This commit is contained in:
Muhsin Keloth
2024-05-22 13:37:58 +05:30
committed by GitHub
parent 0d13c11c44
commit 023b3ad507
16 changed files with 1308 additions and 24 deletions

View File

@@ -0,0 +1,82 @@
class Integrations::Linear::ProcessorService
pattr_initialize [:account!]
def teams
response = linear_client.teams
return { error: response[:error] } if response[:error]
{ data: response['teams']['nodes'].map(&:as_json) }
end
def team_entities(team_id)
response = linear_client.team_entities(team_id)
return response if response[:error]
{
data: {
users: response['users']['nodes'].map(&:as_json),
projects: response['projects']['nodes'].map(&:as_json),
states: response['workflowStates']['nodes'].map(&:as_json),
labels: response['issueLabels']['nodes'].map(&:as_json)
}
}
end
def create_issue(params)
response = linear_client.create_issue(params)
return response if response[:error]
{
data: { id: response['issueCreate']['issue']['id'],
title: response['issueCreate']['issue']['title'] }
}
end
def link_issue(link, issue_id, title)
response = linear_client.link_issue(link, issue_id, title)
return response if response[:error]
{
data: {
id: issue_id,
link: link,
link_id: response.with_indifferent_access[:attachmentLinkURL][:attachment][:id]
}
}
end
def unlink_issue(link_id)
response = linear_client.unlink_issue(link_id)
return response if response[:error]
{
data: { link_id: link_id }
}
end
def search_issue(term)
response = linear_client.search_issue(term)
return response if response[:error]
{ data: response['searchIssues']['nodes'].map(&:as_json) }
end
def linked_issues(url)
response = linear_client.linked_issues(url)
return response if response[:error]
{ data: response['attachmentsForURL']['nodes'].map(&:as_json) }
end
private
def linear_hook
@linear_hook ||= account.hooks.find_by!(app_id: 'linear')
end
def linear_client
credentials = linear_hook.settings
@linear_client ||= Linear.new(credentials['api_key'])
end
end

120
lib/linear.rb Normal file
View File

@@ -0,0 +1,120 @@
class Linear
BASE_URL = 'https://api.linear.app/graphql'.freeze
PRIORITY_LEVELS = (0..4).to_a
def initialize(api_key)
@api_key = api_key
raise ArgumentError, 'Missing Credentials' if api_key.blank?
end
def teams
query = {
query: Linear::Queries::TEAMS_QUERY
}
response = post(query)
process_response(response)
end
def team_entities(team_id)
raise ArgumentError, 'Missing team id' if team_id.blank?
query = {
query: Linear::Queries.team_entities_query(team_id)
}
response = post(query)
process_response(response)
end
def search_issue(term)
raise ArgumentError, 'Missing search term' if term.blank?
query = {
query: Linear::Queries.search_issue(term)
}
response = post(query)
process_response(response)
end
def linked_issues(url)
raise ArgumentError, 'Missing link' if url.blank?
query = {
query: Linear::Queries.linked_issues(url)
}
response = post(query)
process_response(response)
end
def create_issue(params)
validate_team_and_title(params)
validate_priority(params[:priority])
validate_label_ids(params[:label_ids])
variables = {
title: params[:title],
teamId: params[:team_id],
description: params[:description],
assigneeId: params[:assignee_id],
priority: params[:priority],
labelIds: params[:label_ids]
}.compact
mutation = Linear::Mutations.issue_create(variables)
response = post({ query: mutation })
process_response(response)
end
def link_issue(link, issue_id, title)
raise ArgumentError, 'Missing link' if link.blank?
raise ArgumentError, 'Missing issue id' if issue_id.blank?
payload = {
query: Linear::Mutations.issue_link(issue_id, link, title)
}
response = post(payload)
process_response(response)
end
def unlink_issue(link_id)
raise ArgumentError, 'Missing link id' if link_id.blank?
payload = {
query: Linear::Mutations.unlink_issue(link_id)
}
response = post(payload)
process_response(response)
end
private
def validate_team_and_title(params)
raise ArgumentError, 'Missing team id' if params[:team_id].blank?
raise ArgumentError, 'Missing title' if params[:title].blank?
end
def validate_priority(priority)
return if priority.nil? || PRIORITY_LEVELS.include?(priority)
raise ArgumentError, 'Invalid priority value. Priority must be 0, 1, 2, 3, or 4.'
end
def validate_label_ids(label_ids)
return if label_ids.nil?
return if label_ids.is_a?(Array) && label_ids.all?(String)
raise ArgumentError, 'label_ids must be an array of strings.'
end
def post(payload)
HTTParty.post(
BASE_URL,
headers: { 'Authorization' => @api_key, 'Content-Type' => 'application/json' },
body: payload.to_json
)
end
def process_response(response)
return response.parsed_response['data'].with_indifferent_access if response.success? && !response.parsed_response['data'].nil?
{ error: response.parsed_response, error_code: response.code }
end
end

56
lib/linear/mutations.rb Normal file
View File

@@ -0,0 +1,56 @@
module Linear::Mutations
def self.graphql_value(value)
case value
when String
# Strings must be enclosed in double quotes
"\"#{value}\""
when Array
# Arrays need to be recursively converted
"[#{value.map { |v| graphql_value(v) }.join(', ')}]"
else
# Other types (numbers, booleans) can be directly converted to strings
value.to_s
end
end
def self.graphql_input(input)
input.map { |key, value| "#{key}: #{graphql_value(value)}" }.join(', ')
end
def self.issue_create(input)
<<~GRAPHQL
mutation {
issueCreate(input: { #{graphql_input(input)} }) {
success
issue {
id
title
}
}
}
GRAPHQL
end
def self.issue_link(issue_id, link, title)
<<~GRAPHQL
mutation {
attachmentLinkURL(url: "#{link}", issueId: "#{issue_id}", title: "#{title}") {
success
attachment {
id
}
}
}
GRAPHQL
end
def self.unlink_issue(link_id)
<<~GRAPHQL
mutation {
attachmentDelete(id: "#{link_id}") {
success
}
}
GRAPHQL
end
end

100
lib/linear/queries.rb Normal file
View File

@@ -0,0 +1,100 @@
module Linear::Queries
TEAMS_QUERY = <<~GRAPHQL.freeze
query {
teams {
nodes {
id
name
}
}
}
GRAPHQL
def self.team_entities_query(team_id)
<<~GRAPHQL
query {
users {
nodes {
id
name
}
}
projects {
nodes {
id
name
}
}
workflowStates(
filter: { team: { id: { eq: "#{team_id}" } } }
) {
nodes {
id
name
}
}
issueLabels(
filter: { team: { id: { eq: "#{team_id}" } } }
) {
nodes {
id
name
}
}
}
GRAPHQL
end
def self.search_issue(term)
<<~GRAPHQL
query {
searchIssues(term: "#{term}") {
nodes {
id
title
description
identifier
}
}
}
GRAPHQL
end
def self.linked_issues(url)
<<~GRAPHQL
query {
attachmentsForURL(url: "#{url}") {
nodes {
id
title
issue {
id
identifier
title
description
priority
createdAt
url
assignee {
name
avatarUrl
}
state {
name
color
}
labels {
nodes{
id
name
color
description
}
}
}
}
}
}
GRAPHQL
end
end