hq-graphql 2.0.7 → 2.1.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -0
  3. data/lib/hq-graphql.rb +0 -2
  4. data/lib/hq/graphql.rb +28 -21
  5. data/lib/hq/graphql/active_record_extensions.rb +23 -27
  6. data/lib/hq/graphql/association_loader.rb +49 -0
  7. data/lib/hq/graphql/comparator.rb +1 -1
  8. data/lib/hq/graphql/config.rb +17 -11
  9. data/lib/hq/graphql/engine.rb +0 -1
  10. data/lib/hq/graphql/enum.rb +77 -0
  11. data/lib/hq/graphql/enum/sort_by.rb +10 -0
  12. data/lib/hq/graphql/enum/sort_order.rb +10 -0
  13. data/lib/hq/graphql/field.rb +12 -11
  14. data/lib/hq/graphql/field_extension/association_loader_extension.rb +15 -0
  15. data/lib/hq/graphql/field_extension/paginated_arguments.rb +22 -0
  16. data/lib/hq/graphql/field_extension/paginated_loader.rb +45 -0
  17. data/lib/hq/graphql/input_object.rb +12 -7
  18. data/lib/hq/graphql/inputs.rb +4 -3
  19. data/lib/hq/graphql/mutation.rb +0 -1
  20. data/lib/hq/graphql/object.rb +42 -11
  21. data/lib/hq/graphql/object_association.rb +50 -0
  22. data/lib/hq/graphql/paginated_association_loader.rb +158 -0
  23. data/lib/hq/graphql/resource.rb +47 -156
  24. data/lib/hq/graphql/resource/auto_mutation.rb +163 -0
  25. data/lib/hq/graphql/root_mutation.rb +1 -2
  26. data/lib/hq/graphql/root_query.rb +0 -1
  27. data/lib/hq/graphql/scalars.rb +0 -1
  28. data/lib/hq/graphql/schema.rb +1 -1
  29. data/lib/hq/graphql/types.rb +22 -8
  30. data/lib/hq/graphql/types/object.rb +7 -11
  31. data/lib/hq/graphql/types/uuid.rb +7 -14
  32. data/lib/hq/graphql/version.rb +1 -2
  33. metadata +12 -39
  34. data/lib/hq/graphql/loaders.rb +0 -4
  35. data/lib/hq/graphql/loaders/association.rb +0 -52
  36. data/lib/hq/graphql/resource/mutation.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df65e3199361b5b97a2600a99ff594ba2194032a7c9f7d5553ba045264e311c4
4
- data.tar.gz: 7ada8061fd1cb4a5f4b5f66d42d0ab9304698320001ec0a97a6e8ca5962e4860
3
+ metadata.gz: 5aefcd1b96fb05c456ebdef007f9bcad196e7aba2cd864d6dd7c83f3b3fbc95c
4
+ data.tar.gz: 0173c91a781e4872cbf65c597f3266030850c8e0ba31b05d8f9b578f11263216
5
5
  SHA512:
6
- metadata.gz: a83be3fbfba6ca63deffcbcb379f5f21943d542e812eda2ff6d11e37eacb522d02b63d59bb66f9f9d22d0b8a6ef4d3920fe0b884e72bda0c46ea9b5819e7f859
7
- data.tar.gz: 03da6a427384a58b832e62119f21337b824b3129d3b2b73e7ee0a8cf79c7e6013ed04594f876aa322e631f4f0535189a0e02b88f64bd575b2f4f8cce0e1e9de7
6
+ metadata.gz: 8a7b74ef1631a0d98004beb6bec61ea6898994c3ff25366afbc1fa5ba47bc60e36061ea3875c655ecfc9ac088b5f809b28fb411c05bfaeeda4e7256742345d5e
7
+ data.tar.gz: 3f6ecdc50f8a59d0e4c5c204fe4ed5967064e82f0f4a9aa961277532d6a49be5fb534204fa03a928777133e1b9541ec86fd2f9b196c383ff9da2ce1c94736cdc
data/README.md CHANGED
@@ -78,6 +78,34 @@ class AdvisorResource
78
78
  end
