Class: LinearToonMcp::Resolvers::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/linear_toon_mcp/resolvers/base.rb

Overview

Base class for entity resolvers. Subclasses declare their lookup attributes and any required parent scope. UUIDs always pass through unchanged regardless of the declared attributes.

Defaults derive from the class name:

WorkflowState.connection_name   # => "workflowStates"
WorkflowState.filter_type_name  # => "WorkflowStateFilter"
WorkflowState.entity_label      # => "State"

Override any default via Base.connection, Base.filter_type, or Base.label.

Constant Summary collapse

ATTRIBUTES =

Lookup attribute catalog: value predicate paired with GraphQL filter builder.

{
  name: {
    matches: ->(_v) { true },
    filter: ->(v) { {name: {eqIgnoreCase: v}} }
  },
  email: {
    matches: ->(v) { v.include?("@") },
    filter: ->(v) { {email: {eq: v}} }
  },
  number: {
    matches: ->(v) { v.match?(NUMERIC_RE) },
    filter: ->(v) { {number: {eq: v.to_i}} }
  },
  slug: {
    matches: ->(_v) { true },
    filter: ->(v) { {slugId: {eqIgnoreCase: v}} }
  },
  key: {
    matches: ->(v) { v.match?(/\A[A-Z]+\z/) },
    filter: ->(v) { {key: {eq: v}} }
  },
  type: {
    matches: ->(v) { v.match?(/\A[a-z]+\z/) },
    filter: ->(v) { {type: {eq: v}} }
  }
}.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**scope) ⇒ Base

Returns a new instance of Base.



158
159
160
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 158

def initialize(**scope)
  @scope = scope
end

Class Attribute Details

.scope_configObject (readonly)

Returns the value of attribute scope_config.



95
96
97
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 95

def scope_config
  @scope_config
end

Class Method Details

.attributesObject

Returns the attributes declared via lookup_by.



91
92
93
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 91

def attributes
  @attributes || []
end

.call(value:, **scope) ⇒ String

Resolves value to a UUID using LinearToonMcp.client.

Team.call(value: "Engineering")
WorkflowState.call(value: "Done", team_id: tid)

Parameters:

  • value (String)
  • scope (Hash)

    parent-scope kwargs (e.g. team_id:)

Returns:

  • (String)

    resolved UUID

Raises:

  • (Error)

    when no attribute resolves the value



144
145
146
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 144

def call(value:, **scope)
  new(**scope).resolve(value)
end

.call_many(values:, **scope) ⇒ Array<String>

Resolves each value via call, forwarding scope.

IssueLabel.call_many(values: ["bug", "p1"], team_id: tid)

Returns:

  • (Array<String>)


153
154
155
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 153

def call_many(values:, **scope)
  values.map { |v| call(value: v, **scope) }
end

.connection(name) ⇒ Object

Overrides the derived GraphQL connection name.



76
77
78
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 76

def connection(name)
  @connection = name.to_s
end

.connection_nameObject

Returns the GraphQL connection name.

WorkflowState.connection_name  # => "workflowStates"


100
101
102
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 100

def connection_name
  @connection ||= "#{entity_name[0].downcase}#{entity_name[1..]}s"
end

.entity_labelObject

Returns the not-found label — the trailing CamelCase word of entity_name.

WorkflowState.entity_label  # => "State"


115
116
117
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 115

def entity_label
  @label ||= entity_name.scan(/[A-Z][a-z]+/).last || entity_name
end

.entity_nameObject

Returns the entity name.

WorkflowState.entity_name  # => "WorkflowState"


122
123
124
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 122

def entity_name
  @entity_name ||= name.split("::").last
end

.filter_type(name) ⇒ Object

Overrides the derived GraphQL filter type name.



81
82
83
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 81

def filter_type(name)
  @filter_type = name.to_s
end

.filter_type_nameObject

Returns the GraphQL filter input type name.

WorkflowState.filter_type_name  # => "WorkflowStateFilter"


107
108
109
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 107

def filter_type_name
  @filter_type ||= "#{entity_name}Filter"
end

.label(name) ⇒ Object

Overrides the derived not-found label.



86
87
88
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 86

def label(name)
  @label = name.to_s
end

.lookup_by(*attrs) ⇒ Object

Declares lookup attributes for this resolver, in priority order.

class Team < Base
  lookup_by :key, :name
end

Parameters:

  • attrs (Array<Symbol>)

    attribute names from ATTRIBUTES



53
54
55
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 53

def lookup_by(*attrs)
  @attributes = attrs.freeze
end

.queryObject

Returns the memoized GraphQL query.



127
128
129
130
131
132
133
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 127

def query
  @query ||= <<~GRAPHQL
    query($filter: #{filter_type_name}) {
      #{connection_name}(filter: $filter, first: 1) { nodes { id } }
    }
  GRAPHQL
end

.scoped_by(key, optional: false, workspace_fallback: false) ⇒ Object

Declares a parent-scoping kwarg expected by call. The kwarg name implies the GraphQL filter key — :team_id produces {team: {id: {eq: value}}}.

class Cycle < Base
  scoped_by :team_id
  lookup_by :name
end

Parameters:

  • key (Symbol)

    kwarg name passed to call

  • optional (Boolean) (defaults to: false)

    omit the scope filter when scope arg is nil

  • workspace_fallback (Boolean) (defaults to: false)

    when set, the scope filter becomes an or: matching either the scoped parent or workspace-level records (parent null)



71
72
73
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 71

def scoped_by(key, optional: false, workspace_fallback: false)
  @scope_config = {key: key, optional: optional, workspace_fallback: workspace_fallback}.freeze
end

Instance Method Details

#resolve(value) ⇒ Object

Resolves value to a UUID. UUIDs pass through unchanged; otherwise each lookup_by attribute is tried in declared order and the first GraphQL lookup that returns a node wins.

Raises:

  • (Error)

    when nothing resolves value



167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/linear_toon_mcp/resolvers/base.rb', line 167

def resolve(value)
  return value if value.match?(UUID_RE)

  self.class.attributes.each do |attr|
    definition = ATTRIBUTES.fetch(attr) { raise Error, "Unknown attribute: #{attr.inspect}" }
    next unless definition[:matches].call(value)

    id = lookup(definition[:filter].call(value).merge(scope_filter))
    return id if id
  end

  raise Error, not_found_message(value)
end