Class: Graphiti::Query

Inherits:
Object show all
Defined in:
lib/graphiti/query.rb

Defined Under Namespace

Classes: RemoteSideloadResource

Constant Summary collapse

[true, "true"].freeze
LINKLESS_FORMATS =
[:json, :xml, "json", "xml"].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil) ⇒ Query

Returns a new instance of Query.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/graphiti/query.rb', line 10

def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
  if positional.any?
    Graphiti::DEPRECATOR.warn("Passing Query.new trailing arguments positionally is deprecated. Use association_name:/nested_include:/parents:/action: keywords.")
    association_name ||= positional[0]
    nested_include ||= positional[1]
    parents = positional[2] if positional.length > 2
    action ||= positional[3]
  end

  @resource = resource
  @association_name = association_name
  @params = params
  @params = @params.permit! if @params.respond_to?(:permit!)
  @params = @params.to_h if @params.respond_to?(:to_h)
  @params = @params.deep_symbolize_keys
  @include_param = nested_include || @params[:include]
  @parents = parents
  @action = parse_action(action)
  @entity_map = Concurrent::Map.new if parents.empty?
end

Instance Attribute Details

#actionObject (readonly)

Returns the value of attribute action.



8
9
10
# File 'lib/graphiti/query.rb', line 8

def action
  @action
end

#association_nameObject (readonly)

Returns the value of attribute association_name.



8
9
10
# File 'lib/graphiti/query.rb', line 8

def association_name
  @association_name
end

#paramsObject (readonly)

Returns the value of attribute params.



8
9
10
# File 'lib/graphiti/query.rb', line 8

def params
  @params
end

#resourceObject (readonly)

Returns the value of attribute resource.



8
9
10
# File 'lib/graphiti/query.rb', line 8

def resource
  @resource
end

Instance Method Details

#association?Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/graphiti/query.rb', line 31

def association?
  !!@association_name
end

#cache_keyObject



305
306
307
# File 'lib/graphiti/query.rb', line 305

def cache_key
  "args-#{query_cache_key}"
end

#debug_requested?Boolean

Returns:

  • (Boolean)


106
107
108
# File 'lib/graphiti/query.rb', line 106

def debug_requested?
  !!@params[:debug]
end

#entity_mapObject

Concurrent::Map because sideload scopes resolve on pool threads



36
37
38
39
40
# File 'lib/graphiti/query.rb', line 36

def entity_map
  return root.entity_map unless root == self

  @entity_map
end

#extra_fieldsObject



201
202
203
# File 'lib/graphiti/query.rb', line 201

def extra_fields
  @extra_fields ||= parse_fieldset(@params[:extra_fields] || {})
end

#fieldsObject



191
192
193
194
195
196
197
198
199
# File 'lib/graphiti/query.rb', line 191

def fields
  @fields ||= begin
    hash = parse_fieldset(@params[:fields] || {})
    hash.each_pair do |type, fields|
      hash[type] += extra_fields[type] if extra_fields[type]
    end
    hash
  end
end

#filtersObject



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/graphiti/query.rb', line 205

def filters
  @filters ||= {}.tap do |hash|
    (@params[:filter] || {}).each_pair do |name, value|
      name = name.to_sym

      if legacy_nested?(name)
        value.keys.each do |key|
          filter_name = key.to_sym
          filter_value = value[key]

          if @resource.get_attr!(filter_name, :filterable, request: true)
            hash[filter_name] = filter_value
          end
        end
      elsif nested?(name)
        name = name.to_s.split(".").last.to_sym
        validate!(name, :filterable)
        hash[name] = value
      elsif top_level? && validate!(name, :filterable)
        hash[name] = value
      end
    end
  end
end

#hashObject



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/graphiti/query.rb', line 110

def hash
  @hash ||= {}.tap do |h|
    h[:filter] = filters
    h[:sort] = sorts
    h[:page] = pagination
    if association?
      resource_type = @resource.class.type
      h[:extra_fields] = {resource_type => extra_fields[resource_type]} if extra_fields.key?(resource_type)
    else
      h[:fields] = fields
      h[:extra_fields] = extra_fields
    end
    h[:stats] = stats
    h[:include] = sideload_hash
  end.reject { |_, value| value.empty? }
end

#include_hashObject



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/graphiti/query.rb', line 269

def include_hash
  @include_hash ||= begin
    requested = include_directive.to_hash

    allowlist = nil
    if @resource.context&.respond_to?(:sideload_allowlist)
      allowlist = @resource.context.sideload_allowlist
      allowlist = allowlist[@resource.current_action] if allowlist
    end

    scrubbed = allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested

    scrubbed.filter do |key, value|
      sideload = @resource.class.sideload(key)
      sideload.nil? || sideload.readable?
    end
  end
end

#includes_requested?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/graphiti/query.rb', line 69

def includes_requested?
  @include_param.present?
end

Returns:

  • (Boolean)


73
74
75
76
77
# File 'lib/graphiti/query.rb', line 73

def links_requested?
  return @links_requested if defined?(@links_requested)

  @links_requested = TRUTHY_LINKS.include?(@params[:links])
end

#page_links?Boolean Also known as: pagination_links?

Returns:

  • (Boolean)


91
92
93
94
95
96
97
98
99
# File 'lib/graphiti/query.rb', line 91

def page_links?
  if action == :find
    false
  elsif @resource.page_links == :on_demand
    page_links_requested?
  else
    @resource.page_links
  end
end

Returns:

  • (Boolean)