79
79
  ```
80
80
 
81
+ ### Enums
82
+ Auto generate enums from the database using ActiveRecord
83
+ This comes in handy when we have constants that we want represented as enums.
84
+
85
+ #### Example
86
+ Let's assume we're saving data into a user types table
87
+ ```postgresl
88
+ # select * from user_types;
89
+ id | name
90
+ --- +-------------
91
+ 1 | Admin
92
+ 2 | Support User
93
+ (2 rows)
94
+ ```
95
+
96
+ ```ruby
97
+ class Enums::UserType < ::HQ::GraphQL::Enum
98
+ with_model
99
+ end
100
+ ```
101
+ This class automatically uses the UserType ActiveRecord model to generate an enum:
102
+ ```graphql
103
+ enum UserType {
104
+ Admin
105
+ SupportUser
106
+ }
107
+ ```
108
+
81
109
  ### Root Mutations
82
110
  Add mutations to your schema
83
111
 
@@ -2,6 +2,4 @@
2
2
  # rubocop:enable Naming/FileName
3
3
  # frozen_string_literal: true
4
4
 
5
- # typed: strict
6
-
7
5
  require "hq/graphql"
@@ -1,48 +1,51 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "rails"
5
4
  require "graphql"
6
5
  require "graphql/batch"
7
- require "sorbet-runtime"
8
6
  require "hq/graphql/field"
9
7
  require "hq/graphql/config"
10
8
 
11
9
  module HQ
12
10
  module GraphQL
13
- extend T::Sig
14
- extend T::Generic
15
-
16
- @config = T.let(::HQ::GraphQL::Config.new, ::HQ::GraphQL::Config)
17
-
18
- sig { returns(::HQ::GraphQL::Config) }
19
11
  def self.config
20
- @config
12
+ @config ||= ::HQ::GraphQL::Config.new
21
13
  end
22
14
 
23
15
  def self.configure(&block)
24
16
  config.instance_eval(&block)
25
17
  end
26
18
 
27
- sig { params(action: T.untyped, object: T.untyped, context: ::GraphQL::Query::Context).returns(T::Boolean) }
28
19
  def self.authorized?(action, object, context)
29
- !config.authorize || T.must(config.authorize).call(action, object, context)
20
+ !config.authorize || config.authorize.call(action, object, context)
30
21
  end
31
22
 
32
- sig { params(action: T.untyped, field: ::HQ::GraphQL::Field, object: T.untyped, context: ::GraphQL::Query::Context).returns(T::Boolean) }
33
23
  def self.authorize_field(action, field, object, context)
34
- !config.authorize_field || T.must(config.authorize_field).call(action, field, object, context)
24
+ !config.authorize_field || config.authorize_field.call(action, field, object, context)
35
25
  end
36
26
 
37
- sig { params(scope: T.untyped, context: ::GraphQL::Query::Context).returns(T.untyped) }
38
27
  def self.default_scope(scope, context)
39
28
  config.default_scope.call(scope, context)
40
29
  end
41
30
 
42
- sig { void }
31
+ def self.extract_class(klass)
32
+ config.extract_class.call(klass)
33
+ end
34
+
35
+ def self.lookup_resource(klass)
36
+ [klass, klass.base_class, klass.superclass].lazy.map do |k|
37
+ config.resource_lookup.call(k) || resources.detect { |r| r.model_klass == k }
38
+ end.reject(&:nil?).first
39
+ end
40
+
41
+ def self.use_experimental_associations?
42
+ !!config.use_experimental_associations
43
+ end
44
+
43
45
  def self.reset!
44
46
  @root_queries = nil
45
- @types = nil
47
+ @enums = nil
48
+ @resources = nil
46
49
  ::HQ::GraphQL::Inputs.reset!
47
50
  ::HQ::GraphQL::Types.reset!
48
51
  end
@@ -51,21 +54,25 @@ module HQ
51
54
  @root_queries ||= Set.new
52
55
  end
53
56
 
54
- # sig { returns(T::Set[::HQ::GraphQL::Resource]) }
55
- def self.types
56
- @types ||= Set.new
57
+ def self.enums
58
+ @enums ||= Set.new
59
+ end
60
+
61
+ def self.resources
62
+ @resources ||= Set.new
57
63
  end
58
64
  end
59
65
  end
60
66
 
61
- require "hq/graphql/active_record_extensions"
67
+ require "hq/graphql/association_loader"
62
68
  require "hq/graphql/scalars"
63
69
  require "hq/graphql/comparator"
70
+ require "hq/graphql/enum"
64
71
  require "hq/graphql/inputs"
65
72
  require "hq/graphql/input_object"
66
- require "hq/graphql/loaders"
67
73
  require "hq/graphql/mutation"
68
74
  require "hq/graphql/object"
75
+ require "hq/graphql/paginated_association_loader"
69
76
  require "hq/graphql/resource"
70
77
  require "hq/graphql/root_mutation"
71
78
  require "hq/graphql/root_query"
@@ -1,35 +1,31 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module HQ
5
4
  module GraphQL
6
5
  module ActiveRecordExtensions
7
- extend T::Sig
8
- extend T::Helpers
9
-
10
6
  class Error < StandardError
11
7
  MISSING_MODEL_MSG = "Your GraphQL object must be connected to a model: `::HQ::GraphQL::Object.with_model 'User'`"
12
8
  MISSING_ATTR_MSG = "Can't find attr %{model}.%{attr}'`"
