Class: GraphqlDeclarative::Resolver
- Inherits:
-
GraphQL::Schema::Resolver
- Object
- GraphQL::Schema::Resolver
- GraphqlDeclarative::Resolver
- Defined in:
- lib/graphql_declarative/resolver.rb
Overview
The public surface. A resolver declares; it does not define resolve.
class Resolvers::Courses < GraphqlDeclarative::Resolver
type Types::Course.connection_type, null: false
filterable_by Types::CourseFilter
sortable_by :title, :created_at
paginate cursor: :id, default_page_size: 25, max_page_size: 100
preload author: :profile
preload_from_selection
def base_scope
Course.where(school_id: context[:school_id])
end
end
resolve is provided: base_scope -> Filter -> Sort -> Preloader -> page.
Order matters. Filter before sort; preload last, after the page is bounded,
or you preload the whole table.
Defined Under Namespace
Classes: SortDirection
Constant Summary collapse
- DEFAULT_PAGE_SIZE =
Defaults for
paginate, matching SPEC.md 6.5/6.7. 25- MAX_PAGE_SIZE =
100
Class Method Summary collapse
-
.cursor_column ⇒ Symbol?
The
paginate cursor:column, if one was given. -
.filter_class ⇒ Class?
The declared filter input, inherited if the subclass did not declare its own.
-
.filterable_by(filter_class) ⇒ Object
Adds the
filter:argument of the given input type. - .own_static_preloads ⇒ Object
-
.paginate(cursor: nil, default_page_size: DEFAULT_PAGE_SIZE, max_page_size: MAX_PAGE_SIZE) ⇒ Object
Adds
first:andafter:, and records the page-size policy. -
.paginate? ⇒ Boolean
Whether
paginatewas declared here or by an ancestor. -
.preload(*args) ⇒ Object
Preloads that are applied on every request regardless of the query.
-
.preload_from_selection(max_depth: Preloader::DEFAULT_MAX_DEPTH) ⇒ Object
Derive the rest of the preload set from what the query actually selects, so adding a field to a query cannot reintroduce an N+1 and no hand-kept
.includeslist can drift. - .preload_from_selection? ⇒ Boolean
- .preload_max_depth ⇒ Object
-
.sortable_by(*fields) ⇒ Object
Adds
sort_by:(an enum of exactly these fields) andsort_direction:. -
.sortable_fields ⇒ Array<Symbol>
The sortable whitelist, empty when undeclared.
-
.static_preloads ⇒ Array
Declared preloads, ancestors first.
Instance Method Summary collapse
-
#base_scope ⇒ Object
Subclasses implement this and nothing else.
-
#resolve(**args) ⇒ Object
SPEC.md 5, in order.
Class Method Details
.cursor_column ⇒ Symbol?
Returns the paginate cursor: column, if one was given.
132 133 134 135 136 137 |
# File 'lib/graphql_declarative/resolver.rb', line 132 def cursor_column own = defined?(@own_cursor_column) ? @own_cursor_column : nil return own if own superclass.respond_to?(:cursor_column) ? superclass.cursor_column : nil end |
.filter_class ⇒ Class?
Returns the declared filter input, inherited if the subclass did not declare its own.
65 66 67 68 69 70 |
# File 'lib/graphql_declarative/resolver.rb', line 65 def filter_class own = defined?(@own_filter_class) ? @own_filter_class : nil return own if own superclass.respond_to?(:filter_class) ? superclass.filter_class : nil end |
.filterable_by(filter_class) ⇒ Object
Adds the filter: argument of the given input type. The class itself is
remembered because Filter.apply reads definitions back off it at
request time — that registry is the only source of column and
association identifiers in the SQL layer (SPEC.md 7).
52 53 54 55 56 57 58 59 60 61 |
# File 'lib/graphql_declarative/resolver.rb', line 52 def filterable_by(filter_class) unless filter_class.is_a?(Class) && filter_class < GraphQL::Schema::InputObject raise Error, "filterable_by expects a GraphqlDeclarative::FilterInput subclass, got #{filter_class.inspect}" end @own_filter_class = filter_class argument :filter, filter_class, required: false filter_class end |
.own_static_preloads ⇒ Object
153 154 155 |
# File 'lib/graphql_declarative/resolver.rb', line 153 def own_static_preloads @own_static_preloads ||= [] end |
.paginate(cursor: nil, default_page_size: DEFAULT_PAGE_SIZE, max_page_size: MAX_PAGE_SIZE) ⇒ Object
Adds first: and after:, and records the page-size policy.
default_page_size/max_page_size are set through graphql-ruby's own
resolver DSL so the field reports them in introspection too; the
connection reads them back from there.
cursor: is optional and names the column to sort (and therefore issue
cursors) by when no sortable_by is declared. SPEC.md 6.7 lists
paginate(default_page_size:, max_page_size:); cursor: is accepted
because the documented example in this file's header passes it.
112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/graphql_declarative/resolver.rb', line 112 def paginate(cursor: nil, default_page_size: DEFAULT_PAGE_SIZE, max_page_size: MAX_PAGE_SIZE) @own_paginate = true @own_cursor_column = cursor&.to_sym self.default_page_size(Integer(default_page_size)) self.max_page_size(Integer(max_page_size)) argument :first, GraphQL::Types::Int, required: false argument :after, GraphQL::Types::String, required: false true end |
.paginate? ⇒ Boolean
Returns whether paginate was declared here or by an ancestor.
125 126 127 128 129 |
# File 'lib/graphql_declarative/resolver.rb', line 125 def paginate? return true if defined?(@own_paginate) && @own_paginate superclass.respond_to?(:paginate?) ? superclass.paginate? : false end |
.preload(*args) ⇒ Object
Preloads that are applied on every request regardless of the query.
Accepts anything .preload accepts: :author, [:a, :b],
{author: :profile}.
142 143 144 145 |
# File 'lib/graphql_declarative/resolver.rb', line 142 def preload(*args) own_static_preloads.concat(args) own_static_preloads end |
.preload_from_selection(max_depth: Preloader::DEFAULT_MAX_DEPTH) ⇒ Object
Derive the rest of the preload set from what the query actually selects,
so adding a field to a query cannot reintroduce an N+1 and no hand-kept
.includes list can drift.
This needs the lookahead, which graphql-ruby only passes when the field
asks for it, hence the extras registration.
163 164 165 166 167 168 169 170 |
# File 'lib/graphql_declarative/resolver.rb', line 163 def preload_from_selection(max_depth: Preloader::DEFAULT_MAX_DEPTH) @own_preload_from_selection = true @own_preload_max_depth = Integer(max_depth) current = extras extras(current + [:lookahead]) unless current.include?(:lookahead) true end |
.preload_from_selection? ⇒ Boolean
172 173 174 175 176 |
# File 'lib/graphql_declarative/resolver.rb', line 172 def preload_from_selection? return true if defined?(@own_preload_from_selection) && @own_preload_from_selection superclass.respond_to?(:preload_from_selection?) ? superclass.preload_from_selection? : false end |
.preload_max_depth ⇒ Object
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/graphql_declarative/resolver.rb', line 178 def preload_max_depth own = defined?(@own_preload_max_depth) ? @own_preload_max_depth : nil return own if own if superclass.respond_to?(:preload_max_depth) superclass.preload_max_depth else Preloader::DEFAULT_MAX_DEPTH end end |
.sortable_by(*fields) ⇒ Object
Adds sort_by: (an enum of exactly these fields) and sort_direction:.
The whitelist is the whole point: sort_by reaches ActiveRecord only
after Sort has matched it against this list and swapped in the list's
own canonical name. A column name never travels from the client into
SQL (SPEC.md 7).
78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/graphql_declarative/resolver.rb', line 78 def sortable_by(*fields) fields = fields.flatten.compact.map(&:to_sym) raise Error, "sortable_by requires at least one field" if fields.empty? # Union with the inherited list rather than replacing it: a subclass # that adds one sortable field should not silently drop its parent's. fields = (inherited_sortable_fields + fields).uniq @own_sortable_fields = fields.freeze argument :sort_by, build_sort_by_enum(fields), required: false argument :sort_direction, SortDirection, required: false, default_value: :asc fields end |
.sortable_fields ⇒ Array<Symbol>
Returns the sortable whitelist, empty when undeclared. Empty means "sort by :id ascending" (SPEC.md 6.7); Sort treats :id as implicitly sortable, so an empty whitelist is not a broken resolver.
95 96 97 98 99 100 |
# File 'lib/graphql_declarative/resolver.rb', line 95 def sortable_fields own = defined?(@own_sortable_fields) ? @own_sortable_fields : nil return own if own inherited_sortable_fields end |
.static_preloads ⇒ Array
Returns declared preloads, ancestors first.
148 149 150 151 |
# File 'lib/graphql_declarative/resolver.rb', line 148 def static_preloads inherited = superclass.respond_to?(:static_preloads) ? superclass.static_preloads : [] inherited + own_static_preloads end |
Instance Method Details
#base_scope ⇒ Object
Subclasses implement this and nothing else. Multi-tenancy scoping lives here; the gem never adds or removes conditions of its own (SPEC.md 7).
275 276 277 |
# File 'lib/graphql_declarative/resolver.rb', line 275 def base_scope raise NotImplementedError, "#{self.class} must define #base_scope" end |
#resolve(**args) ⇒ Object
SPEC.md 5, in order. The order is load-bearing:
base_scope
-> Filter.apply association filters become id subqueries, so
the paginated relation is never joined
-> Sort.apply whitelisted column + :id tiebreaker
-> Cursor.seek AFTER Sort, because the seek predicate is built
from the sort column
-> LIMIT page_size + 1 (inside KeysetConnection; the +1 is how
has_next_page is known without a COUNT)
-> Preloader (inside KeysetConnection, on the bounded page)
-> KeysetConnection
The last two steps are handed to KeysetConnection rather than done here on purpose: page_size clamping decides the LIMIT, and preloading must happen strictly after that LIMIT. Doing it here would mean computing the page size twice and getting invariant 1 wrong the second time.
236 237 238 239 240 241 242 243 244 245 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 |
# File 'lib/graphql_declarative/resolver.rb', line 236 def resolve(**args) scope = validated_base_scope model = scope.klass begin reject_backward_pagination!(args) scope = Filter.apply(scope, self.class.filter_class, args[:filter]) direction = normalize_direction(args[:sort_direction]) allowed, requested = sort_request(args) column = Sort.column_for(scope, allowed: allowed, field: requested) scope = Sort.apply(scope, allowed: allowed, field: requested, direction: direction) after = pagination_argument(args, :after) scope = apply_seek(scope, model, column, direction, after) KeysetConnection.new( scope, sort_column: column, sort_direction: direction, preloader: preloader_for(args, model), first: pagination_argument(args, :first), after: after, context: context, parent: object, field: field, ** ) rescue Error => e # SPEC.md 7: a bad cursor, an unsortable field or an unknown filter key # is the client's mistake, not a server fault. It surfaces as a GraphQL # error rather than a 500. raise GraphQL::ExecutionError, e. end end |