hq-graphql 2.1.1 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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