Class: Synthra::Export::GraphQLFederation

Inherits:
Object
  • Object
show all
Defined in:
lib/synthra/export/graphql_federation.rb

Overview

GraphQL Federation Export

Exports schemas to Apollo Federation v2 compatible GraphQL SDL. Includes @key directives, entity references, and subgraph support.

Examples:

Export federated schema

exporter = Synthra::Export::GraphQLFederation.new(registry)
sdl = exporter.export

CLI

$ synthra export --all -f graphql-federation -o schema.graphql

Constant Summary collapse

TYPE_MAP =

Type mapping from Synthra to GraphQL

{
  "text" => "String",
  "name" => "String",
  "email" => "String",
  "url" => "String",
  "uuid" => "ID",
  "id_sequence" => "ID",
  "number" => "Int",
  "integer" => "Int",
  "age" => "Int",
  "airport_elevation" => "Int",
  "discount" => "Int",
  "formula" => "Int",
  "row_number" => "Int",
  "sequence" => "Int",
  "float" => "Float",
  "money" => "Float",
  "latitude" => "Float",
  "longitude" => "Float",
  "airport_latitude" => "Float",
  "airport_longitude" => "Float",
  "product_price" => "Float",
  "tax_rate" => "Float",
  "transaction_amount" => "Float",
  "boolean" => "Boolean",
  "timestamp" => "DateTime",
  "datetime" => "DateTime",
  "date" => "Date",
  "object" => "JSON",
  "map_by_field" => "JSON",
  "array" => "[String]"
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(registry, **options) ⇒ GraphQLFederation

Create new exporter

Parameters:

  • registry (Registry)

    schema registry

  • options (Hash)

    export options

Options Hash (**options):

  • :service_name (String)

    subgraph service name

  • :service_url (String)

    subgraph URL

  • :key_fields (Array<String>)

    default key fields

  • :include_mutations (Boolean)

    include mutation types



63
64
65
66
67
68
69
70
71
72
# File 'lib/synthra/export/graphql_federation.rb', line 63

def initialize(registry, **options)
  @registry = registry
  @options = {
    service_name: "fake-data-service",
    service_url: "http://localhost:4000/graphql",
    key_fields: %w[id uuid],
    include_mutations: true,
    include_subscriptions: false
  }.merge(options)
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



52
53
54
# File 'lib/synthra/export/graphql_federation.rb', line 52

def options
  @options
end

#registryObject (readonly)

Returns the value of attribute registry.



52
53
54
# File 'lib/synthra/export/graphql_federation.rb', line 52

def registry
  @registry
end

Instance Method Details

#add_relationship_fields(lines, schema) ⇒ Object (private)



348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/synthra/export/graphql_federation.rb', line 348

def add_relationship_fields(lines, schema)
  # Check for relationship indicators in field names
  schema.fields.each do |field|
    if field.name.end_with?("_id") && !field.name.start_with?("external_")
      # Infer relationship
      related_name = field.name.sub(/_id$/, "").split("_").map(&:capitalize).join
      if @registry.schema?(related_name)
        lines << ""
        lines << "  # Resolved relationship"
        lines << "  #{to_snake_case(related_name)}: #{related_name} @provides(fields: \"id\")"
      end
    end
  end
end

#exportString

Export to Federation SDL

Returns:

  • (String)

    GraphQL SDL



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/synthra/export/graphql_federation.rb', line 78

def export
  lines = []

  # Federation schema extension
  lines << federation_header
  lines << ""

  # Scalar definitions
  lines << scalar_definitions
  lines << ""

  # Type definitions
  @registry.names.sort.each do |name|
    schema = @registry.schema(name)
    lines << generate_type(schema)
    lines << ""
  end

  # Query type
  lines << generate_query_type
  lines << ""

  # Mutation type
  if @options[:include_mutations]
    lines << generate_mutation_type
    lines << ""
  end

  # Subscription type
  if @options[:include_subscriptions]
    lines << generate_subscription_type
    lines << ""
  end

  # Input types
  @registry.names.sort.each do |name|
    schema = @registry.schema(name)
    lines << generate_input_type(schema)
    lines << ""
  end

  lines.join("\n").strip
end

#federation_headerObject (private)



124
125
126
127
128
129
130
# File 'lib/synthra/export/graphql_federation.rb', line 124

def federation_header
  <<~HEADER
    extend schema
      @link(url: "https://specs.apollo.dev/federation/v2.0",
            import: ["@key", "@shareable", "@external", "@provides", "@requires"])
  HEADER
end

#field_directives(field, schema) ⇒ Object (private)



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/synthra/export/graphql_federation.rb', line 330

def field_directives(field, schema)
  directives = []

  # Mark shareable fields
  if %w[id uuid].include?(field.name)
    # Key fields are automatically shareable
  elsif field.type_name == "timestamp"
    directives << "@shareable"
  end

  # External references
  if field.type_name.start_with?("Ref(")
    directives << "@external"
  end

  directives.empty? ? "" : " #{directives.join(' ')}"
end

#field_to_graphql(field, input: false) ⇒ Object (private)



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/synthra/export/graphql_federation.rb', line 303

def field_to_graphql(field, input: false)
  type_name = field.type_name

  # Handle arrays
  if type_name == "array"
    element = field.type_args[:element]
    inner_type = if @registry.schema?(element.to_s)
                   input ? "#{element}Input" : element.to_s
                 else
                   TYPE_MAP[element.to_s] || "String"
                 end
    return "[#{inner_type}!]"
  end

  # Handle enums
  if type_name == "enum"
    return "#{field.name.capitalize}Enum"
  end

  # Handle references
  if @registry.schema?(type_name)
    return input ? "#{type_name}Input" : type_name
  end

  TYPE_MAP[type_name] || "String"
end

#find_key_field(schema) ⇒ Object (private)



363
364
365
366
367
368
# File 'lib/synthra/export/graphql_federation.rb', line 363

def find_key_field(schema)
  @options[:key_fields].each do |key|
    return key if schema.fields.any? { |f| f.name == key }
  end
  nil
end

#generate_connection_typesObject (private)



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/synthra/export/graphql_federation.rb', line 246

def generate_connection_types
  types = @registry.names.map do |name|
    <<~CONNECTION
      type #{name}Connection {
        edges: [#{name}Edge!]!
        pageInfo: PageInfo!
        totalCount: Int!
      }

      type #{name}Edge {
        cursor: String!
        node: #{name}!
      }
    CONNECTION
  end

  page_info = <<~PAGEINFO
    type PageInfo {
      hasNextPage: Boolean!
      hasPreviousPage: Boolean!
      startCursor: String
      endCursor: String
    }
  PAGEINFO

  [page_info, *types].join("\n")
