hq-graphql 2.1.1 → 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5aefcd1b96fb05c456ebdef007f9bcad196e7aba2cd864d6dd7c83f3b3fbc95c
4
- data.tar.gz: 0173c91a781e4872cbf65c597f3266030850c8e0ba31b05d8f9b578f11263216
3
+ metadata.gz: 6bec5e8d7faf20cfc45c6420b0f989aee48ca2b5c6f72a3d2a9ff1d4be030530
4
+ data.tar.gz: 6d71560948e242279e32ec521a86c351e7c2278af78694581f5e1bf27b5807a5
5
5
  SHA512:
6
- metadata.gz: 8a7b74ef1631a0d98004beb6bec61ea6898994c3ff25366afbc1fa5ba47bc60e36061ea3875c655ecfc9ac088b5f809b28fb411c05bfaeeda4e7256742345d5e
7
- data.tar.gz: 3f6ecdc50f8a59d0e4c5c204fe4ed5967064e82f0f4a9aa961277532d6a49be5fb534204fa03a928777133e1b9541ec86fd2f9b196c383ff9da2ce1c94736cdc
6
+ metadata.gz: c62a8b032a5f43dd27c8a50685739cc6a07d21bab55a7b4a6dc750377080f9a3440c6954a129ef56c1c4bd60a90f4d390efae6937875d949c66a5b26b5ab918b
7
+ data.tar.gz: 40966a645c6777ee54311a8e8eb3f7df001413fdd91d1bcb21e1928c947299a331e69a18960fba6f0102996559f06c2dc56bdeb7ba611b5707b3f7830e61553d
@@ -8,6 +8,10 @@ require "hq/graphql/config"
8
8
 
9
9
  module HQ
10
10
  module GraphQL
11
+ class << self
12
+ delegate :default_object_class, to: :config
13
+ end
14
+
11
15
  def self.config
12
16
  @config ||= ::HQ::GraphQL::Config.new
13
17
  end
@@ -73,6 +77,7 @@ require "hq/graphql/input_object"
73
77
  require "hq/graphql/mutation"
74
78
  require "hq/graphql/object"
75
79
  require "hq/graphql/paginated_association_loader"
80
+ require "hq/graphql/record_loader"
76
81
  require "hq/graphql/resource"
77
82
  require "hq/graphql/root_mutation"
78
83
  require "hq/graphql/root_query"
@@ -27,7 +27,7 @@ module HQ
27
27
  end
28
28
 
29
29
  def lazy_load!
30
- lazy_load.map(&:call)
30
+ lazy_load.each(&:call)
31
31
  @lazy_load = []
32
32
  end
33
33
 
