Created
March 15, 2024 20:35
-
-
Save rmosolgo/c74fd1fdb0f4e2f847c64d1331d21746 to your computer and use it in GitHub Desktop.
Removing items from `nodes` after it has been paginated
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# In this approach `nodes` are removed from a connection _after_ pagination has been applied. | |
# | |
# This approach has some downsides: | |
# - It may leak data about unauthorized nodes. hasNextPage and hasPrevPage will | |
# indicate that items were removed from the list; depending on sort and filter options, | |
# a client could learn about the attributes of the removed nodes. | |
# - pagination metadata may be wrong, since the list of nodes was modified after it was calculated. | |
# (some pagination info may be calculated _while_ the list of nodes is prepared.) | |
# | |
# The best alternative approach is to use `scope_items` instead, but I share this | |
# in case there are situations where that isn't feasible. | |
# | |
require "bundler/inline" | |
gemfile do | |
gem "graphql", "2.2.12" | |
gem "sqlite3" | |
gem "rails" | |
end | |
require "active_record" | |
# Set up the database and add some records: | |
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") | |
ActiveRecord::Schema.define do | |
self.verbose = false | |
create_table :things, force: true do |t| | |
t.column :name, :string | |
t.column :authorized, :boolean, default: true | |
end | |
end | |
class Thing < ActiveRecord::Base; end | |
Thing.create!(name: "Alfalfa") | |
Thing.create!(name: "Barley") | |
Thing.create!(name: "Corn", authorized: false) | |
Thing.create!(name: "Durum") | |
class MySchema < GraphQL::Schema | |
# Make a custom Connection object which will apply authorization to its nodes | |
class FilteredThingsConnection < GraphQL::Pagination::ActiveRecordRelationConnection | |
# Before returning nodes, remove unauthorized ones. (Pagination metadata will be unaffected.) | |
def nodes | |
@loaded_and_authorized_nodes ||= begin | |
paginated_nodes = super | |
res = Thing.remove_unauthorized_nodes(paginated_nodes) | |
pp res | |
res | |
end | |
end | |
end | |
class Thing < GraphQL::Schema::Object | |
field :name, String | |
def self.remove_unauthorized_nodes(all_nodes) | |
all_nodes.select(&:authorized?) | |
end | |
end | |
class Query < GraphQL::Schema::Object | |
field :things, Thing.connection_type | |
def things | |
things = ::Thing.all | |
# Wrap the ActiveRecord::Relation in a custom connection object: | |
FilteredThingsConnection.new(things) | |
end | |
end | |
query(Query) | |
end | |
pp MySchema.execute("{ things { nodes { name } } }").to_h | |
# {"data"=>{"things"=>{"nodes"=>[{"name"=>"Alfalfa"}, {"name"=>"Barley"}, {"name"=>"Durum"}]}}} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment