hq-graphql 0.0.2 → 1.0.0
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 +93 -7
- data/lib/hq/graphql/active_record_extensions.rb +125 -0
- data/lib/hq/graphql/input_extensions.rb +56 -0
- data/lib/hq/graphql/input_object.rb +17 -0
- data/lib/hq/graphql/inputs.rb +32 -0
- data/lib/hq/graphql/mutation.rb +13 -0
- data/lib/hq/graphql/object.rb +11 -77
- data/lib/hq/graphql/resource/mutation.rb +32 -0
- data/lib/hq/graphql/resource.rb +199 -0
- data/lib/hq/graphql/root_mutation.rb +20 -0
- data/lib/hq/graphql/root_query.rb +29 -0
- data/lib/hq/graphql/scalars.rb +9 -0
- data/lib/hq/graphql/types/uuid.rb +3 -3
- data/lib/hq/graphql/types.rb +14 -22
- data/lib/hq/graphql/version.rb +1 -1
- data/lib/hq/graphql.rb +27 -11
- data/spec/internal/db/schema.rb +2 -2
- data/spec/lib/graphql/active_record_extensions_spec.rb +63 -0
- data/spec/lib/graphql/inputs_spec.rb +40 -0
- data/spec/lib/graphql/mutation_spec.rb +173 -0
- data/spec/lib/graphql/object_spec.rb +173 -0
- data/spec/lib/graphql/resource_spec.rb +374 -0
- data/spec/lib/graphql/types/uuid_spec.rb +65 -0
- data/spec/lib/graphql/types_spec.rb +40 -0
- data/spec/rails_helper.rb +2 -0
- metadata +28 -14
- data/spec/internal/app/graphql/query.rb +0 -18
- data/spec/internal/app/graphql/schema.rb +0 -3
- data/spec/internal/app/graphql/user_type.rb +0 -3
- data/spec/lib/object_spec.rb +0 -198
- data/spec/lib/types_spec.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b25a73fce273a48a00a185766cae51a9ec2e3150
|
4
|
+
data.tar.gz: 7c325eaef387c340778954fb83dbb3d0ccac9c03
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da27072503a7751c1eefa68ebdf6a0fcb77b28a6213fb8fb6a4d7fdec32d9f0bd14dfb585d0196f4d1119cd2d5ecb8d33e54c7c1e902051612e3fb7d9e2ab815
|
7
|
+
data.tar.gz: c5b52d4d4d8af7fe53b1c1aa50ad28b62eb36b66c2f39de2e6b4a51add6505c110b3e7d6182f0efcb302d53ca70c78ccf1d5817206eef413635d033832cc10af
|
data/README.md
CHANGED
@@ -7,20 +7,106 @@ OneHQ GraphQL interface to [Ruby Graphql](https://github.com/rmosolgo/graphql-ru
|
|
7
7
|
|
8
8
|
## Configuration
|
9
9
|
|
10
|
-
|
10
|
+
### Default Scope
|
11
|
+
Define a global default scope.
|
11
12
|
|
12
13
|
```ruby
|
13
|
-
# The gem assumes that if your model is called `MyModel`, the corresponding type is `MyModelType`.
|
14
|
-
# You can override that convention.
|
15
|
-
# Default is: -> (model_class) { "#{model_class.name.demodulize}Type" }
|
16
14
|
::HQ::GraphQL.config do |config|
|
17
|
-
config.
|
15
|
+
config.default_scope = ->(scope, context) do
|
16
|
+
scope.where(organization_id: context[:organization_id])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
## GraphQL Resource
|
22
|
+
Connect to ActiveRecord to auto generate queries and mutations.
|
23
|
+
|
24
|
+
### Queries
|
25
|
+
Include `::HQ::GraphQL::Resource` and set `self.model_name` to start using queries.
|
26
|
+
Fields will be created for all active record columns. Association fields will be created if the association
|
27
|
+
is also a GraphQL Resource.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class AdvisorResource
|
31
|
+
include ::HQ::GraphQL::Resource
|
32
|
+
|
33
|
+
# ActiveRecord Model Name
|
34
|
+
self.model_name = "Advisor"
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
#### Customize
|
39
|
+
```ruby
|
40
|
+
class AdvisorResource
|
41
|
+
include ::HQ::GraphQL::Resource
|
42
|
+
self.model_name = "Advisor"
|
43
|
+
|
44
|
+
# Turn off fields for active record associations
|
45
|
+
query attributes: true, associations: false do
|
46
|
+
# Create field for addresses
|
47
|
+
add_association :addresses
|
48
|
+
|
49
|
+
# add a custom field
|
50
|
+
field :custom_field, String, null: false
|
51
|
+
|
52
|
+
def custom_field
|
53
|
+
"Fizz"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
### Mutations
|
60
|
+
Mutations will not be created by default. Add `mutations` to a resource to build mutations for create, update, and destroy.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class AdvisorResource
|
64
|
+
include ::HQ::GraphQL::Resource
|
65
|
+
self.model_name = "Advisor"
|
66
|
+
|
67
|
+
# Builds the following mutations:
|
68
|
+
# createAdvisor
|
69
|
+
# updateAdvisor
|
70
|
+
# destroyAdvisor
|
71
|
+
mutations create: true, update: true, destroy: true
|
72
|
+
|
73
|
+
# Turn off fields for active record associations
|
74
|
+
inputs attributes: true, associations: false do
|
75
|
+
# Create input argument for addresses
|
76
|
+
add_association :addresses
|
77
|
+
end
|
18
78
|
end
|
19
79
|
```
|
20
80
|
|
21
|
-
|
81
|
+
### Root Mutations
|
82
|
+
Add mutations to your schema
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class RootMutation < ::HQ::GraphQL::RootMutation; end
|
86
|
+
|
87
|
+
class Schema < ::GraphQL::Schema
|
88
|
+
mutation(RootMutation)
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Root Queries
|
93
|
+
Create a root query:
|
94
|
+
```ruby
|
95
|
+
class AdvisorResource
|
96
|
+
include ::HQ::GraphQL::Resource
|
97
|
+
self.model_name = "Advisor"
|
98
|
+
|
99
|
+
root_query
|
100
|
+
end
|
101
|
+
|
102
|
+
class RootQuery < ::HQ::GraphQL::RootQuery; end
|
103
|
+
|
104
|
+
class Schema < ::GraphQL::Schema
|
105
|
+
mutation(RootQuery)
|
106
|
+
end
|
107
|
+
```
|
22
108
|
|
23
|
-
Create a new ::HQ::GraphQL::Object
|
109
|
+
## Create a new ::HQ::GraphQL::Object
|
24
110
|
```ruby
|
25
111
|
class AdvisorType < ::HQ::GraphQL::Object
|
26
112
|
# Supports graphql-ruby functionality
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module HQ
|
2
|
+
module GraphQL
|
3
|
+
module ActiveRecordExtensions
|
4
|
+
class Error < StandardError
|
5
|
+
MISSING_MODEL_MSG = "Your GraphQL object must be connected to a model: `::HQ::GraphQL::Object.with_model 'User'`".freeze
|
6
|
+
MISSING_ATTR_MSG = "Can't find attr %{model}.%{attr}'`".freeze
|
7
|
+
MISSING_ASSOC_MSG = "Can't find association %{model}.%{assoc}'`".freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
attr_accessor :model_name,
|
16
|
+
:auto_load_attributes,
|
17
|
+
:auto_load_associations
|
18
|
+
|
19
|
+
def lazy_load(&block)
|
20
|
+
@lazy_load ||= []
|
21
|
+
@lazy_load << block if block
|
22
|
+
@lazy_load
|
23
|
+
end
|
24
|
+
|
25
|
+
def lazy_load!
|
26
|
+
lazy_load.map(&:call)
|
27
|
+
@lazy_load = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def model_columns
|
31
|
+
model_columns =
|
32
|
+
if auto_load_attributes
|
33
|
+
model_klass.columns
|
34
|
+
else
|
35
|
+
added_attributes.map { |attr| column_from_model(attr) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# validate removed_attributes exist
|
39
|
+
removed_attributes.each { |attr| column_from_model(attr) }
|
40
|
+
|
41
|
+
model_columns.reject { |c| removed_attributes.include?(c.name.to_sym) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def model_associations
|
45
|
+
model_associations =
|
46
|
+
if auto_load_associations
|
47
|
+
model_klass.reflect_on_all_associations
|
48
|
+
else
|
49
|
+
added_associations.map { |association| association_from_model(association) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# validate removed_associations exist
|
53
|
+
removed_associations.each { |association| association_from_model(association) }
|
54
|
+
|
55
|
+
model_associations.reject { |a| removed_associations.include?(a.name.to_sym) }
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def add_attributes(*attrs)
|
61
|
+
validate_model!
|
62
|
+
added_attributes.concat attrs.map(&:to_sym)
|
63
|
+
end
|
64
|
+
alias_method :add_attribute, :add_attributes
|
65
|
+
alias_method :add_attrs, :add_attributes
|
66
|
+
alias_method :add_attr, :add_attributes
|
67
|
+
|
68
|
+
def remove_attributes(*attrs)
|
69
|
+
validate_model!
|
70
|
+
removed_attributes.concat attrs.map(&:to_sym)
|
71
|
+
end
|
72
|
+
alias_method :remove_attribute, :remove_attributes
|
73
|
+
alias_method :remove_attrs, :remove_attributes
|
74
|
+
alias_method :remove_attr, :remove_attributes
|
75
|
+
|
76
|
+
def add_associations(*associations)
|
77
|
+
validate_model!
|
78
|
+
added_associations.concat associations.map(&:to_sym)
|
79
|
+
end
|
80
|
+
alias_method :add_association, :add_associations
|
81
|
+
|
82
|
+
def remove_associations(*associations)
|
83
|
+
validate_model!
|
84
|
+
removed_associations.concat associations.map(&:to_sym)
|
85
|
+
end
|
86
|
+
alias_method :remove_association, :remove_associations
|
87
|
+
|
88
|
+
def model_klass
|
89
|
+
@model_klass ||= model_name.constantize
|
90
|
+
end
|
91
|
+
|
92
|
+
def column_from_model(attr)
|
93
|
+
model_klass.columns_hash[attr.to_s] || raise(Error, Error::MISSING_ATTR_MSG % { model: model_name, attr: attr })
|
94
|
+
end
|
95
|
+
|
96
|
+
def association_from_model(association)
|
97
|
+
model_klass.reflect_on_association(association) || raise(Error, Error::MISSING_ASSOC_MSG % { model: model_name, assoc: association })
|
98
|
+
end
|
99
|
+
|
100
|
+
def added_attributes
|
101
|
+
@added_attributes ||= []
|
102
|
+
end
|
103
|
+
|
104
|
+
def removed_attributes
|
105
|
+
@removed_attributes ||= []
|
106
|
+
end
|
107
|
+
|
108
|
+
def added_associations
|
109
|
+
@added_associations ||= []
|
110
|
+
end
|
111
|
+
|
112
|
+
def removed_associations
|
113
|
+
@removed_associations ||= []
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_model!
|
117
|
+
lazy_load do
|
118
|
+
model_name || raise(Error, Error::MISSING_MODEL_MSG)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module HQ
|
2
|
+
module GraphQL
|
3
|
+
module InputExtensions
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.include Scalars
|
7
|
+
base.include ::HQ::GraphQL::ActiveRecordExtensions
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
def with_model(model_name, attributes: true, associations: false)
|
14
|
+
self.model_name = model_name
|
15
|
+
self.auto_load_attributes = attributes
|
16
|
+
self.auto_load_associations = associations
|
17
|
+
|
18
|
+
lazy_load do
|
19
|
+
model_columns.each do |column|
|
20
|
+
argument_from_column(column)
|
21
|
+
end
|
22
|
+
|
23
|
+
model_associations.each do |association|
|
24
|
+
argument_from_association association
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def argument_from_association(association)
|
32
|
+
input = ::HQ::GraphQL::Inputs[association.klass]
|
33
|
+
name = association.name
|
34
|
+
name_attributes = "#{name}_attributes"
|
35
|
+
case association.macro
|
36
|
+
when :has_many
|
37
|
+
argument name_attributes, [input], required: false
|
38
|
+
else
|
39
|
+
argument name_attributes, input, required: false
|
40
|
+
end
|
41
|
+
|
42
|
+
if !model_klass.nested_attributes_options.keys.include?(name.to_sym)
|
43
|
+
model_klass.accepts_nested_attributes_for name, allow_destroy: true
|
44
|
+
end
|
45
|
+
rescue ::HQ::GraphQL::Inputs::Error
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def argument_from_column(column)
|
50
|
+
argument column.name, ::HQ::GraphQL::Types.type_from_column(column), required: false
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module HQ
|
2
|
+
module GraphQL
|
3
|
+
class InputObject < ::GraphQL::Schema::InputObject
|
4
|
+
include ::HQ::GraphQL::InputExtensions
|
5
|
+
|
6
|
+
def self.to_graphql
|
7
|
+
lazy_load!
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_indifferent_access
|
12
|
+
to_h.with_indifferent_access
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module HQ
|
2
|
+
module GraphQL
|
3
|
+
module Inputs
|
4
|
+
class Error < StandardError
|
5
|
+
MISSING_TYPE_MSG = "The GraphQL type for `%{klass}` is missing.".freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.[](key)
|
9
|
+
@inputs ||= Hash.new do |hash, klass|
|
10
|
+
hash[klass] = klass_for(klass)
|
11
|
+
end
|
12
|
+
@inputs[key]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Only being used in testing
|
16
|
+
def self.reset!
|
17
|
+
@inputs = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
private
|
22
|
+
|
23
|
+
def klass_for(klass_or_string)
|
24
|
+
klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
|
25
|
+
::HQ::GraphQL.types.detect { |t| t.model_klass == klass }&.input_klass ||
|
26
|
+
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name })
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/hq/graphql/object.rb
CHANGED
@@ -1,58 +1,25 @@
|
|
1
1
|
module HQ
|
2
2
|
module GraphQL
|
3
3
|
class Object < ::GraphQL::Schema::Object
|
4
|
-
|
5
|
-
|
6
|
-
MISSING_ATTR_MSG = "Can't find attr %{model}.%{attr}'`".freeze
|
7
|
-
MISSING_ASSOC_MSG = "Can't find association %{model}.%{assoc}'`".freeze
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.lazy_load(&block)
|
11
|
-
@lazy_load ||= []
|
12
|
-
@lazy_load << block if block
|
13
|
-
@lazy_load
|
14
|
-
end
|
4
|
+
include Scalars
|
5
|
+
include ::HQ::GraphQL::ActiveRecordExtensions
|
15
6
|
|
16
7
|
def self.with_model(model_name, attributes: true, associations: true)
|
17
8
|
self.model_name = model_name
|
9
|
+
self.auto_load_attributes = attributes
|
10
|
+
self.auto_load_associations = associations
|
18
11
|
|
19
12
|
lazy_load do
|
20
|
-
|
21
|
-
|
22
|
-
field_from_column column
|
23
|
-
end
|
13
|
+
model_columns.each do |column|
|
14
|
+
field_from_column(column)
|
24
15
|
end
|
25
16
|
|
26
|
-
|
27
|
-
|
28
|
-
field_from_association association
|
29
|
-
end
|
17
|
+
model_associations.each do |association|
|
18
|
+
field_from_association association
|
30
19
|
end
|
31
20
|
end
|
32
21
|
end
|
33
22
|
|
34
|
-
def self.add_attr(attr)
|
35
|
-
lazy_load do
|
36
|
-
validate_model!(:add_attr)
|
37
|
-
field_from_column model_column(attr)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.remove_attrs(*attrs)
|
42
|
-
removed_attrs.concat attrs.map(&:to_sym)
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.remove_associations(*associations)
|
46
|
-
removed_associations.concat associations.map(&:to_sym)
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.add_association(association)
|
50
|
-
lazy_load do
|
51
|
-
validate_model!(:add_association)
|
52
|
-
field_from_association model_association(association)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
23
|
def self.to_graphql
|
57
24
|
lazy_load!
|
58
25
|
super
|
@@ -61,55 +28,22 @@ module HQ
|
|
61
28
|
class << self
|
62
29
|
private
|
63
30
|
|
64
|
-
attr_accessor :model_name
|
65
|
-
attr_writer :removed_attrs, :removed_associations
|
66
|
-
|
67
|
-
def lazy_load!
|
68
|
-
lazy_load.map(&:call)
|
69
|
-
@lazy_load = []
|
70
|
-
end
|
71
|
-
|
72
|
-
def model_klass
|
73
|
-
@model_klass ||= model_name&.constantize
|
74
|
-
end
|
75
|
-
|
76
|
-
def model_column(attr)
|
77
|
-
model_klass.columns_hash[attr.to_s] || raise(Error, Error::MISSING_ATTR_MSG % { model: model_name, attr: attr })
|
78
|
-
end
|
79
|
-
|
80
|
-
def model_association(association)
|
81
|
-
model_klass.reflect_on_association(association) || raise(Error, Error::MISSING_ASSOC_MSG % { model: model_name, assoc: association })
|
82
|
-
end
|
83
|
-
|
84
|
-
def removed_attrs
|
85
|
-
@removed_attrs ||= []
|
86
|
-
end
|
87
|
-
|
88
|
-
def removed_associations
|
89
|
-
@removed_associations ||= []
|
90
|
-
end
|
91
|
-
|
92
|
-
def validate_model!(action)
|
93
|
-
unless model_name
|
94
|
-
raise Error, Error::MISSING_MODEL_MSG % { action: action }
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
31
|
def field_from_association(association)
|
99
|
-
name = association.name
|
100
32
|
type = ::HQ::GraphQL::Types[association.klass]
|
33
|
+
name = association.name
|
101
34
|
case association.macro
|
102
35
|
when :has_many
|
103
36
|
field name, [type], null: false
|
104
37
|
else
|
105
38
|
field name, type, null: true
|
106
39
|
end
|
40
|
+
rescue ::HQ::GraphQL::Types::Error
|
41
|
+
nil
|
107
42
|
end
|
108
43
|
|
109
44
|
def field_from_column(column)
|
110
45
|
field column.name, ::HQ::GraphQL::Types.type_from_column(column), null: column.null
|
111
46
|
end
|
112
|
-
|
113
47
|
end
|
114
48
|
|
115
49
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module HQ
|
2
|
+
module GraphQL
|
3
|
+
module Resource
|
4
|
+
module Mutation
|
5
|
+
|
6
|
+
def self.build(model_name, graphql_name:, require_primary_key: false, &block)
|
7
|
+
Class.new(::HQ::GraphQL::Mutation) do
|
8
|
+
graphql_name graphql_name
|
9
|
+
|
10
|
+
lazy_load do
|
11
|
+
field :errors, [String], null: false
|
12
|
+
field :resource, ::HQ::GraphQL::Types[model_name], null: true
|
13
|
+
end
|
14
|
+
|
15
|
+
instance_eval(&block)
|
16
|
+
|
17
|
+
if require_primary_key
|
18
|
+
lazy_load do
|
19
|
+
klass = model_name.constantize
|
20
|
+
primary_key = klass.primary_key
|
21
|
+
pk_column = klass.columns.detect { |c| c.name == primary_key.to_s }
|
22
|
+
|
23
|
+
argument primary_key, ::HQ::GraphQL::Types.type_from_column(pk_column), required: true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|