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 +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
|