Skip to content

Commit 819f697

Browse files
committed
Allow stats to be nested_on resources
1 parent a5bbb67 commit 819f697

11 files changed

+162
-5
lines changed

lib/graphiti.rb

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def self.setup!
145145
require "graphiti/scoping/filter"
146146
require "graphiti/stats/dsl"
147147
require "graphiti/stats/payload"
148+
require "graphiti/stats/nested_payload"
148149
require "graphiti/delegates/pagination"
149150
require "graphiti/util/include_params"
150151
require "graphiti/util/field_params"

lib/graphiti/resource_proxy.rb

+12
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ def stats
8383
end
8484
end
8585

86+
def nested_stats
87+
@nested_stats ||= if @query.hash[:stats]
88+
payload = Stats::NestedPayload.new @resource,
89+
@query,
90+
@scope.unpaginated_object,
91+
data
92+
payload.generate
93+
else
94+
{}
95+
end
96+
end
97+
8698
def pagination
8799
@pagination ||= Delegates::Pagination.new(self)
88100
end

lib/graphiti/serializer.rb

+10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def as_jsonapi(*)
2626
super.tap do |hash|
2727
strip_relationships!(hash) if strip_relationships?
2828
add_links!(hash)
29+
add_meta!(hash)
2930
end
3031
end
3132

@@ -62,6 +63,15 @@ def strip_relationships!(hash)
6263
end
6364
end
6465

66+
def add_meta!(hash)
67+
return if @resource.try(:type).nil?
68+
69+
resource_stats = @_exposures[:proxy].nested_stats.fetch(@resource.type, {})
70+
nested_stats = resource_stats[@object.id]
71+
72+
hash[:meta] = {stats: nested_stats} if nested_stats.present?
73+
end
74+
6575
def strip_relationships?
6676
return false unless Graphiti.config.links_on_demand
6777
params = Graphiti.context[:object].params || {}

lib/graphiti/stats/dsl.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module Stats
2525
# @attr_reader [Symbol] name the stat, e.g. :total
2626
# @attr_reader [Hash] calculations procs for various metrics
2727
class DSL
28-
attr_reader :name, :calculations
28+
attr_reader :name, :calculations, :nested_on
2929

3030
# @param [Adapters::Abstract] adapter the Resource adapter
3131
# @param [Symbol, Hash] config example: +:total+ or +{ total: [:count] }+
@@ -35,6 +35,8 @@ def initialize(adapter, config)
3535
@adapter = adapter
3636
@calculations = {}
3737
@name = config.keys.first
38+
@nested_on = config[:nested_on]
39+
3840
Array(config.values.first).each { |c| send(:"#{c}!") }
3941
end
4042

lib/graphiti/stats/nested_payload.rb

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module Graphiti
2+
module Stats
3+
# Generate the nested stats payload so we can return it in the response for each record i.e.
4+
#
5+
# {
6+
# data: [
7+
# {
8+
# id: "1",
9+
# type: "employee",
10+
# attributes: {},
11+
# relationships: {},
12+
# meta: { stats: { total: { count: 100 } } }
13+
# }
14+
# ],
15+
# meta: {}
16+
# }
17+
18+
class NestedPayload
19+
def initialize(resource, query, scope, data)
20+
@resource = resource
21+
@query = query
22+
@scope = scope
23+
@data = data
24+
end
25+
26+
# Generate the payload for +{ meta: { stats: { ... } } }+
27+
# Loops over all calculations, computes then, and gives back
28+
# a hash of stats and their results.
29+
# @return [Hash] the generated payload
30+
def generate
31+
{}.tap do |stats|
32+
@query.stats.each_pair do |name, calculation|
33+
nested_on = @resource.stats[name].nested_on
34+
next if nested_on.blank?
35+
36+
stats[nested_on] ||= {}
37+
38+
each_calculation(name, calculation) do |calc, function|
39+
data_arr = @data.is_a?(Enumerable) ? @data : [@data]
40+
41+
data_arr.each do |object|
42+
args = [@scope, name]
43+
args << @resource.context if function.arity >= 3
44+
args << object if function.arity == 4
45+
result = function.call(*args)
46+
47+
stats[nested_on][object.id] ||= {}
48+
stats[nested_on][object.id][name] ||= {}
49+
stats[nested_on][object.id][name][calc] = result
50+
end
51+
end
52+
end
53+
end
54+
end
55+
56+
private
57+
58+
def each_calculation(name, calculations)
59+
calculations.each do |calc|
60+
function = @resource.stat(name, calc)
61+
yield calc, function
62+
end
63+
end
64+
end
65+
end
66+
end

