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.
- checksums.yaml +4 -4
- data/README.md +28 -0
- data/lib/hq-graphql.rb +0 -2
- data/lib/hq/graphql.rb +28 -21
- data/lib/hq/graphql/active_record_extensions.rb +23 -27
- data/lib/hq/graphql/association_loader.rb +49 -0
- data/lib/hq/graphql/comparator.rb +1 -1
- data/lib/hq/graphql/config.rb +17 -11
- data/lib/hq/graphql/engine.rb +0 -1
- data/lib/hq/graphql/enum.rb +77 -0
- data/lib/hq/graphql/enum/sort_by.rb +10 -0
- data/lib/hq/graphql/enum/sort_order.rb +10 -0
- data/lib/hq/graphql/field.rb +12 -11
- data/lib/hq/graphql/field_extension/association_loader_extension.rb +15 -0
- data/lib/hq/graphql/field_extension/paginated_arguments.rb +22 -0
- data/lib/hq/graphql/field_extension/paginated_loader.rb +45 -0
- data/lib/hq/graphql/input_object.rb +12 -7
- data/lib/hq/graphql/inputs.rb +4 -3
- data/lib/hq/graphql/mutation.rb +0 -1
- data/lib/hq/graphql/object.rb +42 -11
- data/lib/hq/graphql/object_association.rb +50 -0
- data/lib/hq/graphql/paginated_association_loader.rb +158 -0
- data/lib/hq/graphql/resource.rb +47 -156
- data/lib/hq/graphql/resource/auto_mutation.rb +163 -0
- data/lib/hq/graphql/root_mutation.rb +1 -2
- data/lib/hq/graphql/root_query.rb +0 -1
- data/lib/hq/graphql/scalars.rb +0 -1
- data/lib/hq/graphql/schema.rb +1 -1
- data/lib/hq/graphql/types.rb +22 -8
- data/lib/hq/graphql/types/object.rb +7 -11
- data/lib/hq/graphql/types/uuid.rb +7 -14
- data/lib/hq/graphql/version.rb +1 -2
- metadata +12 -39
- data/lib/hq/graphql/loaders.rb +0 -4
- data/lib/hq/graphql/loaders/association.rb +0 -52
- data/lib/hq/graphql/resource/mutation.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5aefcd1b96fb05c456ebdef007f9bcad196e7aba2cd864d6dd7c83f3b3fbc95c
|
4
|
+
data.tar.gz: 0173c91a781e4872cbf65c597f3266030850c8e0ba31b05d8f9b578f11263216
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/hq-graphql.rb
CHANGED
data/lib/hq/graphql.rb
CHANGED
@@ -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 ||
|
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 ||
|
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
|
-
|
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
|
-
@
|
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
|
-
|
55
|
-
|
56
|
-
|
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/
|
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
|
-
|
17
|
-
extend
|
18
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
data/lib/hq/graphql/config.rb
CHANGED
@@ -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 <
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
data/lib/hq/graphql/engine.rb
CHANGED
@@ -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
|