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.
@@ -0,0 +1,199 @@
1
+ require "hq/graphql/resource/mutation"
2
+
3
+ module HQ
4
+ module GraphQL
5
+ module Resource
6
+
7
+ def self.included(base)
8
+ ::HQ::GraphQL.types << base
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ attr_accessor :model_name
14
+
15
+ def find_record(attrs, context)
16
+ primary_key = model_klass.primary_key.to_sym
17
+ primary_key_value = attrs[primary_key]
18
+ scope(context).find_by(primary_key => primary_key_value)
19
+ end
20
+
21
+ def model_klass
22
+ @model_klass ||= model_name&.safe_constantize
23
+ end
24
+
25
+ def mutation_klasses
26
+ @mutation_klasses ||= {}.with_indifferent_access
27
+ end
28
+
29
+ def input_klass
30
+ @input_klass ||= begin
31
+ scoped_model_name = model_name
32
+ scoped_inputs_proc = inputs_proc
33
+ Class.new(::HQ::GraphQL::InputObject) do
34
+ graphql_name "#{scoped_model_name.demodulize}Input"
35
+ instance_eval(&scoped_inputs_proc)
36
+ end
37
+ end
38
+ end
39
+
40
+ def query_klass
41
+ @query_klass ||= build_graphql_object
42
+ end
43
+
44
+ protected
45
+
46
+ def inputs_proc
47
+ @inputs_proc ||= build_input_proc
48
+ end
49
+
50
+ def default_scope(&block)
51
+ @default_scope = block
52
+ end
53
+
54
+ def input(**options, &block)
55
+ @inputs_proc = build_input_proc(**options, &block)
56
+ end
57
+
58
+ def mutations(create: true, update: true, destroy: true)
59
+ model_display_name = model_name.demodulize
60
+ scoped_self = self
61
+ delayed_execute_inputs_proc = proc { scoped_self.inputs_proc }
62
+
63
+ if create
64
+ create_mutation = ::HQ::GraphQL::Resource::Mutation.build(model_name, graphql_name: "#{model_display_name}Create") do
65
+ define_method(:resolve) do |**attrs|
66
+ resource = scoped_self.model_klass.new
67
+ resource.assign_attributes(attrs)
68
+ if resource.save
69
+ {
70
+ resource: resource,
71
+ errors: [],
72
+ }
73
+ else
74
+ {
75
+ resource: nil,
76
+ errors: resource.errors.full_messages
77
+ }
78
+ end
79
+ end
80
+
81
+ lazy_load do
82
+ instance_eval(&delayed_execute_inputs_proc.call)
83
+ end
84
+ end
85
+
86
+ mutation_klasses["create_#{model_display_name.underscore}"] = create_mutation
87
+ end
88
+
89
+ if update
90
+ update_mutation = ::HQ::GraphQL::Resource::Mutation.build(
91
+ model_name,
92
+ graphql_name: "#{model_display_name}Update",
93
+ require_primary_key: true
94
+ ) do
95
+ define_method(:resolve) do |**attrs|
96
+ resource = scoped_self.find_record(attrs, context)
97
+
98
+ if resource
99
+ resource.assign_attributes(attrs)
100
+ if resource.save
101
+ {
102
+ resource: resource,
103
+ errors: [],
104
+ }
105
+ else
106
+ {
107
+ resource: resource,
108
+ errors: resource.errors.full_messages
109
+ }
110
+ end
111
+ else
112
+ {
113
+ resource: nil,
114
+ errors: ["Unable to find #{model_display_name}"],
115
+ }
116
+ end
117
+ end
118
+
119
+ lazy_load do
120
+ instance_eval(&delayed_execute_inputs_proc.call)
121
+ end
122
+ end
123
+
124
+ mutation_klasses["update_#{model_display_name.underscore}"] = update_mutation
125
+ end
126
+
127
+ if destroy
128
+ destroy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
129
+ model_name,
130
+ graphql_name: "#{model_display_name}Destroy",
131
+ require_primary_key: true
132
+ ) do
133
+ define_method(:resolve) do |**attrs|
134
+ resource = scoped_self.find_record(attrs, context)
135
+
136
+ if resource
137
+ if resource.destroy
138
+ {
139
+ resource: resource,
140
+ errors: [],
141
+ }
142
+ else
143
+ {
144
+ resource: resource,
145
+ errors: resource.errors.full_messages
146
+ }
147
+ end
148
+ else
149
+ {
150
+ resource: nil,
151
+ errors: ["Unable to find #{model_display_name}"],
152
+ }
153
+ end
154
+ end
155
+ end
156
+
157
+ mutation_klasses["destroy_#{model_display_name.underscore}"] = destroy_mutation
158
+ end
159
+ end
160
+
161
+ def query(**options, &block)
162
+ @query_klass = build_graphql_object(**options, &block)
163
+ end
164
+
165
+ def root_query
166
+ ::HQ::GraphQL.root_queries << self
167
+ end
168
+
169
+ def scope(context)
170
+ scope = model_klass
171
+ scope = ::HQ::GraphQL.default_scope(scope, context)
172
+ @default_scope&.call(scope, context) || scope
173
+ end
174
+
175
+ private
176
+
177
+ def build_graphql_object(**options, &block)
178
+ scoped_model_name = model_name
179
+ Class.new(::HQ::GraphQL::Object) do
180
+ graphql_name scoped_model_name
181
+
182
+ with_model scoped_model_name, **options
183
+
184
+ instance_eval(&block) if block
185
+ end
186
+ end
187
+
188
+ def build_input_proc(**options, &block)
189
+ scoped_model_name = model_name
190
+ proc do
191
+ with_model scoped_model_name, **options
192
+ instance_eval(&block) if block
193
+ end
194
+ end
195
+
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,20 @@
1
+ module HQ
2
+ module GraphQL
3
+ class RootMutation < ::HQ::GraphQL::Object
4
+
5
+ def self.inherited(base)
6
+ super
7
+ base.class_eval do
8
+ lazy_load do
9
+ ::HQ::GraphQL.types.each do |type|
10
+ type.mutation_klasses.each do |mutation_name, klass|
11
+ field mutation_name, mutation: klass
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module HQ
2
+ module GraphQL
3
+ class RootQuery < ::HQ::GraphQL::Object
4
+
5
+ def self.inherited(base)
6
+ super
7
+ base.class_eval do
8
+ lazy_load do
9
+ ::HQ::GraphQL.root_queries.each do |graphql|
10
+ klass = graphql.model_klass
11
+ field_name = klass.name.demodulize.underscore
12
+ primary_key = klass.primary_key
13
+ pk_column = klass.columns.detect { |c| c.name == primary_key.to_s }
14
+
15
+ field field_name, graphql.query_klass, null: true do
16
+ argument primary_key, ::HQ::GraphQL::Types.type_from_column(pk_column), required: true
17
+ end
18
+
19
+ define_method(field_name) do |**attrs|
20
+ graphql.find_record(attrs, context)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ require "hq/graphql/types/uuid"
2
+
3
+ module HQ
4
+ module GraphQL
5
+ module Scalars
6
+ UUID = ::HQ::GraphQL::Types::UUID
7
+ end
8
+ end
9
+ end
@@ -5,11 +5,11 @@ module HQ
5
5
  description "UUID"
