Creating a Linear issue from Chatwoot fails with a GraphQL parse error
when the title, description, or search term contains double quotes. For
example, a description like `the sender is "Bot"` produces this broken
query:
```graphql
issueCreate(input: { description: "the sender is "Bot"" })
```
Linear's API rejects this with `Syntax Error: Expected ":", found
String`. This affects issue creation, issue linking, and issue search —
any flow where user-provided text is interpolated into a GraphQL query.
The `graphql_value` helper was only escaping newlines (`\n`) but not
quotes, backslashes, or other characters that are meaningful inside a
GraphQL string literal. On top of that, `issue_link` and `search_issue`
bypassed `graphql_value` entirely, using raw string interpolation
instead.
The fix replaces the manual `gsub` escaping with Ruby's `to_json`, which
produces a properly escaped, double-quoted string that handles all
special characters. This is a minimal, well-understood substitution —
`to_json` on a Ruby string returns a valid JSON string literal, which is
also a valid GraphQL string literal since GraphQL uses the same escaping
rules. The `issue_link` mutation and `search_issue` query are updated to
route their parameters through `graphql_value` instead of raw
interpolation.
The `team_entities_query` and `linked_issues` methods in `queries.rb`
also use raw interpolation, but their inputs are system-generated IDs
and URLs rather than user-provided text, so they're left as-is to keep
this change focused.
67 lines
1.5 KiB
Ruby
67 lines
1.5 KiB
Ruby
module Linear::Mutations
|
|
def self.graphql_value(value)
|
|
case value
|
|
when String
|
|
value.to_json
|
|
when Array
|
|
"[#{value.map { |v| graphql_value(v) }.join(', ')}]"
|
|
else
|
|
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
|
|
identifier
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
def self.issue_link(params)
|
|
issue_id = params[:issue_id]
|
|
link = params[:link]
|
|
title = params[:title]
|
|
user_name = params[:user_name]
|
|
user_avatar_url = params[:user_avatar_url]
|
|
|
|
user_params = []
|
|
user_params << "createAsUser: #{graphql_value(user_name)}" if user_name.present?
|
|
user_params << "displayIconUrl: #{graphql_value(user_avatar_url)}" if user_avatar_url.present?
|
|
|
|
user_params_str = user_params.any? ? ", #{user_params.join(', ')}" : ''
|
|
|
|
<<~GRAPHQL
|
|
mutation {
|
|
attachmentLinkURL(url: #{graphql_value(link)}, issueId: #{graphql_value(issue_id)}, title: #{graphql_value(title)}#{user_params_str}) {
|
|
success
|
|
attachment {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
|
|
def self.unlink_issue(link_id)
|
|
<<~GRAPHQL
|
|
mutation {
|
|
attachmentDelete(id: "#{link_id}") {
|
|
success
|
|
}
|
|
}
|
|
GRAPHQL
|
|
end
|
|
end
|