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