graphiti_graphql 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94c56621cb36c796bbefd0e8f626d90e359e2e8740b2cb443a8293e2f98b57df
4
- data.tar.gz: dd1d065a140a292adab0f5d3c54e426dfcfa55b3691523f50ceb947fa752fdbd
3
+ metadata.gz: 75a9873f8fda91fa24f41d06e146826f3de27165452b61202dc45d597e869ef5
4
+ data.tar.gz: 69b67b64c1af770a2df57a74e6a30175e7c80f4d26d4a73a2db958f153b1c5b3
5
5
  SHA512:
6
- metadata.gz: 94886a1ebbed6fee376239b1a10c197c3891cd280aad29973a7a1ce4d6f769e9c1fc3949bb4613dbe98c44d1f584a1b2863955c605798525e24ee1766b136891
7
- data.tar.gz: d0ca0298e0e77da84361a1748a2b36457569910d43aed602a94cfd6eb8869c32c8a0db611bca68dbf64382fe0c5cd8dddb8956c105af9ae7819c4e92cce03adc
6
+ metadata.gz: fe62cbb5d5dd96ae3008add2bd13c465e0f2fd944869a478f544690bf6c459db2daf1504957adb243412a88cd318759fc6cd46f03a436b7b428d912d2530d2be
7
+ data.tar.gz: 2bd729f975995cf7433a3fc7c6864586dd5515955c68941c91e6036d2ef54cd024a3508a0869ed67eda7d6e61f612c37ae1b9b9b13ef19bb636948629edb4222
data/Gemfile CHANGED
@@ -1,6 +1,4 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in graphiti_graphql.gemspec
4
- gemspec
5
-
6
- gem "graphiti"
4
+ gemspec
data/Gemfile.lock CHANGED
@@ -1,19 +1,9 @@
1
- PATH
2
- remote: ../graphiti
3
- specs:
4
- graphiti (1.2.32)
5
- activesupport (>= 4.1)
6
- concurrent-ruby (~> 1.0)
7
- dry-types (>= 0.15.0, < 2.0)
8
- graphiti_errors (~> 1.1.0)
9
- jsonapi-renderer (~> 0.2, >= 0.2.2)
10
- jsonapi-serializable (~> 0.3.0)
11
-
12
1
  PATH
13
2
  remote: .
14
3
  specs:
15
- graphiti_graphql (0.1.1)
4
+ graphiti_graphql (0.1.2)
16
5
  activesupport (>= 4.1)
6
+ graphiti (>= 1.2.36)
17
7
  graphql (~> 1.12)
18
8
 
19
9
  GEM
@@ -54,6 +44,13 @@ GEM
54
44
  dry-inflector (~> 0.1, >= 0.1.2)
55
45
  dry-logic (~> 1.0, >= 1.0.2)
56
46
  google-protobuf (3.15.2)
47
+ graphiti (1.2.36)
48
+ activesupport (>= 4.1)
49
+ concurrent-ruby (~> 1.0)
50
+ dry-types (>= 0.15.0, < 2.0)
51
+ graphiti_errors (~> 1.1.0)
52
+ jsonapi-renderer (~> 0.2, >= 0.2.2)
53
+ jsonapi-serializable (~> 0.3.0)
57
54
  graphiti_errors (1.1.2)
58
55
  jsonapi-serializable (~> 0.1)
59
56
  graphiti_spec_helpers (1.0.5)
@@ -63,13 +60,13 @@ GEM
63
60
  graphql-batch (0.4.3)
64
61
  graphql (>= 1.3, < 2)
65
62
  promise.rb (~> 0.7.2)
66
- i18n (1.8.7)
63
+ i18n (1.8.9)
67
64
  concurrent-ruby (~> 1.0)
68
65
  jsonapi-renderer (0.2.2)
69
66
  jsonapi-serializable (0.3.1)
70
67
  jsonapi-renderer (~> 0.2.0)
71
68
  method_source (1.0.0)
72
- minitest (5.14.3)
69
+ minitest (5.14.4)
73
70
  parallel (1.20.1)
74
71
  parser (3.0.0.0)
75
72
  ast (~> 2.4.1)
@@ -129,7 +126,6 @@ DEPENDENCIES
129
126
  activemodel (>= 4.1)
130
127
  apollo-federation (~> 1.1)
131
128
  bundler (~> 2.0)
132
- graphiti!
133
129
  graphiti_graphql!
134
130
  graphiti_spec_helpers