6
6
 
7
7
  class << self
8
- def self.coerce_input(value, context)
8
+ def coerce_input(value, context)
9
9
  validate_and_return_uuid(value)
10
10
  end
11
11
 
12
- def self.coerce_result(value, context)
12
+ def coerce_result(value, context)
13
13
  validate_and_return_uuid(value)
14
14
  end
15
15
 
@@ -24,7 +24,7 @@ module HQ
24
24
  end
25
25
 
26
26
  def validate_uuid(value)
27
- !!value.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
27
+ !!value.to_s.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
28
28
  end
29
29
  end
30
30
  end
@@ -1,14 +1,15 @@
1
- require "hq/graphql/types/uuid"
2
-
3
1
  module HQ
4
2
  module GraphQL
5
3
  module Types
4
+ class Error < StandardError
5
+ MISSING_TYPE_MSG = "The GraphQL type for `%{klass}` is missing.".freeze
6
+ end
6
7
 
7
8
  def self.[](key)
8
- @schema_objects ||= Hash.new do |hash, klass|
9
+ @types ||= Hash.new do |hash, klass|
9
10
  hash[klass] = klass_for(klass)
10
11
  end
11
- @schema_objects[key]
12
+ @types[key]
12
13
  end
13
14
 
14
15
  def self.type_from_column(column)
