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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: be9de36cb5b0f12696e98d85156404d89acdc6d5
4
- data.tar.gz: 0e887aabc0d14374ad192e464a8b1e3e4ac4ea8f
3
+ metadata.gz: b25a73fce273a48a00a185766cae51a9ec2e3150
4
+ data.tar.gz: 7c325eaef387c340778954fb83dbb3d0ccac9c03
5
5
  SHA512:
6
- metadata.gz: 7ec2755f98cd6045e93da317edec985a3a148c4baef5a7e30d24c1568020327f455d4a7efb14b117d482c0b51106e69ad67f0db33c6051c8b42d750f951086c8
7
- data.tar.gz: aae1f46e971c88db6a4e87d919dc354e0235931041bf485172df0109520892f1e0294a10b99c81943ca71132b92ed1bfd56f76fdda5d2cb0e3b9e9323bc56665
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
- You can pass configuration options as a block to `::HQ::GraphQL.configure`.
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.model_to_graphql_type = -> (model_class) { "::CustomNameSpace::#{model_class.name}Type" }
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
- ## Usage
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
@@ -0,0 +1,13 @@
1
+ module HQ
2
+ module GraphQL
3
+ class Mutation < ::GraphQL::Schema::Mutation
4
+ include ::HQ::GraphQL::InputExtensions
5
+
6
+ def self.generate_payload_type
7
+ lazy_load!
8
+ super
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -1,58 +1,25 @@
1
1
  module HQ
2
2
  module GraphQL
3
3
  class Object < ::GraphQL::Schema::Object
4
- class Error < StandardError
5
- MISSING_MODEL_MSG = "Can't perform %{action} without connecting 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.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
- if attributes
21
- model_klass.columns.reject { |c| removed_attrs.include?(c.name.to_sym) }.each do |column|
22
- field_from_column column
23
- end
13
+ model_columns.each do |column|
14
+ field_from_column(column)
24
15
  end
25
16
 
26
- if associations
27
- model_klass.reflect_on_all_associations.reject { |a| removed_associations.include?(a.name.to_sym) }.each do |association|
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