Skip to content

Commit 3c585fc

Browse files
Lee Richmondrichmolj
authored andcommitted
Accommodate statistics in hash renderer
* Add stats when requested * Nest arrays within "nodes" key for GQL, so stats can be a sibling * Don't render "nodes" when *only* stats requested & GQL * Add stats to the schema/diffing As with all hash rendering code, it's not lovely but gets the job done. Did some benchmarking and if anything should render flat JSON very slightly faster; JSON:API performance was equivalent.
1 parent 4a7ad6a commit 3c585fc

File tree

6 files changed

+148
-31
lines changed

6 files changed

+148
-31
lines changed

lib/graphiti/hash_renderer.rb

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,22 @@ def to_hash(fields: nil, include: {}, name_chain: [], graphql: false)
7474
name_chain << k unless name_chain.last == k
7575

7676
unless remote_resource? && serializers.nil?
77-
attrs[name.to_sym] = if serializers.is_a?(Array)
78-
serializers.map do |rr|
77+
payload = if serializers.is_a?(Array)
78+
data = serializers.map { |rr|
7979
rr.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
80-
end
80+
}
81+
graphql ? {nodes: data} : data
8182
elsif serializers.nil?
8283
if @resource.class.respond_to?(:sideload)
8384
if @resource.class.sideload(k).type.to_s.include?("_many")
84-
[]
85+
graphql ? {nodes: []} : []
8586
end
8687
end
8788
else
8889
serializers.to_hash(fields: fields, include: nested_include, graphql: graphql, name_chain: name_chain)
8990
end
91+
92+
attrs[name.to_sym] = payload
9093
end
9194
end
9295

@@ -133,29 +136,58 @@ def render(options)
133136
serializers = options[:data]
134137
opts = options.slice(:fields, :include)
135138
opts[:graphql] = @graphql
136-
to_hash(serializers, opts).tap do |hash|
137-
hash.merge!(options.slice(:meta)) unless options[:meta].empty?
138-
end
139+
top_level_key = get_top_level_key(@resource, serializers.is_a?(Array))
140+
141+
hash = {top_level_key => {}}
142+
nodes = get_nodes(serializers, opts)
143+
add_nodes(hash, top_level_key, options, nodes, @graphql)
144+
add_stats(hash, top_level_key, options, @graphql)
145+
hash
139146
end
140147

141148
private
142149

143-
def to_hash(serializers, opts)
144-
{}.tap do |hash|
145-
top_level_key = :data
146-
if @graphql
147-
top_level_key = @resource.graphql_entrypoint
148-
unless serializers.is_a?(Array)
149-
top_level_key = top_level_key.to_s.singularize.to_sym
150-
end
150+
def get_top_level_key(resource, is_many)
151+
key = :data
152+
153+
if @graphql
154+
key = @resource.graphql_entrypoint
155+
key = key.to_s.singularize.to_sym unless is_many
156+
end
157+
158+
key
159+
end
160+
161+
def get_nodes(serializers, opts)
162+
if serializers.is_a?(Array)
163+
serializers.map do |s|
164+
s.to_hash(**opts)
151165
end
166+
else
167+
serializers.to_hash(**opts)
168+
end
169+
end
170+
171+
def add_nodes(hash, top_level_key, opts, nodes, graphql)
172+
payload = nodes
173+
if graphql && nodes.is_a?(Array)
174+
payload = {nodes: nodes}
175+
end
152176

153-
hash[top_level_key] = if serializers.is_a?(Array)
154-
serializers.map do |s|
155-
s.to_hash(**opts)
177+
# Don't render nodes if we only requested stats
178+
unless graphql && opts[:fields].values == [[:stats]]
179+
hash[top_level_key] = payload
180+
end
181+
end
182+
183+
def add_stats(hash, top_level_key, options, graphql)
184+
if options[:meta] && !options[:meta].empty?
185+
if @graphql
186+
if (stats = options[:meta][:stats])
187+
hash[top_level_key][:stats] = stats
156188
end
157189
else
158-
serializers.to_hash(**opts)
190+
hash.merge!(options.slice(:meta))
159191
end
160192
end
161193
end

lib/graphiti/schema.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def generate_resources
9696
extra_attributes: extra_attributes(r),
9797
sorts: sorts(r),
9898
filters: filters(r),
99-
relationships: relationships(r)
99+
relationships: relationships(r),
100+
stats: stats(r)
100101
}
101102

102103
if r.grouped_filters.any?
@@ -169,6 +170,14 @@ def flag(value)
169170
end
170171
end
171172

173+
def stats(resource)
174+
{}.tap do |stats|
175+
resource.stats.each_pair do |name, config|
176+
stats[name] = config.calculations.keys
177+
end
178+
end
179+
end
180+
172181
def sorts(resource)
173182
{}.tap do |s|
174183
resource.sorts.each_pair do |name, sort|

lib/graphiti/schema_diff.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def compare_resources
3131
compare_sorts(r, new_resource)
3232
compare_filters(r, new_resource)
3333
compare_filter_group(r, new_resource)
34+
compare_stats(r, new_resource)
3435
compare_relationships(r, new_resource)
3536
end
3637
end
@@ -226,6 +227,21 @@ def compare_filter_group(old_resource, new_resource)
226227
end
227228
end
228229