101
102
103
104
# File 'lib/graphiti/query.rb', line 101

def page_links_requested?
  [@params[:page_links], @params[:pagination_links]]
    .any? { |value| [true, "true"].include?(value) }
end

#paginate?Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/graphiti/query.rb', line 301

def paginate?
  ![false, "false"].include?(@params[:paginate])
end

#paginationObject



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/graphiti/query.rb', line 252

def pagination
  @pagination ||= {}.tap do |hash|
    (@params[:page] || {}).each_pair do |name, value|
      if legacy_nested?(name)
        value.each_pair do |k, v|
          hash[k.to_sym] = cast_page_param(k.to_sym, v)
        end
      elsif nested?(name)
        param_name = name.to_s.split(".").last.to_sym
        hash[param_name] = cast_page_param(param_name, value)
      elsif top_level? && Scoping::Paginate::PARAMS.include?(name.to_sym)
        hash[name.to_sym] = cast_page_param(name.to_sym, value)
      end
    end
  end
end

#parentsObject



183
184
185
# File 'lib/graphiti/query.rb', line 183

def parents
  @parents ||= []
end

#render_link?(mode) ⇒ Boolean

Returns:

  • (Boolean)


85
86
87
88
89
# File 'lib/graphiti/query.rb', line 85

def render_link?(mode)
  return false if suppress_links?

  (mode == :on_demand) ? links_requested? : mode == true
end

#repeated_resource_classesObject

Deduplication exists for a row two include paths both resolve, so a resource class sitting at one node has nothing to collide with.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/graphiti/query.rb', line 44

def repeated_resource_classes
  return root.repeated_resource_classes unless root == self

  @repeated_resource_classes ||= begin
    counts = Hash.new(0)
    walk = [self]
    while (query = walk.pop)
      counts[query.resource.class] += 1
      query.sideloads.each_pair do |name, sideload_query|
        walk << sideload_query
        sideload = query.resource.class.sideload(name)
        next unless sideload.respond_to?(:children)

        # A polymorphic child resolves under a class no node names.
        sideload.children.each_value { |child| counts[child.resource.class] += 2 }
      end
    end
    counts.select { |_, count| count > 1 }.keys.to_set
  end
end

#resource_for_sideload(sideload) ⇒ Object



146
147
148
149
150
151
152
# File 'lib/graphiti/query.rb', line 146

def resource_for_sideload(sideload)
  if @resource.remote?
    RemoteSideloadResource.new
  else
    sideload.resource
  end
end

#rootObject



187
188
189
# File 'lib/graphiti/query.rb', line 187

def root
  parents.first || self
end

#sideload_hashObject



133
134
135
136
137
138
139
# File 'lib/graphiti/query.rb', line 133

def sideload_hash
  @sideload_hash = {}.tap do |hash|
    sideloads.each_pair do |key, value|
      hash[key] = sideloads[key].hash
    end
  end
end

#sideloadsObject



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/graphiti/query.rb', line 154

def sideloads
  @sideloads ||= {}.tap do |hash|
    include_hash.each_pair do |key, sub_hash|
      sideload = @resource.class.sideload(key)

      if sideload || @resource.remote?
        sl_resource = resource_for_sideload(sideload)
        query_parents = parents + [self]
        sub_hash = sub_hash[:include] if sub_hash.key?(:include)

        # NB: To handle on__<type>--<name>
        # A) relationship_name == :positions
        # B) key == on__employees.positions
        # This way A) ensures sideloads are resolved
        # And B) ensures nested filters, sorts etc still work
        relationship_name = sideload ? sideload.name : key
        hash[relationship_name] = Query.new sl_resource,
          @params,
          association_name: key,
          nested_include: sub_hash,
          parents: query_parents,
          action: :all
      else
        handle_missing_sideload(key)
      end
    end
  end
end

#sortsObject



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/graphiti/query.rb', line 230

def sorts
  @sorts ||= begin
    return @params[:sort] if @params[:sort].is_a?(Array)
    return [] if @params[:sort].nil?

    [].tap do |arr|
      sort_hashes do |key, value, type|
        if legacy_nested?(type)
          unless @resource.remote?
            @resource.get_attr!(key, :sortable, request: true)
          end
          arr << {key => value}
        elsif !type && top_level? && validate!(key, :sortable)
          arr << {key => value}
        elsif nested?("#{type}.#{key}")
          arr << {key => value}
        end
      end
    end
  end
end

#statsObject



288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/graphiti/query.rb', line 288

def stats
  @stats ||= {}.tap do |hash|
    (@params[:stats] || {}).each_pair do |k, v|
      if legacy_nested?(k)
        raise NotImplementedError.new("Association statistics are not currently supported")
      elsif top_level?
        v = v.split(",") if v.is_a?(String)
        hash[k.to_sym] = Array(v).flatten.map(&:to_sym)
      end
    end
  end
end

#suppress_links?Boolean

Returns:

  • (Boolean)


79
80
81
82
83
# File 'lib/graphiti/query.rb', line 79

def suppress_links?
  return @suppress_links if defined?(@suppress_links)

  @suppress_links = LINKLESS_FORMATS.include?(params[:format])
end

#top_level?Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/graphiti/query.rb', line 65

def top_level?
  !association?
end

#zero_results?Boolean

Returns:

  • (Boolean)


127
128
129
130
131
# File 'lib/graphiti/query.rb', line 127

def zero_results?
  !@params[:page].nil? &&
    !@params[:page][:size].nil? &&
    @params[:page][:size].to_i == 0
end