Class: Parse::GraphQL::TypeGenerator

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

Overview

Generates GraphQL::Schema::Object subclasses from Parse::Object subclasses by reading the class-level property and association registries: fields, field_map, references (belongs_to), has_one_associations, has_many_associations, and relations.

v1 scope: type shape only. Default graphql-ruby field resolution invokes the same-named method on the underlying Ruby object, and Parse::Object subclasses already expose typed accessors (song.album, band.fans, etc.) — so no resolvers are emitted. Pagination arguments, Loaders, and Relay connections are deferred until query/mutation passthrough lands (TODO §7).

Cross-class references (pointers, has_many) require all referenced model types to be in the registry BEFORE field emission. Use generate_all to codegen a list of models in one call (two-pass: stub classes first, then add fields), or generate with an explicit registry: hash if you want incremental control.

Constant Summary collapse

SCALAR_TYPE_MAP =

Maps Parse property :type symbols to graphql-ruby built-ins. Cross-class references (:pointer, :relation) are NOT in this map — they need the target class and are handled per-field. Nil mappings fall through to the JSON scalar with a warning.

{
  string: ::GraphQL::Types::String,
  integer: ::GraphQL::Types::Int,
  float: ::GraphQL::Types::Float,
  boolean: ::GraphQL::Types::Boolean,
  date: ::GraphQL::Types::ISO8601DateTime,
  timezone: ::GraphQL::Types::String,
  phone: ::GraphQL::Types::String,
  email: ::GraphQL::Types::String,
  # Parse Bytes is a `{__type: Bytes, base64: ...}` wrapper, not a
  # bare string — the accessor returns the wrapped object, so
  # falling through to the JSON scalar (with a warn) is safer than
  # ::String.to_s on the hash. Subscribers needing structured byte
  # access should declare a `:string` property containing the base64.
  bytes: nil,
  polygon: nil, # JSON fallback (GeoJSON-shaped, multi-ring)
  # Parse vector columns are bounded-length Float arrays
  # (embeddings). Emit as a typed list of Floats, not JSON.
  vector: [::GraphQL::Types::Float],
  array: nil,   # JSON fallback (no element type known)
  object: nil,  # JSON fallback
}.freeze
OMITTED_TYPES =

Property types that are deliberately omitted. ACL is internal authz metadata; exposing it via GraphQL would leak authorization shape into clients. The objectId is exposed under the canonical id: ID field separately.

%i[acl].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class, registry:, _prebuilt:) ⇒ TypeGenerator

Returns a new instance of TypeGenerator.



128
129
130
131
132
# File 'lib/parse/graphql/type_generator.rb', line 128

def initialize(model_class, registry:, _prebuilt:)
  @model_class = model_class
  @registry = registry
  @klass = _prebuilt
end

Class Method Details

.build_stub(model_class) ⇒ Object



119
120
121
122
123
124
125
126
# File 'lib/parse/graphql/type_generator.rb', line 119

def self.build_stub(model_class)
  type_name = model_class.parse_class.tr("_", "")
  description_text = "Generated GraphQL type for Parse class #{model_class.parse_class}."
  Class.new(::GraphQL::Schema::Object) do
    graphql_name type_name
    description description_text
  end
end

.detect_name_collisions!(registry) ⇒ Object

graphql-ruby requires unique graphql_name across the schema. build_stub strips underscores so _User and User collapse to the same name. Raise a clear error rather than letting graphql-ruby's DuplicateNamesError surface at schema-build time, which doesn't say which Parse classes collided.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/parse/graphql/type_generator.rb', line 84

def self.detect_name_collisions!(registry)
  by_gql_name = registry.each_with_object({}) do |(parse_name, type), acc|
    (acc[type.graphql_name] ||= []) << parse_name
  end
  collisions = by_gql_name.select { |_, names| names.size > 1 }
  return if collisions.empty?
  details = collisions.map { |gql, parse| "#{gql}#{parse.join(', ')}" }.join('; ')
  raise "Parse::GraphQL::TypeGenerator: graphql_name collisions: #{details}. " \
        "Parse class names that differ only by underscores collapse to the same " \
        "GraphQL type name. Rename or generate the conflicting classes separately."
end

.generate(model_class, registry: nil) ⇒ Class

Generate a single type. If the model has belongs_to / has_many references to other Parse classes, those targets must already be in the registry — otherwise an error is raised at field-emit time. Prefer generate_all when you have a graph of models.

Parameters:

  • model_class (Class)

    a Parse::Object subclass.

  • registry (Hash, nil) (defaults to: nil)

    shared registry; mutated in place.

Returns:

  • (Class)

    anonymous GraphQL::Schema::Object subclass.



104
105
106
107
108
109
110
# File 'lib/parse/graphql/type_generator.rb', line 104

def self.generate(model_class, registry: nil)
  validate!(model_class)
  registry ||= {}
  stub = registry[model_class.parse_class] ||= build_stub(model_class)
  new(model_class, registry: registry, _prebuilt: stub).populate_fields
  stub
end

.generate_all(model_classes) ⇒ Hash{String => Class}

Generate types for a list of models in one call. Two-pass: creates empty stub classes for every model first so cross-class references resolve regardless of declaration order.

Parameters:

  • model_classes (Array<Class>)

    Parse::Object subclasses.

Returns:

  • (Hash{String => Class})

    registry of generated types keyed by Parse class name.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/parse/graphql/type_generator.rb', line 64

def self.generate_all(model_classes)
  registry = {}
  # Pass 1: stub classes registered by parse_class name.
  model_classes.each do |model|
    validate!(model)
    registry[model.parse_class] = build_stub(model)
  end
  # Pass 2: populate fields. Cross-references now resolve.
  model_classes.each do |model|
    new(model, registry: registry, _prebuilt: registry[model.parse_class]).populate_fields
  end
  detect_name_collisions!(registry)
  registry
end

.validate!(model_class) ⇒ Object



112
113
114
115
116
117
# File 'lib/parse/graphql/type_generator.rb', line 112

def self.validate!(model_class)
  unless model_class.is_a?(Class) && model_class < Parse::Object
    raise ArgumentError,
          "Parse::GraphQL::TypeGenerator requires a Parse::Object subclass, got #{model_class.inspect}"
  end
end

Instance Method Details

#populate_fieldsObject



134
135
136
137
138
139
140
# File 'lib/parse/graphql/type_generator.rb', line 134

def populate_fields
  emit_scalar_fields
  emit_belongs_to_fields
  emit_has_one_fields
  emit_has_many_fields
  @klass
end