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.
@@ -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