lib/graphiti/stats/payload.rb

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def initialize(resource, query, scope, data)
2828
def generate
2929
{}.tap do |stats|
3030
@query.stats.each_pair do |name, calculation|
31+
nested_on = @resource.stats[name]&.nested_on
32+
next if nested_on.present?
33+
3134
stats[name] = {}
3235

3336
each_calculation(name, calculation) do |calc, function|

spec/boolean_attribute_spec.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
end
1111

1212
let(:author) { double(id: 1) }
13-
let(:resource) { klass.new(object: author) }
13+
let(:proxy) { double(nested_stats: {}) }
14+
let(:resource) { klass.new(resource: double(type: "klass"), object: author, proxy: proxy) }
1415

1516
subject { resource.as_jsonapi[:attributes] }
1617

spec/fixtures/poro.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ def sum(scope, attr)
304304
end
305305

306306
def average(scope, attr)
307-
"poro_average_#{attr}"
307+
items = ::PORO::DB.all(scope)
308+
items.map(&attr).sum / items.count
308309
end
309310

310311
def maximum(scope, attr)

spec/stats/payload_spec.rb

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def stub_stat(attr, calc, result)
1818
stub_stat(:attr1, :count, 2)
1919
stub_stat(:attr1, :average, 1)
2020
stub_stat(:attr2, :maximum, 3)
21+
22+
stats_obj = double(nested_on: false)
23+
allow(dsl).to receive(:stats).and_return({attr1: stats_obj, attr2: stats_obj})
2124
end
2225

2326
it "generates the correct payload for each requested stat" do
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
require "spec_helper"
2+
3+
RSpec.describe "A resource with nested stats" do
4+
include_context "resource testing"
5+
6+
let!(:employee1) { PORO::Employee.create first_name: "Alice", age: 25 }
7+
let!(:employee2) { PORO::Employee.create first_name: "Bob", age: 40 }
8+
9+
let!(:position1) { PORO::Position.create employee_id: employee1.id, rank: 4 }
10+
let!(:position2) { PORO::Position.create employee_id: employee1.id, rank: 8 }
11+
let!(:position3) { PORO::Position.create employee_id: employee2.id, rank: 10 }
12+
let!(:position4) { PORO::Position.create employee_id: employee2.id, rank: 22 }
13+
14+
let(:state_group_count) { [{id: 10, count: 3}, {id: 11, count: 0}] }
15+
16+
def jsonapi
17+
JSON.parse(proxy.to_jsonapi)
18+
end
19+
20+
describe "has_many" do
21+
context "with include directive" do
22+
let(:resource) do
23+
Class.new(PORO::EmployeeResource) do
24+
def self.name
25+
"PORO::EmployeeResource"
26+
end
27+
28+
has_many :positions
29+
30+
stat age: [:squared], nested_on: :employees do
31+
squared do |scope, attr, context, employee|
32+
employee.age * employee.age
33+
end
34+
end
35+
end
36+
end
37+
38+
before do
39+
allow_any_instance_of(PORO::Employee).to receive(:applications_by_state_group_count).and_return(state_group_count)
40+
41+
params[:include] = "positions"
42+
params[:stats] = {age: "squared"}
43+
render
44+
end
45+
46+
it "includes the top-level stats" do
47+
expect(jsonapi["meta"]["stats"]).to be_nil
48+
end
49+
50+
it "includes the stats nested on employees" do
51+
jsonapi["data"].each do |record|
52+
expect(record["meta"]["stats"]).to_not be_nil
53+
expect(record["meta"]["stats"]["age"]).to_not be_nil
54+
end
55+
end
56+
end
57+
end
58+
end

spec/stats_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
it "responds with average in meta stats" do
6969
render
7070
expect(json["meta"]["stats"])
71-
.to eq({"age" => {"average" => "poro_average_age"}})
71+
.to eq({"age" => {"average" => 0}})
7272
end
7373
end
7474

@@ -190,7 +190,7 @@ def resolve(scope)
190190
render
191191
expect(json["meta"]["stats"]).to eq({
192192
"total" => {"count" => "poro_count_total"},
193-
"age" => {"sum" => "poro_sum_age", "average" => "poro_average_age"}
193+
"age" => {"sum" => "poro_sum_age", "average" => 0}
194194
})
195195
end
196196
end

0 commit comments

Comments
 (0)