135
131
  graphql (~> 1.12)
data/README.md CHANGED
@@ -4,12 +4,14 @@ GraphQL (and Apollo Federation) support for Graphiti. Serve traditional Rails JS
4
4
 
5
5
  Currently read-only, but you can add your own Mutations [manually](#blending-with-graphql-ruby).
6
6
 
7
+ Pre-alpha. Stay tuned.
8
+
7
9
  ## Setup
8
10
 
9
11
  Add to your `Gemfile`:
10
12
 
11
13
  ```rb
12
- gem 'graphiti', ">= 1.2.32"
14
+ gem 'graphiti', ">= 1.2.33"
13
15
  gem "graphiti_graphql"
14
16
  ```
15
17
 
@@ -59,7 +61,7 @@ Define your Schema and Type classes as normal. Then in an initializer:
59
61
  GraphitiGraphQL.schema_class = MySchema
60
62
  ```
61
63
 
62
- Any pre-existing GraphQL endpoint will continue working as normal. But the GQL endpoint you mounted in `config/routes.rb` will now serve BOTH your low-level `graphql-ruby` schema AND your Graphiti-specific schema. Note these cannot (currently) be served side-by-side on under `query` within the *same request*.
64
+ Any pre-existing GraphQL endpoint will continue working as normal. But the GQL endpoint you mounted in `config/routes.rb` will now serve BOTH your low-level `graphql-ruby` schema AND your Graphiti-specific schema. Note these cannot (currently) be served side-by-side under `query` within the *same request*.
63
65
 
64
66
  By default the GraphQL context will be `Graphiti.context[:object]`, which is the controller being called. You might want to customize this so your existing graphql-ruby code continues to expect the same context:
65
67
 
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "graphiti", ">= 1.2.33"
30
+ spec.add_dependency "graphiti", ">= 1.2.36"
31
31
  spec.add_dependency "activesupport", ">= 4.1"
32
32
  spec.add_dependency "graphql", "~> 1.12"
33
33
 
@@ -90,6 +90,22 @@ module GraphitiGraphQL
90
90
  federated_type
91
91
  end
92
92
 
93
+ def define_connection_type(name, type_class)
94
+ name = "#{name}FederatedConnection"
95
+ if (registered = type_registry[name])
96
+ return registered[:type]
97
+ end
98
+
99
+ klass = Class.new(@schema.class.base_object)
100
+ klass.graphql_name(name)
101
+ klass.field :nodes,
102
+ [type_class],
103
+ null: false,
104
+ extras: [:lookahead]
105
+ @schema.send :register, name, klass
106
+ klass
107
+ end
108
+
93
109
  def define_federated_has_many(type_class, relationship)
94
110
  local_name = GraphitiGraphQL::GraphitiSchema::Resource
95
111
  .gql_name(relationship.local_resource_class.name)
@@ -100,16 +116,20 @@ module GraphitiGraphQL
100
116
  local_interface = type_registry["I#{local_name}"]
101
117
  best_type = local_interface ? local_interface[:type] : local_type
102
118
 
119
+ connection_type = define_connection_type(local_name, best_type)
120
+
103
121
  field = type_class.field relationship.name,
104
- [best_type],
122
+ connection_type,
105
123
  null: false,
106
- extras: [:lookahead]
107
-
124
+ connection: false
108
125
  @schema.send :define_arguments_for_sideload_field,
109
126
  field, @schema.graphiti_schema.get_resource(local_resource_name)
110
- type_class.define_method relationship.name do |lookahead:, **arguments|
111
- # TODO test params...do version of sort with array/symbol keys and plain string
112
- params = arguments.as_json
127
+
128
+ type_class.define_method relationship.name do |**arguments|
129
+ {data: object, arguments: arguments}
130
+ end
131
+ connection_type.define_method :nodes do |lookahead:, **arguments|
132
+ params = object[:arguments].as_json
113
133
  .deep_transform_keys { |key| key.to_s.underscore.to_sym }
114
134
  selections = lookahead.selections.map(&:name)
115
135
  selections << relationship.foreign_key
@@ -122,7 +142,7 @@ module GraphitiGraphQL
122
142
 
123
143
  Federation::Loaders::HasMany
124
144
  .for(relationship, params)
125
- .load(object[:id])
145
+ .load(object[:data][:id])
126
146
  end
127
147
  end
128
148
 
@@ -63,6 +63,10 @@ module GraphitiGraphQL
63
63
  config[:name]
64
64
  end
65
65
 
66
+ def stats
67
+ config[:stats]
68
+ end
69
+
66
70
  def type
67
71
  config[:type]
68
72
  end
@@ -38,7 +38,7 @@ module GraphitiGraphQL
38
38
 
39
39
  def render(json, selection_name)
40
40
  payload = if find_one?(selection_name)
41
- {selection_name.to_sym => json.values[0][0]}
41
+ {selection_name.to_sym => json.values[0][:nodes][0]}
42
42
  else
43
43
  json
44
44
  end
@@ -108,15 +108,19 @@ module GraphitiGraphQL
108
108
  gather_filters(params, selection, variables_hash, chained_name)
109
109
  gather_sorts(params, selection, variables_hash, chained_name)
110
110
  gather_pages(params, selection, variables_hash, chained_name)
111
+ gather_stats(params, selection, variables_hash, chained_name)
111
112
  end
112
113
 
113
114
  params[:include] ||= []
114
115
  params[:include] << chained_name if chained_name
115
116
 
116
- fragments = selection.selections.select { |s|
117
+ nodes = selection.selections.find { |s| s.respond_to?(:name) && s.name == "nodes" }
118
+ children = nodes ? nodes.children : selection.selections
119
+
120
+ fragments = children.select { |s|
117
121
  s.is_a?(GraphQL::Language::Nodes::InlineFragment)
118
122
  }
119
- non_fragments = selection.selections - fragments
123
+ non_fragments = children - fragments
120
124
 
121
125
  if pbt
122
126
  # Only id/_type possible here
@@ -223,7 +227,9 @@ module GraphitiGraphQL
223
227
 
224
228
  attr_arg.value.arguments.each do |operator_arg|
225
229
  value = operator_arg.value
226
- if value.respond_to?(:name) # is a variable
230
+ if value.is_a?(GraphQL::Language::Nodes::Enum)
231
+ value = value.name
232
+ elsif value.respond_to?(:name) # is a variable
227
233
  value = variable_hash[operator_arg.value.name]
228
234
  end
229
235
  f[filter_param_name] = {operator_arg.name.underscore => value}
@@ -299,5 +305,22 @@ module GraphitiGraphQL
299
305
  params[:page].merge!(pages)
300
306
  end
301
307
  end
308
+
309
+ def gather_stats(params, selection, variable_hash, chained_name = nil)
310
+ stats = selection.children.find { |c| c.name == "stats" }
311
+ nodes = selection.children.find { |c| c.name == "nodes" }
312
+
313
+ if stats
314
+ stat_param = {}
315
+ stats.children.each do |stat_node|
316
+ stat_name = stat_node.name
317
+ calculations = stat_node.children.map(&:name)
318
+ stat_param[stat_name.to_sym] = calculations.join(",")
319
+ end
320
+ stat_param = {chained_name => stat_param} if chained_name
321
+ params[:stats] = stat_param
322
+ params[:page] = {size: 0} unless nodes
323
+ end
324
+ end
302
325
  end
303
326
  end
@@ -132,11 +132,30 @@ module GraphitiGraphQL
132
132
 
133
133
  private
134
134
 
135
+ def generate_connection_type(resource, top_level: true)
136
+ name = "#{resource.graphql_class_name}#{top_level ? "TopLevel" : ""}Connection"
137
+ if registered = type_registry[name]
138
+ return registered[:type]
139
+ end
140
+
141
+ type = type_registry[resource.graphql_class_name][:type]
142
+ klass = Class.new(self.class.base_object)
143
+ klass.graphql_name(name)
144
+ klass.field :nodes,
145
+ [type],
146
+ "List #{resource.graphql_class_name(false).pluralize}",
147
+ null: false
148
+ if top_level
149
+ klass.field :stats, generate_stat_class(resource), null: false
150
+ end
151
+ register(name, klass)
152
+ klass
153
+ end
154
+
135
155
  def add_index(query_class, resource)
136
156
  field_name = resource.graphql_entrypoint.to_s.underscore.to_sym
137
157
  field = query_class.field field_name,
138
- [type_registry[resource.graphql_class_name][:type]],
139
- "List #{resource.graphql_class_name(false).pluralize}",
158
+ generate_connection_type(resource, top_level: true),
140
159
  null: false
141
160
  @query_fields[field_name] = resource
142
161
  define_arguments_for_sideload_field(field, resource)
@@ -147,7 +166,7 @@ module GraphitiGraphQL
147
166
  field = query_class.field field_name,
148
167
  type_registry[resource.graphql_class_name][:type],
149
168
  "Single #{resource.graphql_class_name(false).singularize}",
150
- null: true
169
+ null: false
151
170
  @query_fields[field_name] = resource
152
171
  define_arguments_for_sideload_field field,
153
172
  resource,
@@ -232,17 +251,38 @@ module GraphitiGraphQL
232
251
  # TODO guarded operators or otherwise whatever eq => nil is
233
252
  def generate_filter_attribute_type(type_name, filter_name, filter_config)
234
253
  klass = Class.new(GraphQL::Schema::InputObject)
235
- klass.graphql_name "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
254
+ filter_graphql_name = "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
255
+ klass.graphql_name(filter_graphql_name)
236
256
  filter_config[:operators].each do |operator|
237
257
  canonical_graphiti_type = Graphiti::Types
238
258
  .name_for(filter_config[:type])
239
259
  type = GQL_TYPE_MAP[canonical_graphiti_type]
240
260
  required = !!filter_config[:required] && operator == "eq"
261
+
262
+ if (allowlist = filter_config[:allow])
263
+ type = define_allowlist_type(filter_graphql_name, allowlist)
264
+ end
265
+
266
+ type = [type] unless !!filter_config[:single]
241
267
  klass.argument operator, type, required: required
242
268
  end
243
269
  klass
244
270
  end
245
271
 
272
+ def define_allowlist_type(filter_graphql_name, allowlist)
273
+ name = "#{filter_graphql_name}Allow"
274
+ if (registered = type_registry[name])
275
+ return registered[:type]
276
+ end
277
+ klass = Class.new(GraphQL::Schema::Enum)
278
+ klass.graphql_name(name)
279
+ allowlist.each do |allowed|
280
+ klass.value(allowed)
281
+ end
282
+ register(name, klass)
283
+ klass
284
+ end
285
+
246
286
  def generate_resource_for_sideload(sideload)
247
287
  if sideload.type == :polymorphic_belongs_to
248
288
  unless registered?(sideload.parent_resource)
@@ -269,7 +309,11 @@ module GraphitiGraphQL
269
309
  type_registry[sideload.graphql_class_name][:type]
270
310
  end
271
311
 
272
- gql_field_type = sideload.to_many? ? [gql_type] : gql_type
312
+ gql_field_type = if sideload.to_many?
313
+ generate_connection_type(sideload.resource, top_level: false)
314
+ else
315
+ gql_type
316
+ end
273
317
  field_name = name.to_s.camelize(:lower)
274
318
  unless type_class.fields[field_name]
275
319
  field = type_class.field field_name.to_sym,
@@ -277,7 +321,6 @@ module GraphitiGraphQL
277
321
  null: !sideload.to_many?
278
322
 
279
323
  # No sort/filter/paginate on belongs_to
280
- # unless sideload.type.to_s.include?('belongs_to')
281
324
  unless sideload.type == :polymorphic_belongs_to
282
325
  define_arguments_for_sideload_field(field, sideload.resource)
283
326
  end
@@ -373,6 +416,25 @@ module GraphitiGraphQL
373
416
  klass
374
417
  end
375
418
 
419
+ def generate_stat_class(resource)
420
+ klass = Class.new(self.class.base_object)
421
+ klass.graphql_name "#{resource.graphql_class_name(false)}Stats"
422
+ resource.stats.each_pair do |name, calculations|
423
+ calc_class = generate_calc_class(resource, name, calculations)
424
+ klass.field name, calc_class, null: false
425
+ end
426
+ klass
427
+ end
428
+
429
+ def generate_calc_class(resource, stat_name, calculations)
430
+ klass = Class.new(self.class.base_object)
431
+ klass.graphql_name "#{resource.graphql_class_name(false)}#{stat_name}Calculations"
432
+ calculations.each do |calc|
433
+ klass.field calc, Float, null: false
434
+ end
435
+ klass
436
+ end
437
+
376
438
  def registered?(resource)
377
439
  name = resource.graphql_class_name(false)
378
440
  !!type_registry[name]
@@ -1,3 +1,3 @@
1
1
  module GraphitiGraphQL
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti_graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-07 00:00:00.000000000 Z
11
+ date: 2021-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphiti
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.33
19
+ version: 1.2.36
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.2.33
26
+ version: 1.2.36
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement