hq-graphql 2.0.6 → 2.1.0

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: 29c063cc8a29b6e30df91f11f8ed83bcaebee761fff76630bc2b2e80af9781d2
4
- data.tar.gz: cb3e76b3c272ee517fd9de9b71c65695b5379e8cd3f2a2b412edb85419b12fe3
3
+ metadata.gz: 05c40fc2e6eaccc1277c1b6f36b19983af507721b011d6d25c9010f877c18db7
4
+ data.tar.gz: cf67ac6f3d02f3c6a5c5b0f1124e8aacd706d1e30b0077af69cad336d5dc1475
5
5
  SHA512:
6
- metadata.gz: 992f59fa59af5c4d3d06a5902a85dde3a1839c881ac25fa15586ff8a92d15e4cde75192159a9ec0e939c286c99a2e83a2390203b84bf3de0c151bbf796945213
7
- data.tar.gz: 242640df93c46a0a1d8f1af7851bf659901b896ae8c5f2e4088ece50909fdd31ea81cd20c6809a5e24f2c965b36db74bfdcb081c18ef698b062f3fb14ad54038
6
+ metadata.gz: 2f643af54cb80d2325dc8829c620f9c4d0c48a950aced92cb0313b2aacba52a3b797f42deb0c1f8848b9454cf70d014c4795f64d5e34444d87cc4fdbe36702c8
7
+ data.tar.gz: df55f4034030bed0f035a170d581bfe030f62a2545b2f4b7d15d48838c9777a7e4aa357e5f86618ee7bb692ba001c263df9973f8e546f8a1a902d0e8f61d4821
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,23 +54,29 @@ 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
67
  require "hq/graphql/active_record_extensions"
68
+ require "hq/graphql/association_loader"
62
69
  require "hq/graphql/scalars"
63
70
  require "hq/graphql/comparator"
71
+ require "hq/graphql/enum"
64
72
  require "hq/graphql/inputs"
65
73
  require "hq/graphql/input_object"
66
- require "hq/graphql/loaders"
67
74
  require "hq/graphql/mutation"
68
75
  require "hq/graphql/object"
76
+ require "hq/graphql/paginated_association_loader"
69
77
  require "hq/graphql/resource"
70
78
  require "hq/graphql/root_mutation"
71
79
  require "hq/graphql/root_query"
80
+ require "hq/graphql/schema"
72
81
  require "hq/graphql/types"
73
82
  require "hq/graphql/engine"
@@ -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,15 +29,11 @@ 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
36
36
 
37
- def dump_schema_to_file(directory:, filename:, schema:)
38
- ::FileUtils.mkdir_p(directory)
39
- ::File.open(::File.join(directory, filename), "w") { |file| file.write(schema.to_definition) }
40
- end
41
-
42
37
  private
43
38
 
44
39
  def convert_schema_to_string(schema)
@@ -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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HQ::GraphQL
4
+ class Enum < ::GraphQL::Schema::Enum
5
+ ## Auto generate enums from the database using ActiveRecord
6
+ # This comes in handy when we have constants that we want represented as enums.
7
+ #
8
+ # == Example
9
+ # Let's assume we're saving data into a user types table
10
+ # # select * from user_types;
11
+ # id | name
12
+ # --- +-------------
13
+ # 1 | Admin
14
+ # 2 | Support User
15
+ # (2 rows)
16
+ #
17
+ # ```ruby
18
+ # class Enums::UserType < ::HQ::GraphQL::Enum
19
+ # with_model
20
+ # end
21
+ # ```
22
+ #
23
+ # Creates the following enum:
24
+ # ```graphql
25
+ # enum UserType {
26
+ # Admin
27
+ # SupportUser
28
+ # }
29
+ # ```
30
+ def self.with_model(
31
+ klass = default_model_name.safe_constantize,
32
+ prefix: nil,
33
+ register: true,
34
+ scope: nil,
35
+ value_method: :name
36
+ )
37
+ raise ArgumentError.new(<<~ERROR) if !klass
38
+ `::HQ::GraphQL::Enum.with_model {...}' had trouble automatically inferring the class name.
39
+ Avoid this by manually passing in the class name: `::HQ::GraphQL::Enum.with_model(#{default_model_name}) {...}`
40
+ ERROR
41
+
42
+ if register
43
+ ::HQ::GraphQL.enums << klass
44
+ ::HQ::GraphQL::Types.register(klass, self)
45
+ end
46
+
47
+ lazy_load do
48
+ records = scope ? klass.instance_exec(&scope) : klass.all
49
+ records.each do |record|
50
+ value "#{prefix}#{record.send(value_method).delete(" ")}", value: record
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.lazy_load(&block)
56
+ @lazy_load ||= []
57
+ @lazy_load << block if block
58
+ @lazy_load
59
+ end
60
+
61
+ def self.lazy_load!
62
+ lazy_load.map(&:call)
63
+ @lazy_load = []
64
+ end
65
+
66
+ def self.to_graphql
67
+ lazy_load!
68
+ super
69
+ end
70
+
71
+ def self.default_model_name
72
+ to_s.sub(/^((::)?\w+)::/, "")
73
+ end
74
+ end
75
+ end
76
+
77
+ require "hq/graphql/enum/sort_by"
78
+ require "hq/graphql/enum/sort_order"