Class: ForestAdminDatasourceGraphqlHasura::Query::QueryBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb

Overview

Builds Hasura GraphQL operations (queries and mutations) with variables. All methods return { query:, variables: }.

names is { root:, base:, aggregate:, insert:, update:, delete: }: root is the select root field, base (the GraphQL type name) is what the generated type names derive from — <base>_bool_exp, <base>_insert_input… — and the operation roots carry their resolved names, custom_root_fields applied when the metadata declares them.

Class Method Summary collapse

Class Method Details

.aggregate(names, filter, aggregation, extra_where: nil) ⇒ Object

extra_where is a raw bool_exp and-combined with the converted filter (the null-bucket query adds { fk => { _is_null => true } }).



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 93

def aggregate(names, filter, aggregation, extra_where: nil)
  args = []
  var_defs = []
  variables = {}

  where = combine(FilterConverter.convert(filter.condition_tree), extra_where)

  if where
    var_defs << "$where: #{names[:base]}_bool_exp"
    args << 'where: $where'
    variables['where'] = where
  end

  query = <<~GRAPHQL
    query Aggregate#{camelize(names[:base])}#{wrap(var_defs)} {
      #{names[:aggregate]}#{wrap(args)} {
        aggregate {
          #{aggregation_selection(aggregation)}
        }
      }
    }
  GRAPHQL

  { query: query, variables: variables }
end

.aggregation_selection(aggregation) ⇒ Object

row_count tells a group with no rows at all (SQL grouping omits it) from one whose rows exist but hold NULL in the aggregated column (SQL keeps it, at zero for a count and at NULL otherwise).



178
179
180
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 178

def aggregation_selection(aggregation)
  "#{operation_selection(aggregation)}#{avg_merge_selection(aggregation)}\nrow_count: count"
end

.avg_merge_selection(aggregation) ⇒ Object

An average cannot be merged across parent rows sharing a group value; its sum and non-null count can, weighting it exactly.



192
193
194
195
196
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 192

def avg_merge_selection(aggregation)
  return '' unless aggregation.operation == 'Avg'

  "\navg_sum: sum { #{aggregation.field} }\navg_count: count(columns: #{aggregation.field})"
end

.create(names, records, selection) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 42

def create(names, records, selection)
  query = <<~GRAPHQL
    mutation Insert#{camelize(names[:base])}($objects: [#{names[:base]}_insert_input!]!) {
      #{names[:insert]}(objects: $objects) {
        returning {
          #{selection.join("\n      ")}
        }
      }
    }
  GRAPHQL

  { query: query, variables: { 'objects' => records.map { |record| stringify_keys(record) } } }
end

.delete(names, filter) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 77

def delete(names, filter)
  query = <<~GRAPHQL
    mutation Delete#{camelize(names[:base])}($where: #{names[:base]}_bool_exp!) {
      #{names[:delete]}(where: $where) {
        affected_rows
      }
    }
  GRAPHQL

  # `{}` (match all) is deliberate here: a bulk delete with "select all"
  # legitimately carries no condition, and wiping is the requested semantic.
  { query: query, variables: { 'where' => FilterConverter.convert(filter.condition_tree) || {} } }
end

.grouped_aggregate(names, relation, filter, aggregation, page) ⇒ Object

relation is { parent_table:, parent_field:, relation_name:, parent_order_fields: }, page is { limit:, offset: }. Parents are ordered by their primary key so offset pagination is stable, and filtered by the chart's predicate through the relationship, so the pages only walk parents owning at least one matching child row.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 124

def grouped_aggregate(names, relation, filter, aggregation, page)
  args = []
  var_defs = ['$parentLimit: Int', '$parentOffset: Int']
  variables = { 'parentLimit' => page[:limit], 'parentOffset' => page[:offset] }
  parent_args = ['limit: $parentLimit', 'offset: $parentOffset', parent_order(relation)]

  where = FilterConverter.convert(filter.condition_tree)

  if where
    var_defs << "$where: #{names[:base]}_bool_exp"
    args << 'where: $where'
    parent_args << "where: { #{relation[:relation_name]}: $where }"
    variables['where'] = where
  end

  query = <<~GRAPHQL
    query Aggregate#{camelize(relation[:parent_table])}#{wrap(var_defs)} {
      #{relation[:parent_table]}#{wrap(parent_args)} {
        #{relation[:parent_field]}
        #{relation[:relation_name]}_aggregate#{wrap(args)} {
          aggregate {
            #{aggregation_selection(aggregation)}
          }
        }
      }
    }
  GRAPHQL

  { query: query, variables: variables }
end

.list(names, filter, selection) ⇒ Object

selection holds resolved GraphQL fields, nested relations included ("membership { id full_name }").



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 15

def list(names, filter, selection)
  args = []
  var_defs = []
  variables = {}

  where = FilterConverter.convert(filter.condition_tree)

  if where
    var_defs << "$where: #{names[:base]}_bool_exp"
    args << 'where: $where'
    variables['where'] = where
  end

  add_sort(names, filter, args, var_defs, variables)
  add_pagination(filter, args, var_defs, variables)

  query = <<~GRAPHQL
    query List#{camelize(names[:root])}#{wrap(var_defs)} {
      #{names[:root]}#{wrap(args)} {
        #{selection.join("\n    ")}
      }
    }
  GRAPHQL

  { query: query, variables: variables }
end

.operation_selection(aggregation) ⇒ Object



182
183
184
185
186
187
188
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 182

def operation_selection(aggregation)
  if aggregation.operation == 'Count'
    aggregation.field ? "count(columns: #{aggregation.field})" : 'count'
  else
    "#{aggregation.operation.downcase} { #{aggregation.field} }"
  end
end

.orphan_keys(names, filter, column, relation_name, limit) ⇒ Object

Distinct values of column among rows without a matching parent — the dangling foreign keys a grouped chart must keep as groups of their own. distinct_on requires the matching order_by.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 158

def orphan_keys(names, filter, column, relation_name, limit)
  where = combine(
    FilterConverter.convert(filter.condition_tree),
    { '_and' => [{ '_not' => { relation_name => {} } }, { column => { '_is_null' => false } }] }
  )

  query = <<~GRAPHQL
    query OrphanKeys#{camelize(names[:root])}($where: #{names[:base]}_bool_exp, $limit: Int) {
      #{names[:root]}(where: $where, distinct_on: [#{column}], order_by: [{ #{column}: asc }], limit: $limit) {
        #{column}
      }
    }
  GRAPHQL

  { query: query, variables: { 'where' => where, 'limit' => limit } }
end

.update(names, filter, patch) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/forest_admin_datasource_graphql_hasura/query/query_builder.rb', line 56

def update(names, filter, patch)
  where = FilterConverter.convert(filter.condition_tree)

  # Backstop behind the collection guard: `{}` is vacuously true for
  # Hasura, so a filterless update would rewrite the whole table.
  if where.nil?
    raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
          "Refusing to update every row of '#{names[:root]}': the filter carries no condition."
  end

  query = <<~GRAPHQL
    mutation Update#{camelize(names[:base])}($where: #{names[:base]}_bool_exp!, $set: #{names[:base]}_set_input!) {
      #{names[:update]}(where: $where, _set: $set) {
        affected_rows
      }
    }
  GRAPHQL

  { query: query, variables: { 'where' => where, 'set' => stringify_keys(patch) } }
end