13
9
  MISSING_ASSOC_MSG = "Can't find association %{model}.%{assoc}'`"
14
10
  end
15
11
 
16
- module ClassMethods
17
- extend T::Sig
18
- include Kernel
12
+ def self.included(klass)
13
+ klass.extend(ClassMethods)
14
+ end
19
15
 
16
+ module ClassMethods
20
17
  attr_accessor :model_name,
21
18
  :authorize_action,
22
19
  :auto_load_attributes,
23
- :auto_load_associations
20
+ :auto_load_associations,
21
+ :auto_load_enums
24
22
 
25
- sig { params(block: T.nilable(T.proc.void)).returns(T::Array[T.proc.void]) }
26
23
  def lazy_load(&block)
27
24
  @lazy_load ||= []
28
25
  @lazy_load << block if block
29
26
  @lazy_load
30
27
  end
31
28
 
32
- sig { void }
33
29
  def lazy_load!
34
30
  lazy_load.map(&:call)
35
31
  @lazy_load = []
@@ -50,12 +46,19 @@ module HQ
50
46
  end
51
47
 
52
48
  def model_associations
53
- model_associations =
54
- if auto_load_associations
55
- model_klass.reflect_on_all_associations
56
- else
57
- added_associations.map { |association| association_from_model(association) }
58
- end
49
+ model_associations = []
50
+ enums = model_klass.reflect_on_all_associations.select { |a| is_enum?(a) }
51
+ associatons = model_klass.reflect_on_all_associations - enums
52
+
53
+ if auto_load_enums
54
+ model_associations.concat(enums)
55
+ end
56
+
57
+ if auto_load_associations
58
+ model_associations.concat(associatons)
59
+ end
60
+
61
+ model_associations.concat(added_associations.map { |association| association_from_model(association) }).uniq
59
62
 
60
63
  # validate removed_associations exist
61
64
  removed_associations.each { |association| association_from_model(association) }
@@ -65,7 +68,6 @@ module HQ
65
68
 
66
69
  private
67
70
 
68
- sig { params(attrs: T.any(String, Symbol)).void }
69
71
  def add_attributes(*attrs)
70
72
  validate_model!
71
73
  added_attributes.concat attrs.map(&:to_sym)
@@ -74,7 +76,6 @@ module HQ
74
76
  alias_method :add_attrs, :add_attributes
75
77
  alias_method :add_attr, :add_attributes
76
78
 
77
- sig { params(attrs: T.any(String, Symbol)).void }
78
79
  def remove_attributes(*attrs)
79
80
  validate_model!
80
81
  removed_attributes.concat attrs.map(&:to_sym)
