hq-graphql 2.0.7 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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