@@ -5,6 +5,7 @@ module HQ
5
5
  class Config < Struct.new(
6
6
  :authorize,
7
7
  :authorize_field,
8
+ :default_object_class,
8
9
  :default_scope,
9
10
  :extract_class,
10
11
  :resource_lookup,
@@ -63,13 +63,12 @@ module HQ
63
63
  end
64
64
 
65
65
  def field_from_association(association, auto_nil:, internal_association: false, &block)
66
- # The PaginationAssociationLoader doesn't support through associations yet
67
- return if association.through_reflection? && ::HQ::GraphQL.use_experimental_associations?
68
-
69
66
  association_klass = association.klass
70
- name = association.name
71
- klass = model_klass
72
- type = Types[association_klass]
67
+ name = association.name.to_s
68
+ return if fields[name]
69
+
70
+ klass = model_klass
71
+ type = Types[association_klass]
73
72
  case association.macro
74
73
  when :has_many
75
74
  field name, [type], null: false, klass: model_name do
@@ -91,7 +90,10 @@ module HQ
91
90
  end
92
91
 
93
92
  def field_from_column(column, auto_nil:)
94
- field column.name, Types.type_from_column(column), null: !auto_nil || column.null
93
+ name = column.name
94
+ return if fields[name]
95
+
96
+ field name, Types.type_from_column(column), null: !auto_nil || column.null
95
97
  end
96
98
 
97
99
  def association_required?(association)
@@ -3,6 +3,33 @@
3
3
  module HQ
4
4
  module GraphQL
5
5
  module ObjectAssociation
6
+ def reflect_on_association(association)
7
+ resource_reflections[association.to_s]&.reflection(model_klass)
8
+ end
9
+
10
+ def update(name, &block)
11
+ resource_reflections[name.to_s] = UpdatedReflection.new(name, block)
12
+ end
13
+
14
+ def belongs_to(name, scope = nil, **options, &block)
15
+ add_reflection(name, scope, options, :belongs_to, block)
16
+ end
17
+
18
+ def has_many(name, scope = nil, through: nil, **options, &block)
19
+ raise TypeError, "has_many through is unsupported" if through
20
+ add_reflection(name, scope, options, :has_many, block)
21
+ end
22
+
23
+ private
24
+
25
+ def resource_reflections
26
+ @resource_reflections ||= {}
27
+ end
28
+
29
+ def add_reflection(name, scope, options, macro, block)
30
+ resource_reflections[name.to_s] = ResourceReflection.new(name, scope, options, macro, block)
31
+ end
32
+
6
33
  class ResourceReflection
7
34
  attr_reader :name, :scope, :options, :macro, :block
8
35
 
@@ -23,27 +50,17 @@ module HQ
23
50
  end
24
51
  end
25
52
 
26
- def reflect_on_association(association)
27
- resource_reflections[association.to_s]&.reflection(model_klass)
28
- end
29
-
30
- def belongs_to(name, scope = nil, **options, &block)
31
- add_reflection(name, scope, options, :belongs_to, block)
32
- end
33
-
34
- def has_many(name, scope = nil, through: nil, **options, &block)
35
- raise TypeError, "has_many through is unsupported" if through
36
- add_reflection(name, scope, options, :has_many, block)
37
- end
53
+ class UpdatedReflection
54
+ attr_reader :name, :block
38
55
 
39
- private
40
-
41
- def resource_reflections
42
- @resource_reflections ||= {}
43
- end
56
+ def initialize(name, block)
57
+ @name = name
58
+ @block = block
59
+ end
44
60
 
45
- def add_reflection(name, scope, options, macro, block)
46
- resource_reflections[name.to_s] = ResourceReflection.new(name, scope, options, macro, block)
61
+ def reflection(model_klass)
62
+ model_klass.reflect_on_association(name)
63
+ end
47
64
  end
48
65
  end
49
66
  end
@@ -23,7 +23,7 @@ module HQ
23
23
  @limit = [0, limit].max if limit
24
24
  @offset = [0, offset].max if offset
25
25
  @scope = scope
26
- @sort_by = sort_by || :updated_at
26
+ @sort_by = sort_by || :created_at
27
27
  @sort_order = normalize_sort_order(sort_order)
28
28
 
29
29
  validate!
@@ -39,7 +39,8 @@ module HQ
39
39
  end
40
40
 
41
41
  def perform(records)
42
- scope =
42
+ values = records.map { |r| source_value(r) }
43
+ scope =
43
44
  if @limit || @offset
44
45
  # If a limit or offset is added, then we need to transform the query
45
46
  # into a lateral join so that we can limit on groups of data.
@@ -57,53 +58,70 @@ module HQ
57
58
  # > ) a_top ON TRUE
58
59
  # > WHERE addresses.user_id IN ($1, $2, ..., $N)
59
60
  # > ORDER BY a_top.created_at DESC
60
- inner_table = association_class.arel_table
61
- association_table = inner_table.alias("outer")
61
+ inner_table = association_class.arel_table
62
+ lateral_join_table = through_reflection? ? through_association.klass.arel_table : inner_table
63
+ from_table = lateral_join_table.alias("outer")
62
64
 
63
65
  inside_scope = default_scope.
64
66
  select(inner_table[::Arel.star]).
65
- from(inner_table).
66
- where(inner_table[association_key].eq(association_table[association_key])).
67
+ where(lateral_join_table[target_join_key].eq(from_table[target_join_key])).
67
68
  reorder(arel_order(inner_table)).
68
69
  limit(@limit).
69
70
  offset(@offset)
70
71
 
71
- outside_table = ::Arel::Table.new("top")
72
+ if through_reflection?
73
+ # expose the through_reflection key
74
+ inside_scope = inside_scope.select(lateral_join_table[target_join_key])
75
+ end
76
+
77
+ lateral_table = ::Arel::Table.new("top")
72
78
  association_class.
73
- select(outside_table[::Arel.star]).distinct.
74
- from(association_table).
75
- joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{outside_table.name} ON TRUE").
76
- where(association_table[association_key].in(records.map { |r| join_value(r) })).
77
- reorder(arel_order(outside_table))
79
+ select(lateral_table[::Arel.star]).distinct.
80
+ from(from_table).
81
+ where(from_table[target_join_key].in(values)).
82
+ joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{lateral_table.name} ON TRUE").
83
+ reorder(arel_order(lateral_table))
78
84
  else
79
- default_scope.
80
- reorder(arel_order(association_class.arel_table)).
81
- where(association_key => records.map { |r| join_value(r) })
85
+ scope = default_scope.reorder(arel_order(association_class.arel_table))
86
+
87
+ if through_reflection?
88
+ scope.where(through_association.name => { target_join_key => values }).
89
+ # expose the through_reflection key
90
+ select(association_class.arel_table[::Arel.star], through_association.klass.arel_table[target_join_key])
91
+ else
92
+ scope.where(target_join_key => values)
93
+ end
82
94
  end
83
95
 
84
96
  results = scope.to_a
85
97
  records.each do |record|
86
- fulfill(record, association_value(record, results)) unless fulfilled?(record)
98
+ fulfill(record, target_value(record, results)) unless fulfilled?(record)
87
99
  end
88
100
  end
89
101
 
90
102
  private
91
103
 
92
- def association_key
93
- belongs_to? ? association.association_primary_key : association.foreign_key
104
+ def source_join_key
105
+ belongs_to? ? association.foreign_key : association.association_primary_key
94
106
  end
95
107
 
96
- def association_value(record, results)
97
- enumerator = has_many? ? :select : :detect
98
- results.send(enumerator) { |r| r.send(association_key) == join_value(record) }
108
+ def source_value(record)
109
+ record.send(source_join_key)
99
110
  end
100
111
 
101
- def join_key
102
- belongs_to? ? association.foreign_key : association.association_primary_key
112
+ def target_join_key
113
+ if through_reflection?
114
+ through_association.foreign_key
115
+ elsif belongs_to?
116
+ association.association_primary_key
117
+ else
118
+ association.foreign_key
119
+ end
103
120
  end
104
121
 
105
- def join_value(record)
106
- record.send(join_key)
122
+ def target_value(record, results)
123
+ enumerator = has_many? ? :select : :detect
124
+ results.send(enumerator) { |r| r.send(target_join_key) == source_value(record) }
107
125
  end
108
126
 
109
127
  def default_scope
@@ -111,6 +129,14 @@ module HQ
111
129
  scope = association.scopes.reduce(scope, &:merge)
112
130
  scope = association_class.default_scopes.reduce(scope, &:merge)
113
131
  scope = scope.merge(@scope) if @scope
132
+
133
+ if through_reflection?
134
+ source = association_class.arel_table
135
+ target = through_association.klass.arel_table
136
+ join = source.join(target).on(target[association.foreign_key].eq(source[source_join_key]))
137
+ scope = scope.joins(join.join_sources)
138
+ end
139
+
114
140
  scope
115
141
  end
116
142
 
@@ -122,6 +148,14 @@ module HQ
122
148
  association.macro == :has_many
123
149
  end
124
150
 
151
+ def through_association
152
+ association.through_reflection
153
+ end
154
+
155
+ def through_reflection?
156
+ association.through_reflection?
157
+ end
158
+
125
159
  def association
126
160
  if @internal_association
127
161
  Types[@model].reflect_on_association(@association_name)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HQ
4
+ module GraphQL
5
+ class RecordLoader < ::GraphQL::Batch::Loader
6
+ def initialize(model, column: model.primary_key, where: nil)
7
+ @model = model
8
+ @column = column.to_s
9
+ @column_type = model.type_for_attribute(@column)
10
+ @where = where
11
+ end
12
+
13
+ def load(key)
14
+ super(@column_type.cast(key))
15
+ end
16
+
17
+ def perform(keys)
18
+ query(keys).each { |record| fulfill(record.public_send(@column), record) }
19
+ keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
20
+ end
21
+
22
+ private
23
+
24
+ def query(keys)
25
+ scope = @model
26
+ scope = scope.where(@where) if @where
27
+ scope.where(@column => keys)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -59,12 +59,20 @@ module HQ
59
59
  @input_klass ||= build_input_object
60
60
  end
61
61
 
62
- def nil_query_klass
63
- @nil_query_klass ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
64
- end
65
-
66
- def query_klass
67
- @query_klass ||= build_graphql_object
62
+ def nil_query_object
63
+ @nil_query_object ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
64
+ end
65
+
66
+ def query_object
67
+ @query_object ||= begin
68
+ if @query_object_options
69
+ options, block = @query_object_options
70
+ @query_object_options = nil
71
+ build_graphql_object(**options, &block)
72
+ else
73
+ build_graphql_object
74
+ end
75
+ end
68
76
  end
69
77
 
70
78
  def sort_fields_enum
@@ -89,7 +97,11 @@ module HQ
89
97
  end
90
98
 
91
99
  def query(**options, &block)
92
- @query_klass = build_graphql_object(**options, &block)
100
+ @query_object_options = [options, block]
101
+ end
102
+
103
+ def query_class(klass)
104
+ @query_class = klass
93
105
  end
94
106
 
95
107
  def sort_fields(*fields)
@@ -100,7 +112,7 @@ module HQ
100
112
  resource = self
101
113
  resolver = -> {
102
114
  Class.new(::GraphQL::Schema::Resolver) do
103
- type = is_array ? [resource.query_klass] : resource.query_klass
115
+ type = is_array ? [resource.query_object] : resource.query_object
104
116
  type type, null: null
105
117
  class_eval(&block) if block
106
118
  end
@@ -156,7 +168,8 @@ module HQ
156
168
  def build_graphql_object(name: graphql_name, **options, &block)
157
169
  scoped_graphql_name = name
158
170
  scoped_model_name = model_name
159
- Class.new(::HQ::GraphQL::Object) do
171
+ object_class = @query_class || ::HQ::GraphQL.default_object_class || ::HQ::GraphQL::Object
172
+ Class.new(object_class) do
160
173
  graphql_name scoped_graphql_name
161
174
 
162
175
  with_model scoped_model_name, **options
@@ -13,7 +13,7 @@ module HQ
13
13
  def self.registry
14
14
  @registry ||= Hash.new do |hash, options|
15
15
  klass, nil_klass = Array(options)
16
- hash[klass] = nil_klass ? nil_query_klass(klass) : klass_for(klass)
16
+ hash[options] = nil_klass ? nil_query_object(klass) : klass_for(klass)
17
17
  end
18
18
  end
19
19
 
@@ -42,6 +42,10 @@ module HQ
42
42
  ::GraphQL::Types::Float
43
43
  when :boolean
44
44
  ::GraphQL::Types::Boolean
45
+ when :date
46
+ ::GraphQL::Types::ISO8601Date
47
+ when :datetime
48
+ ::GraphQL::Types::ISO8601DateTime
45
49
  else
46
50
  ::GraphQL::Types::String
47
51
  end
@@ -57,12 +61,12 @@ module HQ
57
61
  class << self
58
62
  private
59
63
 
60
- def nil_query_klass(klass_or_string)
61
- find_klass(klass_or_string, :nil_query_klass)
64
+ def nil_query_object(klass_or_string)
65
+ find_klass(klass_or_string, :nil_query_object)
62
66
  end
63
67
 
64
68
  def klass_for(klass_or_string)
65
- find_klass(klass_or_string, :query_klass)
69
+ find_klass(klass_or_string, :query_object)
66
70
  end
67
71
 
68
72
  def find_klass(klass_or_string, method)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
- VERSION = "2.1.1"
5
+ VERSION = "2.1.6"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hq-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Jones
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-22 00:00:00.000000000 Z
11
+ date: 2020-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -264,6 +264,7 @@ files:
264
264
  - lib/hq/graphql/object.rb
265
265
  - lib/hq/graphql/object_association.rb
266
266
  - lib/hq/graphql/paginated_association_loader.rb
267
+ - lib/hq/graphql/record_loader.rb
267
268
  - lib/hq/graphql/resource.rb
268
269
  - lib/hq/graphql/resource/auto_mutation.rb
269
270
  - lib/hq/graphql/root_mutation.rb