230+
def compare_stats(old_resource, new_resource)
231+
old_resource[:stats].each_pair do |name, old_calculations|
232+
new_calculations = new_resource[:stats][name]
233+
if new_calculations
234+
old_calculations.each do |calc|
235+
unless new_calculations.include?(calc)
236+
@errors << "#{old_resource[:name]}: calculation #{calc.inspect} was removed from stat #{name.inspect}."
237+
end
238+
end
239+
else
240+
@errors << "#{old_resource[:name]}: stat #{name.inspect} was removed."
241+
end
242+
end
243+
end
244+
229245
def compare_endpoints
230246
@old[:endpoints].each_pair do |path, old_endpoint|
231247
unless (new_endpoint = @new[:endpoints][path])

spec/rendering_spec.rb

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,9 @@ def resolve(scope)
486486
end
487487

488488
it "is not returned" do
489-
expect(json[:employees][0]).to eq({
489+
expect(json[:employees][:nodes][0]).to eq({
490490
firstName: "John",
491-
positions: [{title: "title1"}]
491+
positions: {nodes: [{title: "title1"}]}
492492
})
493493
end
494494
end
@@ -500,10 +500,10 @@ def resolve(scope)
500500
end
501501

502502
it "is returned" do
503-
expect(json[:employees][0]).to eq({
503+
expect(json[:employees][:nodes][0]).to eq({
504504
id: employee1.id.to_s,
505505
firstName: "John",
506-
positions: [{id: position1.id.to_s, title: "title1"}]
506+
positions: {nodes: [{id: position1.id.to_s, title: "title1"}]}
507507
})
508508
end
509509
end
@@ -515,9 +515,9 @@ def resolve(scope)
515515
end
516516

517517
it "is not returned" do
518-
expect(json[:employees][0]).to eq({
518+
expect(json[:employees][:nodes][0]).to eq({
519519
firstName: "John",
520-
positions: [{title: "title1"}]
520+
positions: {nodes: [{title: "title1"}]}
521521
})
522522
end
523523
end
@@ -529,10 +529,10 @@ def resolve(scope)
529529
end
530530

531531
it "is returned" do
532-
expect(json[:employees][0]).to eq({
532+
expect(json[:employees][:nodes][0]).to eq({
533533
_type: "employees",
534534
firstName: "John",
535-
positions: [{_type: "positions", title: "title1"}]
535+
positions: {nodes: [{_type: "positions", title: "title1"}]}
536536
})
537537
end
538538
end
@@ -553,9 +553,9 @@ def self.name
553553
end
554554

555555
it "is camelized" do
556-
expect(json[:employees][0]).to eq({
556+
expect(json[:employees][:nodes][0]).to eq({
557557
firstName: "John",
558-
positions: [{multiWord: "foo"}]
558+
positions: {nodes: [{multiWord: "foo"}]}
559559
})
560560
end
561561
end
@@ -575,7 +575,8 @@ def self.name
575575
end
576576

577577
it "is camelized" do
578-
expect(json[:employees][0][:importantPositions]).to eq([{
578+
employee = json[:employees][:nodes][0]
579+
expect(employee[:importantPositions][:nodes]).to eq([{
579580
rank: 1,
580581
title: "title1",
581582
importantDepartment: {

spec/schema_diff_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,49 @@ def self.name
891891
end
892892
end
893893

894+
context "when a stat is added" do
895+
before do
896+
resource_a.stat age: [:sum]
897+
end
898+
899+
it { is_expected.to eq([]) }
900+
end
901+
902+
context "when a stat is removed" do
903+
before do
904+
resource_a.stat age: [:sum]
905+
resource_b.config[:stats].delete(:age)
906+
end
907+
908+
it "returns error" do
909+
expect(diff).to eq([
910+
"SchemaDiff::EmployeeResource: stat :age was removed."
911+
])
912+
end
913+
end
914+
915+
context "when a calculation is added" do
916+
before do
917+
resource_a.stat age: [:sum]
918+
resource_b.stat age: [:sum, :average]
919+
end
920+
921+
it { is_expected.to eq([]) }
922+
end
923+
924+
context "when a calculation is removed" do
925+
before do
926+
resource_a.stat age: [:sum, :average]
927+
resource_b.stat age: [:sum]
928+
end
929+
930+
it "returns error" do
931+
expect(diff).to eq([
932+
"SchemaDiff::EmployeeResource: calculation :average was removed from stat :age."
933+
])
934+
end
935+
end
936+
894937
context "when relationship is added" do
895938
before do
896939
resource_b.has_many :positions, resource: position_resource

spec/schema_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
id: {},
3232
first_name: {}
3333
},
34+
stats: {
35+
total: [:count]
36+
},
3437
filters: {
3538
id: {
3639
type: "integer_id",
@@ -552,6 +555,19 @@ def self.name
552555
end
553556
end
554557

558+
context "when an additional statistic/calculations" do
559+
before do
560+
employee_resource.stat age: [:average, :sum]
561+
end
562+
563+
it "is added to the schema" do
564+
expect(schema[:resources][0][:stats]).to eq({
565+
total: [:count],
566+
age: [:average, :sum]
567+
})
568+
end
569+
end
570+
555571
context "when a default sort" do
556572
before do
557573
employee_resource.default_sort = [{foo: :asc}]

0 commit comments

Comments
 (0)