graphql-api 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +256 -0
- data/Rakefile +34 -0
- data/lib/graphql/api.rb +16 -0
- data/lib/graphql/api/command_type.rb +24 -0
- data/lib/graphql/api/helpers.rb +88 -0
- data/lib/graphql/api/query_type.rb +24 -0
- data/lib/graphql/api/schema.rb +284 -0
- data/lib/graphql/api/schema_error.rb +4 -0
- data/lib/graphql/api/version.rb +5 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 213d171fcd46bcfca65679b3af693a3c77e9e3eb
|
4
|
+
data.tar.gz: ebfd301985d54acc70f1150b85a86c2f2f3be9f8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5aa16322431206472f7d22de81f61d516d44b93357aa3b6d45e3c6776dd2983e27b7440015c853466188c1ce01ab04c050124b6a0034903fa27a60d5d07cfb1a
|
7
|
+
data.tar.gz: d9cfc59ecdd63c43d2f9c4a8f74912d340d1decd7fa339f3cfbf2e81ad206890a110bba893cc0a520b7e6908e42bce08f23e90228b84618cddb72b4e0e30a0d1
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2016 Colin Walker
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
# GraphQL-Api
|
2
|
+
GraphQL-Api is an opinionated Graphql framework for Rails that supports
|
3
|
+
auto generating queries based on Active Record models and plain Ruby
|
4
|
+
objects.
|
5
|
+
|
6
|
+
## Example
|
7
|
+
|
8
|
+
Given the following model structure:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class Author < ActiveRecord::Base
|
12
|
+
has_many :blogs
|
13
|
+
# columns: name
|
14
|
+
end
|
15
|
+
|
16
|
+
class Blog < ActiveRecord::Base
|
17
|
+
belongs_to :author
|
18
|
+
# columns: title, content
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
GraphQL-Api will respond to the following queries for the blog resource:
|
23
|
+
|
24
|
+
```graphql
|
25
|
+
query { blog(id: 1) { id, title, author { name } } }
|
26
|
+
|
27
|
+
query { blogs(limit: 5) { id, title, author { name } } }
|
28
|
+
|
29
|
+
mutation { createBlog(input: {name: "test", author_id: 2}) { blog { id } } }
|
30
|
+
|
31
|
+
mutation { updateBlog(input: {id: 1, title: "test"}) { blog { id } } }
|
32
|
+
|
33
|
+
mutation { deleteBlog(input: {id: 1}) { blog_id } }
|
34
|
+
```
|
35
|
+
|
36
|
+
GraphQL-Api also has support for command objects:
|
37
|
+
```ruby
|
38
|
+
# Graphql mutation derived from the below command object:
|
39
|
+
# mutation { blogCreateCommand(input: {tags: ["test", "testing"], name: "hello"}) { blog { id, tags { name } } } }
|
40
|
+
|
41
|
+
class BlogCreateCommand < GraphQL::Api::CommandType
|
42
|
+
inputs name: :string, tags: [:string]
|
43
|
+
returns blog: Blog
|
44
|
+
|
45
|
+
def perform
|
46
|
+
# do something here to add some tags to a blog, you could also use ctx[:current_user] to access the user
|
47
|
+
{blog: blog}
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
... and query objects:
|
54
|
+
```ruby
|
55
|
+
# Graphql query derived from the below query object:
|
56
|
+
# query { blogQuery(content_matches: ["name"]) { id, name } }
|
57
|
+
|
58
|
+
class BlogQuery < GraphQL::Api::QueryType
|
59
|
+
arguments name: :string, content_matches: [:string]
|
60
|
+
return_type [Blog]
|
61
|
+
|
62
|
+
def execute
|
63
|
+
Blog.all
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## Contents
|
70
|
+
|
71
|
+
1. [Guides](#guides)
|
72
|
+
2. [Documentation](#documentation)
|
73
|
+
3. [Roadmap](#roadmap)
|
74
|
+
|
75
|
+
## Guides
|
76
|
+
|
77
|
+
### Endpoint
|
78
|
+
|
79
|
+
Creating an endpoint for GraphQL-Api.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# inside an initializer or other file inside the load path
|
83
|
+
GraphSchema = GraphQL::Api::Schema.new.schema
|
84
|
+
|
85
|
+
# controllers/graphql_controller.rb
|
86
|
+
class GraphqlController < ApplicationController
|
87
|
+
|
88
|
+
# needed by the relay framework, defines the graphql schema
|
89
|
+
def index
|
90
|
+
render json: GraphSchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY)
|
91
|
+
end
|
92
|
+
|
93
|
+
# will respond to graphql requests and pass through the current user
|
94
|
+
def create
|
95
|
+
render json: GraphSchema.execute(
|
96
|
+
params[:query],
|
97
|
+
variables: params[:variables] || {},
|
98
|
+
context: {current_user: current_user}
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Authorization
|
106
|
+
|
107
|
+
GraphQL-Api will check for an `access_<field>?(ctx)` method on all model
|
108
|
+
objects before returning the value. If this method returns false, the
|
109
|
+
value will be `nil`.
|
110
|
+
|
111
|
+
To scope queries for the model, define the `graph_find(args, ctx)` and
|
112
|
+
`graph_where(args, ctx)` methods using the `ctx` parameter to get the
|
113
|
+
current user and apply a scoped query. For example:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class Blog < ActiveRecord::Base
|
117
|
+
belongs_to :author
|
118
|
+
|
119
|
+
def self.graph_find(args, ctx)
|
120
|
+
ctx[:current_user].blogs.find(args[:id])
|
121
|
+
end
|
122
|
+
|
123
|
+
def access_content?(ctx)
|
124
|
+
ctx[:current_user].is_admin?
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
For more complicated access management, define query objects and Poro's
|
131
|
+
with only a subset of fields that can be accessed.
|
132
|
+
|
133
|
+
Future work is this area is ongoing. For example, a CanCan integration
|
134
|
+
could be a much simpler way of separating out this logic.
|
135
|
+
|
136
|
+
## Documentation
|
137
|
+
|
138
|
+
### Querying
|
139
|
+
|
140
|
+
Instantiate an instance of GraphQL-Api and get the `schema` object which is
|
141
|
+
a `GraphQL::Schema` instance from [graphql-ruby](https://rmosolgo.github.io/graphql-ruby).
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
graph = GraphQL::Api::Schema.new(commands: [], models: [], queries: [])
|
145
|
+
graph.schema.execute('query { ... }')
|
146
|
+
```
|
147
|
+
|
148
|
+
GraphQL-Api will load in all models, query objects and commands from the rails
|
149
|
+
app directory automatically. If you store these in a different location
|
150
|
+
you can pass them in directly to the new command.
|
151
|
+
|
152
|
+
### Model Objects
|
153
|
+
|
154
|
+
Model objects are the core return value from GraphQL-Api. They can be a plain
|
155
|
+
old ruby object or they can be an active record model. Active record models
|
156
|
+
have more automatic inference, whereas poro objects are more flexible.
|
157
|
+
|
158
|
+
#### Active Record
|
159
|
+
|
160
|
+
GraphQL-Api reads your associations and columns from your models and creates
|
161
|
+
a graphql schema from them. In the examples above you can see that 'Author'
|
162
|
+
is automatically accessible from the 'Blog' object because the belongs to
|
163
|
+
relationship is set up. Column types are also inferred.
|
164
|
+
|
165
|
+
GraphQL-Api will set up two queries on the main Graphql query object. One for
|
166
|
+
a single record and another for a collection. You can override these queries
|
167
|
+
by setting a `graph_find(args, ctx)` and `graph_where(args, ctx)` class
|
168
|
+
methods on your model. The `ctx` parameter will contain the context passed
|
169
|
+
in from the controller while the `args` parameter will contain the arguments
|
170
|
+
passed into the graphql query.
|
171
|
+
|
172
|
+
#### Poro
|
173
|
+
|
174
|
+
Plain old ruby objects are supported by implementing a class method called
|
175
|
+
`fields` on the object that returns the expected [types](#types) hash.
|
176
|
+
Methods on the Poro should be defined with the same name as the provided
|
177
|
+
fields.
|
178
|
+
|
179
|
+
### Command Objects
|
180
|
+
|
181
|
+
Command objects are an object oriented approach to defining mutations.
|
182
|
+
They take a set of inputs as well as a graphql context and provide a
|
183
|
+
`perform` method that returns a Graphql understandable type. These objects
|
184
|
+
give you an object oriented abstraction for handling mutations.
|
185
|
+
|
186
|
+
Command objects must implement the interface defined in `GraphQL::Api::CommandType`
|
187
|
+
|
188
|
+
### Query Objects
|
189
|
+
|
190
|
+
Query objects are designed to provide a wrapper around complex queries
|
191
|
+
with potentially a lot of inputs. They return a single type or array of
|
192
|
+
types.
|
193
|
+
|
194
|
+
Query objects must implement the interface defined in `GraphQL::Api::QueryType`
|
195
|
+
|
196
|
+
### Customization
|
197
|
+
|
198
|
+
Sometimes you cannot fit every possible use case into a library like GraphQL-Api
|
199
|
+
as a result, you can always drop down to the excellent Graphql library for
|
200
|
+
ruby to combine both hand rolled and GraphQL-Api graphql schemas. Here is an
|
201
|
+
example creating a custom mutation.
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
simple_mutation = GraphQL::Relay::Mutation.define do
|
205
|
+
input_field :name, !types.String
|
206
|
+
return_field :item, types.String
|
207
|
+
resolve -> (inputs, ctx) { {item: 'hello'} }
|
208
|
+
end
|
209
|
+
|
210
|
+
graph = GraphQL::Api::Schema.new
|
211
|
+
mutation = graph.mutation do
|
212
|
+
field 'simpleMutation', simple_mutation.field
|
213
|
+
end
|
214
|
+
|
215
|
+
schema = GraphQL::Schema.define(query: graph.query, mutation: mutation)
|
216
|
+
puts schema.execute('mutation { simpleMutation(input: {name: "hello"}) { item } }')
|
217
|
+
```
|
218
|
+
|
219
|
+
The `GraphQL::Api::Schema#mutation` and `GraphQL::Api::Schema#query` methods accept
|
220
|
+
a block that allows you to add custom fields or methods to the mutation or
|
221
|
+
query definitions. You can refer to the [graphql-ruby](https://rmosolgo.github.io/graphql-ruby)
|
222
|
+
docs for how to do this.
|
223
|
+
|
224
|
+
### Types
|
225
|
+
|
226
|
+
Field types and argument types are all supplied as a hash of key value
|
227
|
+
pairs. An exclamation mark at the end of the type marks it as required,
|
228
|
+
and wrapping the type in an array marks it as a list of that type.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
{
|
232
|
+
name: :string,
|
233
|
+
more_names: [:string],
|
234
|
+
required: :integer!,
|
235
|
+
}
|
236
|
+
```
|
237
|
+
|
238
|
+
The supported types are:
|
239
|
+
|
240
|
+
- integer
|
241
|
+
- text
|
242
|
+
- string
|
243
|
+
- decimal
|
244
|
+
- float
|
245
|
+
- boolean
|
246
|
+
|
247
|
+
Note, these are the same as active record's column types for consistency.
|
248
|
+
|
249
|
+
|
250
|
+
## Roadmap
|
251
|
+
|
252
|
+
- [ ] Customizing resolvers
|
253
|
+
- [ ] CanCan support
|
254
|
+
- [ ] Relay support
|
255
|
+
- [ ] Additional object support (enums, interfaces ...)
|
256
|
+
- [ ] Support non rails frameworks
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Graphite'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
data/lib/graphql/api.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "graphql/api/version"
|
2
|
+
require "graphql/api/schema"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Api
|
6
|
+
|
7
|
+
def self.schema(opts={})
|
8
|
+
GraphQL::Api::Schema.new(opts).schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.graph(opts={})
|
12
|
+
GraphQL::Api::Schema.new(opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module GraphQL::Api
|
2
|
+
class CommandType
|
3
|
+
attr_accessor :ctx, :inputs
|
4
|
+
|
5
|
+
def initialize(inputs, ctx)
|
6
|
+
@inputs = inputs
|
7
|
+
@ctx = ctx
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.inputs(inputs=nil)
|
11
|
+
@inputs = inputs if inputs
|
12
|
+
@inputs || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.returns(fields=nil)
|
16
|
+
@returns = fields if fields
|
17
|
+
@returns || {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def perform
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'graphql/api/schema_error'
|
2
|
+
|
3
|
+
module GraphQL::Api
|
4
|
+
module Helpers
|
5
|
+
def all_constants(root)
|
6
|
+
begin
|
7
|
+
Dir["#{Rails.root}/app/#{root}/*"].map do |f|
|
8
|
+
file = f.split('/')[-1]
|
9
|
+
if file.end_with?('.rb')
|
10
|
+
const = file.split('.')[0].camelize.constantize
|
11
|
+
const unless const.try(:abstract_class)
|
12
|
+
end
|
13
|
+
end.compact
|
14
|
+
rescue
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def graphql_type_for_object(return_type, object_types)
|
20
|
+
if return_type.nil?
|
21
|
+
raise SchemaError.new("return type is nil for object")
|
22
|
+
end
|
23
|
+
|
24
|
+
if return_type.respond_to?(:to_sym) || (return_type.is_a?(Array) && return_type[0].respond_to?(:to_sym))
|
25
|
+
type = graphql_type_of(return_type.to_sym)
|
26
|
+
elsif return_type.is_a?(Array)
|
27
|
+
type = object_types[return_type[0]].to_list_type
|
28
|
+
else
|
29
|
+
type = object_types[return_type]
|
30
|
+
end
|
31
|
+
|
32
|
+
if type.nil?
|
33
|
+
raise SchemaError.new("could not parse return type for: #{return_type}")
|
34
|
+
end
|
35
|
+
|
36
|
+
type
|
37
|
+
end
|
38
|
+
|
39
|
+
def graphql_type_of(type)
|
40
|
+
|
41
|
+
is_required = false
|
42
|
+
if type.to_s.end_with?('!')
|
43
|
+
is_required = true
|
44
|
+
type = type.to_s.chomp('!').to_sym
|
45
|
+
end
|
46
|
+
|
47
|
+
is_list = false
|
48
|
+
if type.is_a?(Array)
|
49
|
+
is_list = true
|
50
|
+
type = type[0]
|
51
|
+
end
|
52
|
+
|
53
|
+
case type
|
54
|
+
when :integer
|
55
|
+
res = GraphQL::INT_TYPE
|
56
|
+
when :text
|
57
|
+
res = GraphQL::STRING_TYPE
|
58
|
+
when :string
|
59
|
+
res = GraphQL::STRING_TYPE
|
60
|
+
when :decimal
|
61
|
+
res = GraphQL::FLOAT_TYPE
|
62
|
+
when :float
|
63
|
+
res = GraphQL::FLOAT_TYPE
|
64
|
+
when :boolean
|
65
|
+
res = GraphQL::BOOLEAN_TYPE
|
66
|
+
else
|
67
|
+
res = GraphQL::STRING_TYPE
|
68
|
+
end
|
69
|
+
|
70
|
+
res = res.to_list_type if is_list
|
71
|
+
res = !res if is_required
|
72
|
+
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
def graphql_type(column)
|
77
|
+
graphql_type_of(column.type)
|
78
|
+
end
|
79
|
+
|
80
|
+
def graphql_fetch(obj, ctx, name)
|
81
|
+
if obj.respond_to?("access_#{name}?")
|
82
|
+
obj.send(name) if obj.send("access_#{name}?", ctx)
|
83
|
+
else
|
84
|
+
obj.send(name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module GraphQL::Api
|
2
|
+
class QueryType
|
3
|
+
attr_accessor :ctx, :inputs
|
4
|
+
|
5
|
+
def initialize(inputs, ctx)
|
6
|
+
@inputs = inputs
|
7
|
+
@ctx = ctx
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.arguments(arguments=nil)
|
11
|
+
@arguments = arguments if arguments
|
12
|
+
@arguments || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.return_type(type=nil)
|
16
|
+
@return_type = type if type
|
17
|
+
@return_type
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'graphql/api/command_type'
|
2
|
+
require 'graphql/api/query_type'
|
3
|
+
require 'graphql/api/helpers'
|
4
|
+
require 'graphql/api/schema_error'
|
5
|
+
require 'graphql'
|
6
|
+
|
7
|
+
include GraphQL::Api::Helpers
|
8
|
+
|
9
|
+
module GraphQL::Api
|
10
|
+
class Schema
|
11
|
+
|
12
|
+
def initialize(commands: [], queries: [], models: [])
|
13
|
+
@types = {}
|
14
|
+
@mutations = {}
|
15
|
+
|
16
|
+
@load_commands = commands
|
17
|
+
@load_queries = queries
|
18
|
+
@load_models = models
|
19
|
+
|
20
|
+
build_model_types
|
21
|
+
build_mutations
|
22
|
+
build_object_types
|
23
|
+
end
|
24
|
+
|
25
|
+
def all_models
|
26
|
+
@all_models ||= all_constants('models') + @load_models
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_queries
|
30
|
+
@all_queries ||= all_constants('queries') + @load_queries
|
31
|
+
end
|
32
|
+
|
33
|
+
def all_commands
|
34
|
+
@all_commands ||= all_constants('commands') + @load_commands
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_type(model_class)
|
38
|
+
object_types = @types
|
39
|
+
|
40
|
+
GraphQL::ObjectType.define do
|
41
|
+
name model_class.name
|
42
|
+
description "Get #{model_class.name}"
|
43
|
+
|
44
|
+
if model_class.respond_to?(:columns)
|
45
|
+
model_class.columns.each do |column|
|
46
|
+
field column.name do
|
47
|
+
type graphql_type(column)
|
48
|
+
resolve -> (obj, args, ctx) { graphql_fetch(obj, ctx, column.name) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if model_class.respond_to?(:fields)
|
54
|
+
model_class.fields.each do |field_name, field_type|
|
55
|
+
field field_name, graphql_type_of(field_type)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if model_class.respond_to?(:reflections)
|
60
|
+
model_class.reflections.each do |name, association|
|
61
|
+
field name do
|
62
|
+
if association.collection?
|
63
|
+
type types[object_types[association.class_name.constantize]]
|
64
|
+
else
|
65
|
+
type object_types[association.class_name.constantize]
|
66
|
+
end
|
67
|
+
resolve -> (obj, args, ctx) { graphql_fetch(obj, ctx, name) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_command_type(object_type)
|
76
|
+
object_types = @types
|
77
|
+
|
78
|
+
GraphQL::Relay::Mutation.define do
|
79
|
+
name object_type.name
|
80
|
+
description "Command #{object_type.name}"
|
81
|
+
|
82
|
+
object_type.inputs.each do |input, type|
|
83
|
+
input_field input, graphql_type_of(type)
|
84
|
+
end
|
85
|
+
|
86
|
+
object_type.returns.each do |return_name, return_type|
|
87
|
+
return_field return_name, graphql_type_for_object(return_type, object_types)
|
88
|
+
end
|
89
|
+
|
90
|
+
resolve -> (inputs, ctx) {
|
91
|
+
object_type.new(inputs, ctx).perform
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_mutation(model_class)
|
97
|
+
return nil unless model_class < ActiveRecord::Base
|
98
|
+
|
99
|
+
object_types = @types
|
100
|
+
|
101
|
+
GraphQL::Relay::Mutation.define do
|
102
|
+
name "Create#{model_class.name}"
|
103
|
+
description "Create #{model_class.name}"
|
104
|
+
|
105
|
+
model_class.columns.each do |column|
|
106
|
+
input_field column.name, graphql_type(column)
|
107
|
+
end
|
108
|
+
|
109
|
+
return_field model_class.name.underscore.to_sym, object_types[model_class]
|
110
|
+
|
111
|
+
resolve -> (inputs, ctx) {
|
112
|
+
item = model_class.create!(inputs.to_h)
|
113
|
+
{model_class.name.underscore.to_sym => item}
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def update_mutation(model_class)
|
119
|
+
return nil unless model_class < ActiveRecord::Base
|
120
|
+
|
121
|
+
object_types = @types
|
122
|
+
|
123
|
+
GraphQL::Relay::Mutation.define do
|
124
|
+
name "Update#{model_class.name}"
|
125
|
+
description "Update #{model_class.name}"
|
126
|
+
|
127
|
+
input_field :id, !types.ID
|
128
|
+
model_class.columns.each do |column|
|
129
|
+
input_field column.name, graphql_type(column)
|
130
|
+
end
|
131
|
+
|
132
|
+
return_field model_class.name.underscore.to_sym, object_types[model_class]
|
133
|
+
|
134
|
+
resolve -> (inputs, ctx) {
|
135
|
+
item = model_class.find(inputs[:id])
|
136
|
+
item.update!(inputs.to_h)
|
137
|
+
{model_class.name.underscore.to_sym => item}
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def delete_mutation(model_class)
|
143
|
+
return nil unless model_class < ActiveRecord::Base
|
144
|
+
|
145
|
+
GraphQL::Relay::Mutation.define do
|
146
|
+
name "Delete#{model_class.name}"
|
147
|
+
description "Delete #{model_class.name}"
|
148
|
+
|
149
|
+
input_field :id, !types.ID
|
150
|
+
|
151
|
+
return_field "#{model_class.name.underscore}_id".to_sym, types.ID
|
152
|
+
|
153
|
+
resolve -> (inputs, ctx) {
|
154
|
+
item = model_class.find(inputs[:id]).destroy!
|
155
|
+
{"#{model_class.name.underscore}_id".to_sym => item.id}
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def query(&block)
|
161
|
+
object_types = @types
|
162
|
+
|
163
|
+
@query ||= GraphQL::ObjectType.define do
|
164
|
+
name 'Query'
|
165
|
+
description 'The query root for this schema'
|
166
|
+
|
167
|
+
instance_eval(&block) if block
|
168
|
+
|
169
|
+
object_types.each do |object_class, graph_type|
|
170
|
+
if object_class < ActiveRecord::Base
|
171
|
+
|
172
|
+
field(object_class.name.camelize(:lower)) do
|
173
|
+
type graph_type
|
174
|
+
argument :id, types.ID
|
175
|
+
|
176
|
+
if object_class.respond_to?(:arguments)
|
177
|
+
object_class.arguments.each do |arg|
|
178
|
+
argument arg, graphql_type(object_class.columns.find { |c| c.name.to_sym == arg.to_sym })
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
resolve -> (obj, args, ctx) {
|
183
|
+
if object_class.respond_to?(:graph_find)
|
184
|
+
object_class.graph_find(args, ctx)
|
185
|
+
else
|
186
|
+
object_class.find_by!(args.to_h)
|
187
|
+
end
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
field(object_class.name.camelize(:lower).pluralize) do
|
192
|
+
type types[graph_type]
|
193
|
+
argument :limit, types.Int
|
194
|
+
|
195
|
+
if object_class.respond_to?(:arguments)
|
196
|
+
object_class.arguments.each do |arg|
|
197
|
+
argument arg, graphql_type(object_class.columns.find { |c| c.name.to_sym == arg.to_sym })
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
resolve -> (obj, args, ctx) {
|
202
|
+
if object_class.respond_to?(:graph_where)
|
203
|
+
object_class.graph_where(args, ctx)
|
204
|
+
else
|
205
|
+
eager_load = []
|
206
|
+
ctx.irep_node.children.each do |child|
|
207
|
+
eager_load << child[0] if object_class.reflections.find { |name, _| name == child[0] }
|
208
|
+
end
|
209
|
+
|
210
|
+
query_args = args.to_h
|
211
|
+
query_args.delete('limit')
|
212
|
+
|
213
|
+
q = object_class.where(query_args)
|
214
|
+
q.eager_load(*eager_load) if eager_load.any?
|
215
|
+
q.limit(args[:limit] || 30)
|
216
|
+
end
|
217
|
+
}
|
218
|
+
end
|
219
|
+
|
220
|
+
elsif object_class.respond_to?(:arguments) && object_class.respond_to?(:return_type)
|
221
|
+
|
222
|
+
field(object_class.name.camelize(:lower)) do
|
223
|
+
type(graphql_type_for_object(object_class.return_type, object_types))
|
224
|
+
|
225
|
+
object_class.arguments.each do |argument_name, argument_type|
|
226
|
+
argument argument_name, graphql_type_of(argument_type)
|
227
|
+
end
|
228
|
+
|
229
|
+
resolve -> (obj, args, ctx) {
|
230
|
+
object_class.new(args, ctx).execute
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def mutation(&block)
|
241
|
+
mutations = @mutations
|
242
|
+
|
243
|
+
@mutation ||= GraphQL::ObjectType.define do
|
244
|
+
name 'Mutation'
|
245
|
+
instance_eval(&block) if block
|
246
|
+
|
247
|
+
mutations.each do |model_class, muts|
|
248
|
+
muts.each do |mutation|
|
249
|
+
field mutation[0], field: mutation[1].field
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def schema
|
256
|
+
@schema ||= GraphQL::Schema.define(query: query, mutation: mutation)
|
257
|
+
end
|
258
|
+
|
259
|
+
def build_model_types
|
260
|
+
all_models.each { |model_class| @types[model_class] = create_type(model_class) }
|
261
|
+
end
|
262
|
+
|
263
|
+
def build_object_types
|
264
|
+
all_queries.each { |query| @types[query] = nil }
|
265
|
+
end
|
266
|
+
|
267
|
+
def build_mutations
|
268
|
+
all_models.each do |model_class|
|
269
|
+
@mutations[model_class] = [
|
270
|
+
["create#{model_class.name}", create_mutation(model_class)],
|
271
|
+
["update#{model_class.name}", update_mutation(model_class)],
|
272
|
+
["delete#{model_class.name}", delete_mutation(model_class)],
|
273
|
+
].map { |x| x if x[1] }.compact
|
274
|
+
end
|
275
|
+
|
276
|
+
all_commands.each do |command|
|
277
|
+
@mutations[command] = [
|
278
|
+
[command.name.camelize(:lower), create_command_type(command)]
|
279
|
+
]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Colin Walker
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 5.0.0.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 5.0.0
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 5.0.0.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: graphql
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
description:
|
62
|
+
email:
|
63
|
+
- colinwalker270@gmail.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- MIT-LICENSE
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- lib/graphql/api.rb
|
72
|
+
- lib/graphql/api/command_type.rb
|
73
|
+
- lib/graphql/api/helpers.rb
|
74
|
+
- lib/graphql/api/query_type.rb
|
75
|
+
- lib/graphql/api/schema.rb
|
76
|
+
- lib/graphql/api/schema_error.rb
|
77
|
+
- lib/graphql/api/version.rb
|
78
|
+
homepage: https://github.com/coldog/graphql-api
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 2.5.1
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Rails graphql framework.
|
102
|
+
test_files: []
|