end

#generate_generation_mode_enumObject (private)



291
292
293
294
295
296
297
298
299
300
301
# File 'lib/synthra/export/graphql_federation.rb', line 291

def generate_generation_mode_enum
  <<~ENUM
    enum GenerationMode {
      RANDOM
      EDGE
      INVALID
      HOSTILE
      MIXED
    }
  ENUM
end

#generate_input_type(schema) ⇒ Object (private)



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/synthra/export/graphql_federation.rb', line 165

def generate_input_type(schema)
  lines = []
  lines << "input #{schema.name}Input {"

  schema.fields.each do |field|
    next if %w[id uuid created_at updated_at].include?(field.name)

    graphql_type = field_to_graphql(field, input: true)
    lines << "  #{field.name}: #{graphql_type}"
  end

  lines << "}"
  lines.join("\n")
end

#generate_mutation_typeObject (private)



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/synthra/export/graphql_federation.rb', line 217

def generate_mutation_type
  lines = []
  lines << "type Mutation {"

  @registry.names.sort.each do |name|
    snake_name = to_snake_case(name)

    lines << "  # Generate and optionally persist #{name}"
    lines << "  create#{name}(input: #{name}Input, seed: Int): #{name}!"
    lines << "  createMany#{name}s(count: Int!, input: #{name}Input, seed: Int): [#{name}!]!"
  end

  lines << "}"
  lines.join("\n")
end

#generate_query_typeObject (private)



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/synthra/export/graphql_federation.rb', line 180

def generate_query_type
  lines = []
  lines << "type Query {"

  @registry.names.sort.each do |name|
    snake_name = to_snake_case(name)

    # Single record by ID
    lines << "  #{snake_name}(id: ID!): #{name}"

    # List with pagination
    lines << "  #{snake_name}s(first: Int, after: String, filter: #{name}Input): #{name}Connection!"

    # Generate fake data
    lines << "  generate#{name}(seed: Int, mode: GenerationMode): #{name}!"
    lines << "  generate#{name}s(count: Int!, seed: Int, mode: GenerationMode): [#{name}!]!"
  end

  # Schema introspection
  lines << ""
  lines << "  # Schema information"
  lines << "  _schemas: [SchemaInfo!]!"
  lines << "  _schema(name: String!): SchemaInfo"

  lines << "}"

  # Add supporting types
  lines << ""
  lines << generate_connection_types
  lines << ""
  lines << generate_schema_info_type
  lines << ""
  lines << generate_generation_mode_enum

  lines.join("\n")
end

#generate_schema_info_typeObject (private)



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/synthra/export/graphql_federation.rb', line 274

def generate_schema_info_type
  <<~SCHEMA_INFO
    type SchemaInfo {
      name: String!
      fields: [FieldInfo!]!
      behaviors: [String!]!
    }

    type FieldInfo {
      name: String!
      type: String!
      optional: Boolean!
      nullable: Boolean!
    }
  SCHEMA_INFO
end

#generate_subscription_typeObject (private)



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/synthra/export/graphql_federation.rb', line 233

def generate_subscription_type
  lines = []
  lines << "type Subscription {"

  @registry.names.sort.each do |name|
    snake_name = to_snake_case(name)
    lines << "  #{snake_name}Generated: #{name}!"
  end

  lines << "}"
  lines.join("\n")
end

#generate_type(schema) ⇒ Object (private)



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/synthra/export/graphql_federation.rb', line 140

def generate_type(schema)
  lines = []

  # Type declaration with @key directive
  key_field = find_key_field(schema)
  key_directive = key_field ? %( @key(fields: "#{key_field}")) : ""

  lines << "type #{schema.name}#{key_directive} {"

  # Fields
  schema.fields.each do |field|
    graphql_type = field_to_graphql(field)
    nullable = field.optional? || field.nullable? ? "" : "!"
    directives = field_directives(field, schema)

    lines << "  #{field.name}: #{graphql_type}#{nullable}#{directives}"
  end

  # Add relationship fields
  add_relationship_fields(lines, schema)

  lines << "}"
  lines.join("\n")
end

#scalar_definitionsObject (private)



132
133
134
135
136
137
138
# File 'lib/synthra/export/graphql_federation.rb', line 132

def scalar_definitions
  <<~SCALARS
    scalar DateTime
    scalar Date
    scalar JSON
  SCALARS
end

#to_snake_case(str) ⇒ Object (private)



370
371
372
373
374
# File 'lib/synthra/export/graphql_federation.rb', line 370

def to_snake_case(str)
  str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
     .gsub(/([a-z\d])([A-Z])/, '\1_\2')
     .downcase
end