@@ -83,14 +84,12 @@ module HQ
83
84
  alias_method :remove_attrs, :remove_attributes
84
85
  alias_method :remove_attr, :remove_attributes
85
86
 
86
- sig { params(associations: T.any(String, Symbol)).void }
87
87
  def add_associations(*associations)
88
88
  validate_model!
89
89
  added_associations.concat associations.map(&:to_sym)
90
90
  end
91
91
  alias_method :add_association, :add_associations
92
92
 
93
- sig { params(associations: T.any(String, Symbol)).void }
94
93
  def remove_associations(*associations)
95
94
  validate_model!
96
95
  removed_associations.concat associations.map(&:to_sym)
@@ -101,7 +100,6 @@ module HQ
101
100
  @model_klass ||= model_name.constantize
102
101
  end
103
102
 
104
- sig { params(attr: Symbol).returns(T.untyped) }
105
103
  def column_from_model(attr)
106
104
  model_klass.columns_hash[attr.to_s] || raise(Error, Error::MISSING_ATTR_MSG % { model: model_name, attr: attr })
107
105
  end
@@ -110,34 +108,32 @@ module HQ
110
108
  model_klass.reflect_on_association(association) || raise(Error, Error::MISSING_ASSOC_MSG % { model: model_name, assoc: association })
111
109
  end
112
110
 
113
- sig { returns(T::Array[Symbol]) }
114
111
  def added_attributes
115
112
  @added_attributes ||= []
116
113
  end
117
114
 
118
- sig { returns(T::Array[Symbol]) }
119
115
  def removed_attributes
120
116
  @removed_attributes ||= []
121
117
  end
122
118
 
123
- sig { returns(T::Array[Symbol]) }
124
119
  def added_associations
125
120
  @added_associations ||= []
126
121
  end
127
122
 
128
- sig { returns(T::Array[Symbol]) }
129
123
  def removed_associations
130
124
  @removed_associations ||= []
131
125
  end
132
126
 
133
- sig { void }
127
+ def is_enum?(association)
128
+ ::HQ::GraphQL.enums.include?(association.klass)
129
+ end
130
+
134
131
  def validate_model!
135
132
  lazy_load do
136
133
  model_name || raise(Error, Error::MISSING_MODEL_MSG)
137
134
  end
138
135
  end
139
136
  end
140
- mixes_in_class_methods(ClassMethods)
141
137
  end
142
138
  end
143
139
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HQ
4
+ module GraphQL
5
+ class AssociationLoader < ::GraphQL::Batch::Loader
6
+ def initialize(model, association_name)
7
+ @model = model
8
+ @association_name = association_name
9
+ validate
10
+ end
11
+
12
+ def load(record)
13
+ raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
14
+ return Promise.resolve(read_association(record)) if association_loaded?(record)
15
+ super
16
+ end
17
+
18
+ # We want to load the associations on all records, even if they have the same id
19
+ def cache_key(record)
20
+ record.object_id
21
+ end
22
+
23
+ def perform(records)
24
+ preload_association(records)
25
+ records.each { |record| fulfill(record, read_association(record)) }
26
+ end
27
+
28
+ private
29
+
30
+ def validate
31
+ unless @model.reflect_on_association(@association_name)
32
+ raise ArgumentError, "No association #{@association_name} on #{@model}"
33
+ end
34
+ end
35
+
36
+ def preload_association(records)
37
+ ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
38
+ end
39
+
40
+ def read_association(record)
41
+ record.public_send(@association_name)
42
+ end
43
+
44
+ def association_loaded?(record)
45
+ record.association(@association_name).loaded?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,4 +1,3 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "graphql/schema_comparator"
@@ -30,6 +29,7 @@ module HQ
30
29
  if level >= CRITICALITY[:non_breaking]
31
30
  changes[:non_breaking] = result.non_breaking_changes
32
31
  end
32
+ return nil unless changes.values.flatten.any?
33
33
 
34
34
  changes
35
35
  end
@@ -1,18 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # typed: strict
4
-
5
3
  module HQ
6
4
  module GraphQL
