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
@@ -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
|
@@ -5,11 +5,11 @@ module HQ
|
|
5
5
|
description "UUID"
|
6
6
|
|
7
7
|
class << self
|
8
|
-
def
|
8
|
+
def coerce_input(value, context)
|
9
9
|
validate_and_return_uuid(value)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
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
|
data/lib/hq/graphql/types.rb
CHANGED
@@ -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
|
-
@
|
9
|
+
@types ||= Hash.new do |hash, klass|
|
9
10
|
hash[klass] = klass_for(klass)
|
10
11
|
end
|
11
|
-
@
|
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::
|
20
|
+
::GraphQL::Types::Int
|
20
21
|
when :decimal
|
21
|
-
::GraphQL::
|
22
|
+
::GraphQL::Types::Float
|
22
23
|
when :boolean
|
23
|
-
::GraphQL::
|
24
|
+
::GraphQL::Types::Boolean
|
24
25
|
else
|
25
|
-
::GraphQL::
|
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
|
-
@
|
32
|
+
@types = nil
|
32
33
|
end
|
33
34
|
|
34
35
|
class << self
|
35
36
|
private
|
36
37
|
|
37
|
-
def klass_for(
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
|
data/lib/hq/graphql/version.rb
CHANGED
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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.
|
25
|
-
|
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"
|
data/spec/internal/db/schema.rb
CHANGED
@@ -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
|