@@ -16,37 +17,28 @@ module HQ
16
17
  when :uuid
17
18
  ::HQ::GraphQL::Types::UUID
18
19
  when :integer
19
- ::GraphQL::INT_TYPE
20
+ ::GraphQL::Types::Int
20
21
  when :decimal
21
- ::GraphQL::FLOAT_TYPE
22
+ ::GraphQL::Types::Float
22
23
  when :boolean
23
- ::GraphQL::BOOLEAN_TYPE
24
+ ::GraphQL::Types::Boolean
24
25
  else
25
- ::GraphQL::STRING_TYPE
26
+ ::GraphQL::Types::String
26
27
  end
27
28
  end
28
29
 
29
30
  # Only being used in testing
30
31
  def self.reset!
31
- @schema_objects = nil
32
+ @types = nil
32
33
  end
33
34
 
34
35
  class << self
35
36
  private
36
37
 
37
- def klass_for(klass)
38
- hql_klass_name = ::HQ::GraphQL.graphql_type_from_model(klass)
39
- hql_klass = hql_klass_name.safe_constantize
40
- return hql_klass if hql_klass
41
-
42
- module_name = hql_klass_name.deconstantize.presence
43
- hql_module = module_name ? (module_name.safe_constantize || ::Object.const_set(module_name, Module.new)) : ::Object
44
-
45
- hql_klass = Class.new(::HQ::GraphQL::Object) do
46
- with_model klass.name
47
- end
48
-
49
- hql_module.const_set(hql_klass_name.demodulize, hql_klass)
38
+ def klass_for(klass_or_string)
39
+ klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
40
+ ::HQ::GraphQL.types.detect { |t| t.model_klass == klass }&.query_klass ||
41
+ raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name })
50
42
  end
51
43
  end
52
44
 
@@ -1,5 +1,5 @@
1
1
  module HQ
2
2
  module GraphQL
3
- VERSION = "0.0.2"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
data/lib/hq/graphql.rb CHANGED
@@ -2,6 +2,7 @@ require "graphql"
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
+
5
6
  def self.config
6
7
  @config ||= ::ActiveSupport::OrderedOptions.new
7
8
  end
@@ -10,23 +11,38 @@ module HQ
10
11
  config.instance_eval(&block)
11
12
  end
12
13
 
13
- # The gem assumes that if your model is called `MyModel`, the corresponding type is `MyModelType`.
14
- # You can override that convention.
15
- #
16
- # ::HQ::GraphQL.config do |config|
17
- # config.model_to_graphql_type = -> (model_class) { "::CustomNameSpace::#{model_class.name}Type" }
18
- # end
19
- def self.model_to_graphql_type
20
- config.model_to_graphql_type ||
21
- @model_to_graphql_type ||= -> (model_class) { "#{model_class.name.demodulize}Type" }
14
+ def self.default_scope(scope, context)
15
+ config.default_scope&.call(scope, context) || scope
16
+ end
17
+
18
+ def self.reset!
19
+ @root_queries = nil
20
+ @types = nil
21
+ ::HQ::GraphQL::Inputs.reset!
22
+ ::HQ::GraphQL::Types.reset!
22
23
  end
23
24
 
24
- def self.graphql_type_from_model(model_class)
25
- model_to_graphql_type.call(model_class)
25
+ def self.root_queries
26
+ @root_queries ||= Set.new
26
27
  end
28
+
29
+ def self.types
30
+ @types ||= Set.new
31
+ end
32
+
27
33
  end
28
34
  end
29
35
 
36
+ require "hq/graphql/active_record_extensions"
37
+ require "hq/graphql/scalars"
38
+ require "hq/graphql/input_extensions"
39
+
40
+ require "hq/graphql/inputs"
41
+ require "hq/graphql/input_object"
42
+ require "hq/graphql/mutation"
30
43
  require "hq/graphql/object"
44
+ require "hq/graphql/resource"
45
+ require "hq/graphql/root_mutation"
46
+ require "hq/graphql/root_query"
31
47
  require "hq/graphql/types"
32
48
  require "hq/graphql/engine"
@@ -6,13 +6,13 @@ ActiveRecord::Schema.define do
6
6
  t.timestamps null: false
7
7
  end