7
- class Config < T::Struct
8
- AuthorizeProc = T.type_alias { T.nilable(T.proc.params(action: T.untyped, object: T.untyped, context: ::GraphQL::Query::Context).returns(T::Boolean)) }
9
- prop :authorize, AuthorizeProc, default: nil
10
-
11
- AuthorizeFieldProc = T.type_alias { T.nilable(T.proc.params(action: T.untyped, field: ::HQ::GraphQL::Field, object: T.untyped, context: ::GraphQL::Query::Context).returns(T::Boolean)) }
12
- prop :authorize_field, AuthorizeFieldProc, default: nil
13
-
14
- DefaultScopeProc = T.type_alias { T.proc.params(arg0: T.untyped, arg1: ::GraphQL::Query::Context).returns(T.untyped) }
15
- prop :default_scope, DefaultScopeProc, default: ->(scope, _context) { scope }
5
+ class Config < Struct.new(
6
+ :authorize,
7
+ :authorize_field,
8
+ :default_scope,
9
+ :extract_class,
10
+ :resource_lookup,
11
+ :use_experimental_associations,
12
+ keyword_init: true
13
+ )
14
+ def initialize(
15
+ default_scope: ->(scope, _context) { scope },
16
+ extract_class: ->(klass) { klass.to_s.gsub(/^Resources|Resource$/, "") },
17
+ resource_lookup: ->(klass) { "::Resources::#{klass}Resource".safe_constantize || "::Resources::#{klass}".safe_constantize },
18
+ **options
19
+ )
20
+ super
21
+ end
16
22
  end
17
23
  end
18
24
  end
@@ -1,4 +1,3 @@
1
- # typed: strong
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module HQ
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/types"
4
+
5
+ module HQ::GraphQL
6
+ class Enum < ::GraphQL::Schema::Enum
7
+ ## Auto generate enums from the database using ActiveRecord
8
+ # This comes in handy when we have constants that we want represented as enums.
9
+ #
10
+ # == Example
11
+ # Let's assume we're saving data into a user types table
12
+ # # select * from user_types;
13
+ # id | name
14
+ # --- +-------------
15
+ # 1 | Admin
16
+ # 2 | Support User
17
+ # (2 rows)
18
+ #
19
+ # ```ruby
20
+ # class Enums::UserType < ::HQ::GraphQL::Enum
21
+ # with_model
22
+ # end
23
+ # ```
24
+ #
25
+ # Creates the following enum:
26
+ # ```graphql
27
+ # enum UserType {
28
+ # Admin
29
+ # SupportUser
30
+ # }
31
+ # ```
32
+ def self.with_model(
33
+ klass = default_model_name.safe_constantize,
34
+ prefix: nil,
35
+ register: true,
36
+ scope: nil,
37
+ value_method: :name
38
+ )
39
+ raise ArgumentError.new(<<~ERROR) if !klass
40
+ `::HQ::GraphQL::Enum.with_model {...}' had trouble automatically inferring the class name.
41
+ Avoid this by manually passing in the class name: `::HQ::GraphQL::Enum.with_model(#{default_model_name}) {...}`
42
+ ERROR
43
+
44
+ if register
45
+ ::HQ::GraphQL.enums << klass
46
+ ::HQ::GraphQL::Types.register(klass, self)
47
+ end
48
+
49
+ lazy_load do
50
+ records = scope ? klass.instance_exec(&scope) : klass.all
51
+ records.each do |record|
52
+ value "#{prefix}#{record.send(value_method).delete(" ")}", value: record
53
+ end
54
+ end
55
+ end
56
+
57
+ def self.lazy_load(&block)
58
+ @lazy_load ||= []
59
+ @lazy_load << block if block
60
+ @lazy_load
61
+ end
62
+
63
+ def self.lazy_load!
64
+ lazy_load.map(&:call)
65
+ @lazy_load = []
66
+ end
67
+
68
+ def self.to_graphql
69
+ lazy_load!
70
+ super
71
+ end
72
+
73
+ def self.default_model_name
74
+ to_s.sub(/^((::)?\w+)::/, "")
75
+ end
76
+ end
77
+ end