Class: Tina4::GraphQL

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/graphql.rb

Overview

─── Main GraphQL class ──────────────────────────────────────────────

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema = nil) ⇒ GraphQL

Returns a new instance of GraphQL.



913
914
915
916
917
918
919
920
921
922
923
924
925
# File 'lib/tina4/graphql.rb', line 913

def initialize(schema = nil)
  @schema = schema || GraphQLSchema.new
  @executor = GraphQLExecutor.new(@schema)
  @field_resolvers = {}

  # Drain any resolvers registered via the class-level GraphQL.resolve()
  # BEFORE this instance was constructed.
  self.class.class_resolvers.each do |type_name, fields|
    fields.each do |field_name, resolver|
      attach_resolver(type_name, field_name, resolver)
    end
  end
end

Class Attribute Details

.default_instanceObject

Returns the value of attribute default_instance.



869
870
871
# File 'lib/tina4/graphql.rb', line 869

def default_instance
  @default_instance
end

Instance Attribute Details

#schemaObject (readonly)

Returns the value of attribute schema.



846
847
848
# File 'lib/tina4/graphql.rb', line 846

def schema
  @schema
end

Class Method Details

._clear_class_resolvers!Object

Test-only — clear the class-level registry. Used by parity tests to avoid bleed-over between cases.



907
908
909
910
# File 'lib/tina4/graphql.rb', line 907

def _clear_class_resolvers!
  @class_resolvers = {}
  @default_instance = nil
end

.auto_schema_enabled?Boolean

Class-level toggle for ORM auto-schema generation. Defaults to true, can be disabled via TINA4_GRAPHQL_AUTO_SCHEMA=false. Initializers and user app code can branch on this before calling ‘schema.from_orm(…)`.

Returns:

  • (Boolean)


851
852
853
854
# File 'lib/tina4/graphql.rb', line 851

def self.auto_schema_enabled?
  val = ENV.fetch("TINA4_GRAPHQL_AUTO_SCHEMA", "true").to_s.strip.downcase
  !%w[false 0 no off].include?(val)
end

.class_resolversObject



871
872
873
# File 'lib/tina4/graphql.rb', line 871

def class_resolvers
  @class_resolvers ||= {}
end

.resolve(type_name, field_name, &block) ⇒ Object

Decorator-style resolver registration.

Tina4::GraphQL.resolve("Query", "products") do |root, args, ctx|
  Product.all
end

Tina4::GraphQL.resolve("Mutation", "createProduct") do |root, args, ctx|
  Product.create(args["input"])
end

Tina4::GraphQL.resolve("Product", "reviews") do |product, args, ctx|
  Review.where("product_id = ?", [product["id"]])
end

Resolvers registered before any GraphQL instance accumulate in a class-level registry. new GraphQL drains them into its schema. Resolvers registered after .default_instance is set wire into the live schema immediately.



893
894
895
896
897
898
899
900
901
902
903
# File 'lib/tina4/graphql.rb', line 893

def resolve(type_name, field_name, &block)
  class_resolvers[type_name] ||= {}
  class_resolvers[type_name][field_name] = block

  # If a default instance is active, attach immediately so post-startup
  # registrations take effect without re-instantiation.
  if @default_instance
    @default_instance.send(:attach_resolver, type_name, field_name, block)
  end
  block
end

Instance Method Details

#execute(query, variables: {}, context: {}, operation_name: nil) ⇒ Object

Execute a query string directly



956
957
958
959
960
961
962
963
964
# File 'lib/tina4/graphql.rb', line 956

def execute(query, variables: {}, context: {}, operation_name: nil)
  parser = GraphQLParser.new(query)
  document = parser.parse
  @executor.execute(document, variables: variables, context: context, operation_name: operation_name)
rescue GraphQLError => e
  { "data" => nil, "errors" => [{ "message" => e.message }] }
rescue => e
  { "data" => nil, "errors" => [{ "message" => "Internal error: #{e.message}" }] }
end

#field_resolver(type_name, field_name) ⇒ Object

Get the field resolver registered for an object type, if any. Used by the executor during nested field resolution.



951
952
953
# File 'lib/tina4/graphql.rb', line 951

def field_resolver(type_name, field_name)
  @field_resolvers.dig(type_name, field_name)
end

#handle_request(body, context: {}) ⇒ Object

Handle an HTTP request body (JSON string)



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
# File 'lib/tina4/graphql.rb', line 1003

def handle_request(body, context: {})
  payload = JSON.parse(body)
  query = payload["query"] || ""
  variables = payload["variables"] || {}
  op_name = payload["operationName"]

  execute(query, variables: variables, context: context, operation_name: op_name)
rescue JSON::ParserError
  { "data" => nil, "errors" => [{ "message" => "Invalid JSON in request body" }] }
end

#introspectObject

Return schema metadata for debugging.



996
997
998
999
1000
# File 'lib/tina4/graphql.rb', line 996

def introspect
  queries = @schema.queries.transform_values { |v| { type: v[:type], args: v[:args] || {} } }
  mutations = @schema.mutations.transform_values { |v| { type: v[:type], args: v[:args] || {} } }
  { types: @schema.types.keys, queries: queries, mutations: mutations }
end

#register_route(path = nil) ⇒ Object

── Route Registration ─────────────────────────────────────────────Register a POST /graphql route in the Tina4 router.

gql = Tina4::GraphQL.new(schema)
gql.register_route           # POST /graphql
gql.register_route("/api/graphql")  # custom path


1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
# File 'lib/tina4/graphql.rb', line 1021

def register_route(path = nil)
  # TINA4_GRAPHQL_ENDPOINT — defaults to /graphql when caller doesn't override.
  path ||= ENV.fetch("TINA4_GRAPHQL_ENDPOINT", "/graphql")
  path = path.to_s
  path = "/#{path}" unless path.start_with?("/")

  graphql = self
  Tina4.post path, auth: false do |request, response|
    body = request.body
    result = graphql.handle_request(body, context: { request: request })
    response.json(result)
  end

  # Optional: GET for GraphiQL/introspection
  Tina4.get path, auth: false do |request, response|
    query = request.params["query"]
    if query
      variables = request.params["variables"]
      variables = JSON.parse(variables) if variables.is_a?(String) && !variables.empty?
      result = graphql.execute(query, variables: variables || {}, context: { request: request })
      response.json(result)
    else
      response.html(graphiql_html(path))
    end
  end
end

#schema_sdlObject

Return schema as GraphQL SDL string.



967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
# File 'lib/tina4/graphql.rb', line 967

def schema_sdl
  sdl = ""
  @schema.types.each do |name, type_obj|
    sdl += "type #{name} {\n"
    type_obj.fields.each { |f| sdl += "  #{f[:name]}: #{f[:type]}\n" }
    sdl += "}\n\n"
  end
  unless @schema.queries.empty?
    sdl += "type Query {\n"
    @schema.queries.each do |name, config|
      args = (config[:args] || {}).map { |k, v| "#{k}: #{v}" }.join(", ")
      arg_str = args.empty? ? "" : "(#{args})"
      sdl += "  #{name}#{arg_str}: #{config[:type]}\n"
    end
    sdl += "}\n\n"
  end
  unless @schema.mutations.empty?
    sdl += "type Mutation {\n"
    @schema.mutations.each do |name, config|
      args = (config[:args] || {}).map { |k, v| "#{k}: #{v}" }.join(", ")
      arg_str = args.empty? ? "" : "(#{args})"
      sdl += "  #{name}#{arg_str}: #{config[:type]}\n"
    end
    sdl += "}\n\n"
  end
  sdl
end