8
8
 
9
- create_table "users", force: true do |t|
9
+ create_table "users", force: true, id: :uuid do |t|
10
10
  t.belongs_to :organization, null: false, index: true, foreign_key: true, type: :uuid
11
11
  t.string :name, null: false
12
12
  t.timestamps null: false
13
13
  end
14
14
 
15
- create_table "advisors", force: true do |t|
15
+ create_table "advisors", force: true, id: :uuid do |t|
16
16
  t.references :organization, null: false, index: true, foreign_key: true, type: :uuid
17
17
  t.string :name, null: false
18
18
  t.timestamps null: false
@@ -0,0 +1,63 @@
1
+ require 'rails_helper'
2
+
3
+ describe ::HQ::GraphQL::ActiveRecordExtensions do
4
+ let(:extended_klass) do
5
+ Class.new do
6
+ include ::HQ::GraphQL::ActiveRecordExtensions
7
+
8
+ @counter = 0
9
+
10
+ lazy_load do
11
+ @counter += 1
12
+ end
13
+
14
+ def self.counter
15
+ @counter
16
+ end
17
+ end
18
+ end
19
+
20
+ describe ".add_attributes" do
21
+ it "aliases add_attributes" do
22
+ add_attributes = extended_klass.method(:add_attributes)
23
+ aggregate_failures do
24
+ expect(add_attributes).to eql(extended_klass.method(:add_attribute))
25
+ expect(add_attributes).to eql(extended_klass.method(:add_attrs))
26
+ expect(add_attributes).to eql(extended_klass.method(:add_attr))
27
+ end
28
+ end
29
+ end
30
+
31
+ describe ".remove_attributes" do
32
+ it "aliases remove_attributes" do
33
+ remove_attributes = extended_klass.method(:remove_attributes)
34
+ aggregate_failures do
35
+ expect(remove_attributes).to eql(extended_klass.method(:remove_attribute))
36
+ expect(remove_attributes).to eql(extended_klass.method(:remove_attrs))
37
+ expect(remove_attributes).to eql(extended_klass.method(:remove_attr))
38
+ end
39
+ end
40
+ end
41
+
42
+ describe ".add_associations" do
43
+ it "aliases add_associations" do
44
+ expect(extended_klass.method(:add_associations)).to eql(extended_klass.method(:add_association))
45
+ end
46
+ end
47
+
48
+ describe ".remove_associations" do
49
+ it "aliases remove_associations" do
50
+ expect(extended_klass.method(:remove_associations)).to eql(extended_klass.method(:remove_association))
51
+ end
52
+ end
53
+
54
+ describe ".lazy_load" do
55
+ it "lazy loads once" do
56
+ # First time it works
57
+ expect { extended_klass.lazy_load! }.to change { extended_klass.counter }.by(1)
58
+ # Second time it does nothing
59
+ expect { extended_klass.lazy_load! }.to change { extended_klass.counter }.by(0)
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,40 @@
1
+ require 'rails_helper'
2
+
3
+ describe ::HQ::GraphQL::Inputs do
4
+
5
+ describe ".[]" do
6
+ let(:graphql_klass) do
7
+ Class.new do
8
+ include ::HQ::GraphQL::Resource
9
+
10
+ self.model_name = "Advisor"
11
+
12
+ input(attributes: false, associations: false) do
13
+ argument :customField, String, "Header for the post", required: true
14
+ end
15
+ end
16
+ end
17
+
18
+ it "finds the type" do
19
+ input_object = graphql_klass.input_klass
20
+
21
+ aggregate_failures do
22
+ expect(input_object.superclass).to eql(::HQ::GraphQL::InputObject)
23
+ expect(input_object).to eql(described_class[Advisor])
24
+ expect(input_object.arguments.keys).to contain_exactly("customField")
25
+ input_object.to_graphql
26
+ expect(input_object.arguments.keys).to contain_exactly("customField")
27
+ end
28
+ end
29
+
30
+ it "finds the type when lookup is a string" do
31
+ input_object = graphql_klass.input_klass
32
+ expect(input_object).to eql(described_class["Advisor"])
33
+ end
34
+
35
+ it "raises an exception for unknown types" do
36
+ expect { described_class[Advisor] }.to raise_error(described_class::Error)
37
+ end
38
+ end
39
+
40
+ end