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.



964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
# File 'lib/tina4/graphql.rb', line 964

def initialize(schema = nil)
  @schema = schema || GraphQLSchema.new
  # Maximum selection-set nesting depth. A deeply nested query (or a
  # circular fragment) would otherwise recurse without bound — a classic
  # GraphQL DoS / stack-overflow vector. Default 50 is far beyond any
  # legitimate query; TINA4_GRAPHQL_MAX_DEPTH <= 0 disables the guard.
  @max_depth = Integer(ENV.fetch("TINA4_GRAPHQL_MAX_DEPTH", "50"), exception: false) || 50
  @executor = GraphQLExecutor.new(@schema, max_depth: @max_depth)
  @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.



920
921
922
# File 'lib/tina4/graphql.rb', line 920

def default_instance
  @default_instance
end

Instance Attribute Details

#max_depthObject

Maximum selection-set nesting depth (DoS / stack-overflow guard). Read from TINA4_GRAPHQL_MAX_DEPTH (default 50; <= 0 disables). Exposed so tests/app code can override it; the writer keeps the executor in sync.



892
893
894
# File 'lib/tina4/graphql.rb', line 892

def max_depth
  @max_depth
end

#schemaObject (readonly)

Returns the value of attribute schema.



887
888
889
# File 'lib/tina4/graphql.rb', line 887

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.



958
959
960
961
# File 'lib/tina4/graphql.rb', line 958

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)


902
903
904
905
# File 'lib/tina4/graphql.rb', line 902

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



922
923
924
# File 'lib/tina4/graphql.rb', line 922

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.



944
945
946
947
948
949
950
951
952
953
954
# File 'lib/tina4/graphql.rb', line 944

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



1012
1013
1014
1015
1016
1017
1018
1019
1020
# File 'lib/tina4/graphql.rb', line 1012

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.



1007
1008
1009
# File 'lib/tina4/graphql.rb', line 1007

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)



1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
# File 'lib/tina4/graphql.rb', line 1059

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.



1052
1053
1054
1055
1056
# File 'lib/tina4/graphql.rb', line 1052

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


1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
# File 'lib/tina4/graphql.rb', line 1077

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|
    # handle_request expects the raw JSON text (it JSON.parses internally),
    # so read body_raw — request.body now returns the PARSED payload.
    body = request.body_raw
    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.



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

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