graphql-rails-api 0.8.0 → 0.9.2
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 +269 -34
- data/lib/generators/graphql_add_fields/graphql_add_fields_generator.rb +3 -3
- data/lib/generators/graphql_all_connections/graphql_all_connections_generator.rb +1 -0
- data/lib/generators/graphql_mutations/graphql_mutations_generator.rb +1 -1
- data/lib/generators/graphql_rails_api/install_generator.rb +10 -95
- data/lib/generators/graphql_resource/graphql_resource_generator.rb +10 -6
- data/lib/graphql/hydrate_query.rb +299 -32
- data/lib/graphql/rails/api/config.rb +3 -4
- data/lib/graphql/rails/api/version.rb +1 -1
- metadata +13 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e7801279a80cf36fceb00b2c734cda2ff89e0c695fa77efa10e706e10fb708b
|
4
|
+
data.tar.gz: e0fa94077d38b530f56617ccf14825f330d427c510bf44259f43baf2d28ffc8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45940578253a9645e325860234d793dc8c00c7fdb69c5758748f45fe66d09c32f08e43d60b3180537953dead3f2b5edf470fe2516cf40a5ed0a237124cd3df8e
|
7
|
+
data.tar.gz: 3f830d57e8ae131cae74735ba29ea24e3a8b2391899aea329b18aec18fc3b75647e3d085201211c514fd6d397dba1739ae0471f0ea906e0650c6214fbcf429ee
|
data/README.md
CHANGED
@@ -1,48 +1,87 @@
|
|
1
|
-
#
|
1
|
+
# Graphql Rails Api
|
2
|
+
`graphql-rails-api` is a wrapper around [graphql-ruby](https://graphql-ruby.org/) for rails application. It describes easily your graphql API in a domain driven design way.
|
2
3
|
|
3
|
-
|
4
|
+
The main purpose of this gem is to earn time providing:
|
5
|
+
- A normalized code architecture by mapping graphql resources to the active record models
|
6
|
+
- A global graphql service to directly perform crud mutations
|
7
|
+
- A set of generators to create or modify graphql resource and active record model at the same time
|
4
8
|
|
5
|
-
|
6
|
-
|
9
|
+
### Notes
|
10
|
+
Only postgresql adapter is maintained.
|
11
|
+
Only model using uuid as identifier are compatible with generated migrations right now.
|
12
|
+
A model User will be created during installation.
|
7
13
|
|
8
14
|
## Installation
|
9
15
|
|
10
|
-
|
11
|
-
```
|
12
|
-
rails new project-name --api --database=postgresql
|
13
|
-
cd project-name
|
14
|
-
bundle
|
15
|
-
rails db:create
|
16
|
-
```
|
17
|
-
|
18
|
-
Add these lines to your application's Gemfile:
|
16
|
+
Add the gem to your application's Gemfile:
|
19
17
|
```ruby
|
20
|
-
gem 'graphql'
|
21
18
|
gem 'graphql-rails-api'
|
22
19
|
```
|
23
20
|
|
24
|
-
|
21
|
+
Download and install the gem:
|
25
22
|
```bash
|
26
23
|
$ bundle
|
27
24
|
$ rails generate graphql_rails_api:install
|
28
25
|
```
|
29
26
|
|
27
|
+
### Options
|
28
|
+
The following options to the `graphql_rails_api:install` command are available:
|
29
|
+
|
30
|
+
|
30
31
|
To disable PostgreSQL uuid extension, add the option `--no-pg-uuid`
|
31
32
|
|
32
33
|
To disable ActionCable websocket subscriptions, add the option `--no-action-cable-subs`
|
33
34
|
|
34
35
|
To disable Apollo compatibility, add the option `--no-apollo-compatibility`
|
35
36
|
|
36
|
-
|
37
|
-
To avoid this, you can just add the option `--no-generate-graphql-route`
|
37
|
+
To avoid the addition of a new post '/graphql' route , add the option `--no-generate-graphql-route`
|
38
38
|
|
39
|
-
|
39
|
+
## Get Started
|
40
40
|
|
41
|
-
|
41
|
+
Generate a new active record model with its graphql type and input type.
|
42
|
+
```bash
|
43
|
+
$ rails generate graphql_resource city name:string
|
44
|
+
```
|
45
|
+
Reboot the rails server, and you're good to go!
|
42
46
|
|
47
|
+
Now You can perform crud mutation on resources:
|
43
48
|
```bash
|
44
|
-
|
49
|
+
curl -X POST http://localhost:3000/graphql \
|
50
|
+
-H "content-type: application/json" \
|
51
|
+
-d '{ "query":"mutation($name: String) { create_city(city: { name: $name }) { name } }", "variables": {"name":"Paris"} }'
|
52
|
+
|
53
|
+
=> {"data":{"create_city":{"name":"Paris"}}}
|
45
54
|
```
|
55
|
+
You can perform queries as well:
|
56
|
+
```bash
|
57
|
+
curl -X POST http://localhost:3000/graphql \
|
58
|
+
-H "content-type: application/json" \
|
59
|
+
-d '{ "query": "{ cities { name } }" }'
|
60
|
+
|
61
|
+
=> {"data":{"cities":[ {"name":"Paris"} ] } }
|
62
|
+
```
|
63
|
+
|
64
|
+
## Generators
|
65
|
+
|
66
|
+
```bash
|
67
|
+
$ rails generate graphql_resource computer code:string price:integer power_bench:float belongs_to:user has_many:hard_drives many_to_many:tags
|
68
|
+
```
|
69
|
+
|
70
|
+
|
71
|
+
This line will create the data migration, the model and the graphql type of the Computer resource.
|
72
|
+
|
73
|
+
It will automatically add `has_many :computers` to the User model
|
74
|
+
|
75
|
+
It will add a `computer_id` to the `HardDrive` model, and
|
76
|
+
|
77
|
+
respectively the `has_many :hard_drives` and `belongs_to :computer` to the `Computer` and `HardDrive` models.
|
78
|
+
|
79
|
+
The `many_to_many` option will make the `has_many through` association and create the join table between tag and computer.
|
80
|
+
|
81
|
+
All of these relations will be propagated to the graphql types.
|
82
|
+
|
83
|
+
### Options
|
84
|
+
|
46
85
|
|
47
86
|
To disable migration generation, add the option `--no-migration`
|
48
87
|
|
@@ -56,40 +95,236 @@ To disable graphql-type generation, add the option `--no-graphql-type`
|
|
56
95
|
|
57
96
|
To disable graphql-input-type generation, add the option `--no-graphql-input-type`
|
58
97
|
|
59
|
-
To disable propagation (has_many creating the id in the other table, many to many creating the join table
|
60
|
-
|
61
|
-
I made the choice of migrate automatically after generating a resource, to avoid doing it each time.
|
62
|
-
You can of course disable the automatic migrate by adding the option `--no-migrate`
|
98
|
+
To disable propagation (has_many creating the id in the other table, many to many creating the join table and apply to the graphql types), add the option `--no-propagation`
|
99
|
+
To avoid running migrations after a resource generation, add the option `--no-migrate`
|
63
100
|
|
64
|
-
## On generating resources
|
65
101
|
|
102
|
+
### Note on enum
|
103
|
+
The library handle enum with an integer column in the model table. Enum is defined into the active record model.
|
104
|
+
Example:
|
66
105
|
```bash
|
67
|
-
$ rails generate graphql_resource
|
106
|
+
$ rails generate graphql_resource house energy_grade:integer belongs_to:user belongs_to:city
|
107
|
+
```
|
108
|
+
house.rb
|
109
|
+
```ruby
|
110
|
+
class House < ApplicationRecord
|
111
|
+
belongs_to :city
|
112
|
+
belongs_to :user
|
113
|
+
enum energy_grade: {
|
114
|
+
good: 0,
|
115
|
+
average: 1,
|
116
|
+
bad: 2,
|
117
|
+
}
|
118
|
+
end
|
68
119
|
```
|
69
120
|
|
70
|
-
|
121
|
+
## About queries
|
122
|
+
3 types of queries are available.
|
71
123
|
|
72
|
-
|
124
|
+
show query:
|
125
|
+
```gql
|
126
|
+
query($id: String!) {
|
127
|
+
city(id: $id) {
|
128
|
+
id
|
129
|
+
}
|
130
|
+
}
|
131
|
+
```
|
132
|
+
index query:
|
133
|
+
A max number of 1000 results are return.
|
134
|
+
```gql
|
135
|
+
query($page: String, per_page: String, $filter: String, $order_by: String) {
|
136
|
+
cities {
|
137
|
+
id
|
138
|
+
}
|
139
|
+
}
|
140
|
+
```
|
141
|
+
index query with pagination: Add the suffix `paginated_` to any index query to use pagination
|
142
|
+
```gql
|
143
|
+
query($page: String, per_page: String, $filter: String, $order_by: String) {
|
144
|
+
paginated_cities {
|
145
|
+
id
|
146
|
+
}
|
147
|
+
}
|
148
|
+
```
|
73
149
|
|
74
|
-
|
75
|
-
|
150
|
+
### Filter query results
|
151
|
+
`filter` is a not required string argument used to filter data based on conditions made on active record model fields.
|
152
|
+
You can use :
|
153
|
+
- parenthesis : `()`
|
154
|
+
- Logical operators : `&&` , `||`
|
155
|
+
- Comparaisons operators : `==`, `!=`, `===`, `!==`, `>`, `<`, `>=`, `<=`
|
76
156
|
|
77
|
-
The
|
157
|
+
The operators `===` and `!==` are used to perform case sensitive comparaisons on string fields
|
158
|
+
Example:
|
159
|
+
The following model is generated
|
160
|
+
```
|
161
|
+
rails generate graphql_resource house \
|
162
|
+
street:string \
|
163
|
+
number:integer \
|
164
|
+
price:float \
|
165
|
+
energy_grade:integer \
|
166
|
+
principal:boolean \
|
167
|
+
belongs_to:user \
|
168
|
+
belongs_to:city
|
169
|
+
```
|
170
|
+
The following filter values can be used:
|
171
|
+
```ruby
|
172
|
+
"street != 'candlewood lane'"
|
173
|
+
"street !== 'Candlewood Lane'"
|
174
|
+
"number <= 50"
|
175
|
+
"price != 50000"
|
176
|
+
"build_at <= '#{DateTime.now - 2.years}'"
|
177
|
+
"user.email == 'jason@gmail.com'"
|
178
|
+
"city.name != 'Berlin'"
|
179
|
+
"street != 'candlewood lane' && (city.name != 'Berlin' || user.email == 'jason@gmail.com')"
|
180
|
+
```
|
78
181
|
|
79
|
-
|
182
|
+
### Order query result argument
|
183
|
+
`order_by` is a non mandatory string argument used to order the returned data.
|
184
|
+
With the model above the following order_by values can be used:
|
185
|
+
```ruby
|
186
|
+
"street DESC"
|
187
|
+
"number ASC"
|
188
|
+
"user.email ASC"
|
189
|
+
```
|
190
|
+
## About mutations
|
191
|
+
The graphql-rails-api application service can handle 5 types of mutation on generated models.
|
192
|
+
|
193
|
+
create mutation:
|
194
|
+
```gql
|
195
|
+
mutation($name: String!) {
|
196
|
+
create_city(
|
197
|
+
city: {
|
198
|
+
name: $name
|
199
|
+
}
|
200
|
+
) {
|
201
|
+
id
|
202
|
+
}
|
203
|
+
}
|
204
|
+
```
|
80
205
|
|
206
|
+
update mutation:
|
207
|
+
```gql
|
208
|
+
mutation($id: String!, $name: String) {
|
209
|
+
update_city(
|
210
|
+
id: $id
|
211
|
+
city: {
|
212
|
+
name: $name
|
213
|
+
}
|
214
|
+
) {
|
215
|
+
id
|
216
|
+
name
|
217
|
+
}
|
218
|
+
}
|
219
|
+
```
|
81
220
|
|
221
|
+
destroy mutation:
|
222
|
+
```gql
|
223
|
+
mutation($id: String!) {
|
224
|
+
destroy_city(id: $id) {
|
225
|
+
id
|
226
|
+
}
|
227
|
+
}
|
228
|
+
```
|
82
229
|
|
83
|
-
|
230
|
+
bulk_create mutation:
|
231
|
+
```gql
|
232
|
+
mutation($cities: [CityInputType]!) {
|
233
|
+
bulk_create_city(cities: $cities) {
|
234
|
+
id
|
235
|
+
}
|
236
|
+
}
|
237
|
+
```
|
84
238
|
|
85
|
-
|
239
|
+
bulk_update:
|
240
|
+
```gql
|
241
|
+
mutation($cities: [CityInputType]!) {
|
242
|
+
bulk_update_city(cities: $cities) {
|
243
|
+
id
|
244
|
+
}
|
245
|
+
}
|
246
|
+
```
|
86
247
|
|
248
|
+
You can override the default application service for all mutation by defining your own method into the corresponding graphql service:
|
87
249
|
|
250
|
+
Example:
|
251
|
+
|
252
|
+
app/graphql/cities/service.rb
|
253
|
+
```ruby
|
254
|
+
module Cities
|
255
|
+
class Service < ApplicationService
|
256
|
+
def create
|
257
|
+
return graphql_error('Forbidden') if params[:name] == 'Forbidden city'
|
258
|
+
|
259
|
+
super
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
```
|
264
|
+
## Custom mutation resource services
|
265
|
+
|
266
|
+
To defined your own custom mutation create a file to defined the mutation type and define the correponding methods.
|
267
|
+
|
268
|
+
Example:
|
269
|
+
|
270
|
+
app/graphql/cities/mutations/custom.rb
|
271
|
+
```ruby
|
272
|
+
Cities::Mutations::Custom = GraphQL::Field.define do
|
273
|
+
description 'Im a custom mutation'
|
274
|
+
type Cities::Type
|
275
|
+
|
276
|
+
argument :id, !types.String
|
277
|
+
argument :name, !types.String
|
278
|
+
|
279
|
+
resolve ApplicationService.call(:city, :custom)
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
app/graphql/cities/service.rb
|
284
|
+
```ruby
|
285
|
+
module Cities
|
286
|
+
class Service < ApplicationService
|
287
|
+
def custom
|
288
|
+
...
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## TODO
|
295
|
+
|
296
|
+
- Clean error management : catching on raise ... no unexpected json blabla
|
297
|
+
- Query type
|
298
|
+
- remove me type
|
299
|
+
- add paginated_resource types
|
300
|
+
- Logging on generators actions
|
301
|
+
- Don't create model user if it exists
|
302
|
+
- Config variable for max number of result (Per page on query)
|
303
|
+
- Handle id type != uuid (generated migration)
|
304
|
+
- Filter:
|
305
|
+
- handle association multiple level
|
306
|
+
- Order
|
307
|
+
- handle order on multiple field
|
308
|
+
- handle order on association multiple level
|
309
|
+
- case sensitivity on order by string / text
|
310
|
+
- Write Spec to test visible_for
|
311
|
+
- Write Spec to test filter order and visible for together
|
312
|
+
- Write spec to test installation and generators
|
313
|
+
- Documentation about user authentication and scope
|
314
|
+
- describe authenticated_user
|
315
|
+
- describe visible_for
|
316
|
+
- describe writable_by
|
317
|
+
|
318
|
+
## Graphql API example
|
319
|
+
|
320
|
+
Example of a backend API: https://github.com/Poilon/graphql-rails-api-example
|
88
321
|
|
89
322
|
## Contributing
|
90
323
|
|
91
324
|
You can post any issue, and contribute by making pull requests.
|
92
325
|
Don't hesitate, even the shortest pull request is great help. <3
|
93
326
|
|
327
|
+
Need any help or wanna talk with me on discord : Poilon#5412
|
328
|
+
|
94
329
|
## License
|
95
330
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -245,10 +245,10 @@ t.#{@id_db_type} :#{resource.underscore.singularize}_id
|
|
245
245
|
file_name = "app/models/#{model.underscore.singularize}.rb"
|
246
246
|
return if !File.exist?(file_name) || File.read(file_name).include?(line)
|
247
247
|
|
248
|
-
|
249
|
-
|
248
|
+
file = open(file_name)
|
249
|
+
line_count = file.readlines.size
|
250
250
|
line_nb = 0
|
251
|
-
|
251
|
+
file.each do |l|
|
252
252
|
line_nb += 1
|
253
253
|
break if l.include?('ApplicationRecord')
|
254
254
|
end
|
@@ -3,7 +3,7 @@ class GraphqlMutationsGenerator < Rails::Generators::NamedBase
|
|
3
3
|
def generate
|
4
4
|
resource = file_name.underscore.singularize
|
5
5
|
dir = "app/graphql/#{resource.pluralize}/mutations"
|
6
|
-
|
6
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
7
7
|
generate_create_mutation(dir, resource)
|
8
8
|
generate_update_mutation(dir, resource)
|
9
9
|
generate_destroy_mutation(dir, resource)
|
@@ -8,7 +8,9 @@ module GraphqlRailsApi
|
|
8
8
|
|
9
9
|
def generate_files
|
10
10
|
@app_name = File.basename(Rails.root.to_s).underscore
|
11
|
-
|
11
|
+
|
12
|
+
folder = 'app/graphql/'
|
13
|
+
FileUtils.mkdir_p(folder) unless File.directory?(folder)
|
12
14
|
|
13
15
|
write_uuid_extensions_migration
|
14
16
|
|
@@ -20,9 +22,7 @@ module GraphqlRailsApi
|
|
20
22
|
|
21
23
|
write_controller
|
22
24
|
|
23
|
-
|
24
|
-
write_websocket_connection
|
25
|
-
write_subscriptions_channel
|
25
|
+
system 'rails g graphql_resource user first_name:string last_name:string email:string'
|
26
26
|
|
27
27
|
write_application_record_methods
|
28
28
|
write_initializer
|
@@ -32,12 +32,6 @@ module GraphqlRailsApi
|
|
32
32
|
|
33
33
|
private
|
34
34
|
|
35
|
-
def write_websocket_models
|
36
|
-
system 'rails g graphql_resource user first_name:string last_name:string email:string'
|
37
|
-
system 'rails g graphql_resource websocket_connection belongs_to:user connection_identifier:string'
|
38
|
-
system 'rails g graphql_resource subscribed_query belongs_to:websocket_connection result_hash:string query:string'
|
39
|
-
end
|
40
|
-
|
41
35
|
def write_route
|
42
36
|
route_file = File.read('config/routes.rb')
|
43
37
|
return if route_file.include?('graphql')
|
@@ -47,8 +41,7 @@ module GraphqlRailsApi
|
|
47
41
|
route_file.gsub(
|
48
42
|
"Rails.application.routes.draw do\n",
|
49
43
|
"Rails.application.routes.draw do\n" \
|
50
|
-
" post '/graphql', to: 'graphql#execute'\n"
|
51
|
-
" mount ActionCable.server => '/cable'\n"
|
44
|
+
" post '/graphql', to: 'graphql#execute'\n"
|
52
45
|
)
|
53
46
|
)
|
54
47
|
end
|
@@ -89,25 +82,13 @@ module GraphqlRailsApi
|
|
89
82
|
all
|
90
83
|
end
|
91
84
|
|
92
|
-
def self.broadcast_queries
|
93
|
-
WebsocketConnection.all.each do |wsc|
|
94
|
-
wsc.subscribed_queries.each do |sq|
|
95
|
-
result = #{@app_name.camelize}Schema.execute(sq.query, context: { current_user: wsc.user })
|
96
|
-
hex = Digest::SHA1.hexdigest(result.to_s)
|
97
|
-
next if sq.result_hash == hex
|
98
|
-
|
99
|
-
sq.update_attributes(result_hash: hex)
|
100
|
-
SubscriptionsChannel.broadcast_to(wsc, query: sq.query, result: result.to_s)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
85
|
|
105
|
-
|
106
|
-
|
107
|
-
|
86
|
+
STRING
|
87
|
+
)
|
88
|
+
end
|
108
89
|
|
109
90
|
def write_require_application_rb
|
110
|
-
write_at('config/application.rb', 5, "require 'graphql/hydrate_query'\nrequire 'rkelly'\n")
|
91
|
+
write_at('config/application.rb', 5, "require 'graphql/hydrate_query'\nrequire 'rkelly'\nrequire 'graphql'\n")
|
111
92
|
end
|
112
93
|
|
113
94
|
def write_uuid_extensions_migration
|
@@ -119,8 +100,7 @@ module GraphqlRailsApi
|
|
119
100
|
class UuidPgExtensions < ActiveRecord::Migration[5.2]
|
120
101
|
|
121
102
|
def change
|
122
|
-
|
123
|
-
execute 'CREATE EXTENSION "uuid-ossp" SCHEMA pg_catalog;'
|
103
|
+
enable_extension 'pgcrypto'
|
124
104
|
end
|
125
105
|
|
126
106
|
end
|
@@ -139,70 +119,6 @@ module GraphqlRailsApi
|
|
139
119
|
)
|
140
120
|
end
|
141
121
|
|
142
|
-
def write_websocket_connection
|
143
|
-
File.write(
|
144
|
-
'app/channels/application_cable/connection.rb',
|
145
|
-
<<~'STRING'
|
146
|
-
module ApplicationCable
|
147
|
-
class Connection < ActionCable::Connection::Base
|
148
|
-
|
149
|
-
identified_by :websocket_connection
|
150
|
-
|
151
|
-
def connect
|
152
|
-
# Check authentication, and define current user
|
153
|
-
self.websocket_connection = WebsocketConnection.create(
|
154
|
-
# user_id: current_user.id
|
155
|
-
)
|
156
|
-
end
|
157
|
-
|
158
|
-
end
|
159
|
-
end
|
160
|
-
STRING
|
161
|
-
)
|
162
|
-
end
|
163
|
-
|
164
|
-
def write_subscriptions_channel
|
165
|
-
File.write(
|
166
|
-
'app/channels/subscriptions_channel.rb',
|
167
|
-
<<~STRING
|
168
|
-
class SubscriptionsChannel < ApplicationCable::Channel
|
169
|
-
|
170
|
-
def subscribed
|
171
|
-
stream_for(websocket_connection)
|
172
|
-
websocket_connection.update_attributes(connection_identifier: connection.connection_identifier)
|
173
|
-
ci = ActionCable.server.connections.map(&:connection_identifier)
|
174
|
-
WebsocketConnection.all.each do |wsc|
|
175
|
-
wsc.destroy unless ci.include?(wsc.connection_identifier)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def subscribe_to_query(data)
|
180
|
-
websocket_connection.subscribed_queries.find_or_create_by(query: data['query'])
|
181
|
-
SubscriptionsChannel.broadcast_to(
|
182
|
-
websocket_connection,
|
183
|
-
query: data['query'],
|
184
|
-
result: #{@app_name.camelize}Schema.execute(data['query'], context: { current_user: websocket_connection.user })
|
185
|
-
)
|
186
|
-
end
|
187
|
-
|
188
|
-
def unsubscribe_to_query(data)
|
189
|
-
websocket_connection.subscribed_queries.find_by(query: data['query'])&.destroy
|
190
|
-
end
|
191
|
-
|
192
|
-
def unsubscribed
|
193
|
-
websocket_connection.destroy
|
194
|
-
ci = ActionCable.server.connections.map(&:connection_identifier)
|
195
|
-
WebsocketConnection.all.each do |wsc|
|
196
|
-
wsc.destroy unless ci.include?(wsc.connection_identifier)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
end
|
201
|
-
|
202
|
-
STRING
|
203
|
-
)
|
204
|
-
end
|
205
|
-
|
206
122
|
def write_controller
|
207
123
|
File.write(
|
208
124
|
'app/controllers/graphql_controller.rb',
|
@@ -217,7 +133,6 @@ module GraphqlRailsApi
|
|
217
133
|
context: { current_user: authenticated_user },
|
218
134
|
operation_name: params[:operationName]
|
219
135
|
)
|
220
|
-
ApplicationRecord.broadcast_queries
|
221
136
|
render json: result
|
222
137
|
end
|
223
138
|
|
@@ -103,7 +103,7 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
103
103
|
end
|
104
104
|
|
105
105
|
def generate_basic_mutations(resource)
|
106
|
-
|
106
|
+
FileUtils.mkdir_p(@mutations_directory) unless File.directory?(@mutations_directory)
|
107
107
|
system("rails generate graphql_mutations #{resource}")
|
108
108
|
|
109
109
|
# Graphql Input Type
|
@@ -126,7 +126,8 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
126
126
|
end
|
127
127
|
|
128
128
|
def generate_graphql_input_type(resource)
|
129
|
-
|
129
|
+
FileUtils.mkdir_p(@mutations_directory) unless File.directory?(@mutations_directory)
|
130
|
+
|
130
131
|
File.write(
|
131
132
|
"#{@mutations_directory}/input_type.rb",
|
132
133
|
<<~STRING
|
@@ -241,6 +242,9 @@ class GraphqlResourceGenerator < Rails::Generators::NamedBase
|
|
241
242
|
end
|
242
243
|
|
243
244
|
def generate_service(resource)
|
245
|
+
|
246
|
+
FileUtils.mkdir_p("app/graphql/#{resource.pluralize}/") unless File.directory?("app/graphql/#{resource.pluralize}/")
|
247
|
+
|
244
248
|
File.write(
|
245
249
|
"app/graphql/#{resource.pluralize}/service.rb",
|
246
250
|
<<~STRING
|
@@ -308,13 +312,13 @@ t.#{@id_db_type} :#{resource.underscore.singularize}_id
|
|
308
312
|
end
|
309
313
|
|
310
314
|
def add_to_model(model, line)
|
311
|
-
file_name = "app
|
315
|
+
file_name = File.join("app","models","#{model.underscore.singularize}.rb")
|
312
316
|
return if !File.exist?(file_name) || File.read(file_name).include?(line)
|
313
317
|
|
314
|
-
|
315
|
-
|
318
|
+
file = open(file_name)
|
319
|
+
line_count = file.readlines.size
|
316
320
|
line_nb = 0
|
317
|
-
|
321
|
+
file.each do |l|
|
318
322
|
line_nb += 1
|
319
323
|
break if l.include?('ApplicationRecord')
|
320
324
|
end
|
@@ -1,40 +1,311 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "deep_pluck"
|
2
|
+
require "rkelly"
|
3
3
|
|
4
4
|
module Graphql
|
5
5
|
class HydrateQuery
|
6
|
-
|
7
|
-
def initialize(model, context, order_by: nil, filter: nil, id: nil, user: nil, page: nil, per_page: 10)
|
6
|
+
def initialize(model, context, order_by: nil, filter: nil, check_visibility: true, id: nil, user: nil, page: nil, per_page: nil)
|
8
7
|
@context = context
|
9
8
|
@filter = filter
|
10
9
|
@order_by = order_by
|
11
10
|
@model = model
|
12
|
-
@
|
11
|
+
@check_visibility = check_visibility
|
12
|
+
|
13
|
+
if id.present? && !valid_id?(id)
|
14
|
+
raise GraphQL::ExecutionError, "Invalid id: #{id}"
|
15
|
+
end
|
16
|
+
|
13
17
|
@id = id
|
14
18
|
@user = user
|
15
|
-
@page = page
|
16
|
-
@per_page = per_page
|
19
|
+
@page = page&.to_i || 1
|
20
|
+
@per_page = per_page&.to_i || 1000
|
21
|
+
@per_page = 1000 if @per_page > 1000
|
17
22
|
end
|
18
23
|
|
19
24
|
def run
|
20
|
-
|
21
|
-
|
25
|
+
if @id
|
26
|
+
@model = @model.where(id: @id)
|
27
|
+
deep_pluck_to_structs(@context&.irep_node).first
|
28
|
+
else
|
29
|
+
@model = @model.limit(@per_page)
|
30
|
+
@model = @model.offset(@per_page * (@page - 1))
|
31
|
+
|
32
|
+
transform_filter if @filter.present?
|
33
|
+
transform_order if @order_by.present?
|
34
|
+
|
35
|
+
deep_pluck_to_structs(@context&.irep_node)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def paginated_run
|
40
|
+
transform_filter if @filter.present?
|
41
|
+
transform_order if @order_by.present?
|
42
|
+
|
43
|
+
@total = @model.length
|
44
|
+
@model = @model.limit(@per_page)
|
45
|
+
@model = @model.offset(@per_page * (@page - 1))
|
46
|
+
|
47
|
+
OpenStruct.new(
|
48
|
+
data: deep_pluck_to_structs(@context&.irep_node&.typed_children&.values&.first.try(:[], "data")),
|
49
|
+
total_count: @total,
|
50
|
+
per_page: @per_page,
|
51
|
+
page: @page,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def transform_order
|
58
|
+
return if @order_by.blank?
|
59
|
+
|
60
|
+
sign = @order_by.split(" ").last.downcase == "desc" ? "desc" : "asc"
|
61
|
+
column = @order_by.split(" ").first.strip
|
62
|
+
|
63
|
+
if column.include?(".")
|
64
|
+
associated_model = column.split(".").first
|
65
|
+
accessor = column.split(".").last
|
66
|
+
assoc = get_assoc!(@model, associated_model)
|
67
|
+
field_type = get_field_type!(assoc.klass, accessor)
|
68
|
+
@model = @model.left_joins(associated_model.to_sym)
|
69
|
+
ordered_field = "#{associated_model.pluralize}.#{accessor}"
|
70
|
+
else
|
71
|
+
field_type = get_field_type!(@model, column)
|
72
|
+
ordered_field = "#{model_name.pluralize}.#{column}"
|
73
|
+
end
|
74
|
+
|
75
|
+
if %i[string text].include?(field_type)
|
76
|
+
@model = @model.order(Arel.sql("upper(#{ordered_field}) #{sign}"))
|
77
|
+
else
|
78
|
+
@model = @model.order(Arel.sql("#{ordered_field} #{sign}"))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def transform_filter
|
83
|
+
return if @filter.blank?
|
84
|
+
|
85
|
+
ast = RKelly::Parser.new.parse(@filter)
|
86
|
+
exprs = ast.value
|
87
|
+
if exprs.count != 1
|
88
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{@filter}, only one expression allowed"
|
89
|
+
end
|
90
|
+
|
91
|
+
@model = handle_node(exprs.first.value, @model)
|
92
|
+
|
93
|
+
if @need_distinct_results
|
94
|
+
@model = @model.distinct
|
95
|
+
end
|
96
|
+
|
97
|
+
rescue RKelly::SyntaxError => e
|
98
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{e.message}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def handle_node(node, model)
|
102
|
+
if node.class == RKelly::Nodes::ParentheticalNode
|
103
|
+
handle_ParentheticalNode(node, model)
|
104
|
+
elsif node.class == RKelly::Nodes::LogicalAndNode
|
105
|
+
handle_LogicalAndNode(node, model)
|
106
|
+
elsif node.class == RKelly::Nodes::LogicalOrNode
|
107
|
+
handle_LogicalOrNode(node, model)
|
108
|
+
elsif node.class == RKelly::Nodes::NotEqualNode
|
109
|
+
handle_NotEqualNode(node, model)
|
110
|
+
elsif node.class == RKelly::Nodes::EqualNode
|
111
|
+
handle_EqualNode(node, model)
|
112
|
+
elsif node.class == RKelly::Nodes::StrictEqualNode
|
113
|
+
handle_StrictEqualNode(node, model)
|
114
|
+
elsif node.class == RKelly::Nodes::NotStrictEqualNode
|
115
|
+
handle_NotStrictEqualNode(node, model)
|
116
|
+
elsif node.class == RKelly::Nodes::GreaterOrEqualNode
|
117
|
+
handle_GreaterOrEqualNode(node, model)
|
118
|
+
elsif node.class == RKelly::Nodes::LessOrEqualNode
|
119
|
+
handle_LessOrEqualNode(node, model)
|
120
|
+
elsif node.class == RKelly::Nodes::LessNode
|
121
|
+
handle_LessNode(node, model)
|
122
|
+
elsif node.class == RKelly::Nodes::GreaterNode
|
123
|
+
handle_GreaterNode(node, model)
|
124
|
+
else
|
125
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{node.class} unknown operator"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_ParentheticalNode(node, model)
|
130
|
+
handle_node(node.value, model)
|
131
|
+
end
|
132
|
+
|
133
|
+
def handle_LogicalAndNode(node, model)
|
134
|
+
handle_node(node.left, model).and(handle_node(node.value, model))
|
135
|
+
end
|
136
|
+
|
137
|
+
def handle_LogicalOrNode(node, model)
|
138
|
+
handle_node(node.left, model).or(handle_node(node.value, model))
|
139
|
+
end
|
140
|
+
|
141
|
+
def handle_dot_accessor_node(node, model)
|
142
|
+
associated_model = node.left.value.value
|
143
|
+
accessor = node.left.accessor
|
144
|
+
assoc = get_assoc!(model, associated_model)
|
145
|
+
field_type = get_field_type!(assoc.klass, accessor)
|
146
|
+
|
147
|
+
if assoc.association_class == ActiveRecord::Associations::HasManyAssociation
|
148
|
+
@need_distinct_results = true
|
149
|
+
end
|
150
|
+
|
151
|
+
model = model.left_joins(associated_model.to_sym)
|
152
|
+
# field = "#{associated_model.pluralize}.#{accessor}"
|
153
|
+
value = value_from_node(node.value, field_type, accessor.to_sym, model)
|
154
|
+
[assoc.klass.arel_table[accessor], model, field_type, value]
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_resolve_node(node, model)
|
158
|
+
field = node.left.value
|
159
|
+
field_type = get_field_type!(model, field)
|
160
|
+
value = value_from_node(node.value, field_type, field.to_sym, model)
|
161
|
+
[model.klass.arel_table[field], model, field_type, value]
|
162
|
+
end
|
22
163
|
|
23
|
-
|
24
|
-
|
164
|
+
def handle_operator_node(node, model)
|
165
|
+
if node.left.class == RKelly::Nodes::DotAccessorNode
|
166
|
+
handle_dot_accessor_node(node, model)
|
167
|
+
elsif node.left.class == RKelly::Nodes::ResolveNode
|
168
|
+
handle_resolve_node(node, model)
|
169
|
+
else
|
170
|
+
raise GraphQL::ExecutionError, "Invalid left value: #{node.left.class}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def value_from_node(node, sym_type, sym, model)
|
175
|
+
if node.class == RKelly::Nodes::StringNode
|
176
|
+
val = node.value.gsub(/^'|'$|^"|"$/, "")
|
177
|
+
if sym_type == :datetime
|
178
|
+
DateTime.parse(val)
|
179
|
+
elsif sym_type == :date
|
180
|
+
Date.parse(val)
|
181
|
+
elsif sym_type == :integer
|
182
|
+
# Enums are handled here : We are about to compare a string with an integer column
|
183
|
+
# If the symbol and the value correspond to an existing enum into the model
|
184
|
+
if model.klass.defined_enums[sym.to_s]&.keys&.include?(val)
|
185
|
+
# return the corresponding enum value
|
186
|
+
model.klass.defined_enums[sym.to_s][val]
|
187
|
+
else
|
188
|
+
raise GraphQL::ExecutionError, "Invalid value: #{val}, compare a string with an integer column #{sym}"
|
189
|
+
end
|
190
|
+
else
|
191
|
+
val
|
192
|
+
end
|
193
|
+
elsif node.class == RKelly::Nodes::NumberNode
|
194
|
+
node.value
|
195
|
+
elsif node.class == RKelly::Nodes::TrueNode
|
196
|
+
true
|
197
|
+
elsif node.class == RKelly::Nodes::FalseNode
|
198
|
+
false
|
199
|
+
elsif node.class == RKelly::Nodes::NullNode
|
200
|
+
nil
|
201
|
+
else
|
202
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{node} unknown rvalue node"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def sanitize_sql_like(value)
|
207
|
+
ActiveRecord::Base::sanitize_sql_like(value)
|
208
|
+
end
|
209
|
+
|
210
|
+
def handle_NotEqualNode(node, model)
|
211
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
212
|
+
|
213
|
+
if value.nil?
|
214
|
+
model.where.not(arel_field.eq(nil))
|
215
|
+
elsif type == :text || type == :string
|
216
|
+
model.where.not(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
|
217
|
+
else
|
218
|
+
model.where.not(arel_field.eq(value))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def handle_NotStrictEqualNode(node, model)
|
223
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
224
|
+
|
225
|
+
if value.nil?
|
226
|
+
model.where.not(arel_field.eq(nil))
|
227
|
+
elsif type == :text || type == :string
|
228
|
+
model.where.not(arel_field.matches(sanitize_sql_like(value), false, true))
|
229
|
+
else
|
230
|
+
model.where.not(arel_field.eq(value))
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def handle_EqualNode(node, model)
|
235
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
236
|
+
|
237
|
+
if value.nil?
|
238
|
+
model.where(arel_field.eq(nil))
|
239
|
+
elsif type == :text || type == :string
|
240
|
+
model.where(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
|
241
|
+
else
|
242
|
+
model.where(arel_field.eq(value))
|
243
|
+
end
|
244
|
+
end
|
25
245
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
246
|
+
def handle_StrictEqualNode(node, model)
|
247
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
248
|
+
|
249
|
+
if value.nil?
|
250
|
+
model.where(arel_field.eq(nil))
|
251
|
+
elsif type == :text || type == :string
|
252
|
+
model.where(arel_field.matches(sanitize_sql_like(value), false, true))
|
253
|
+
else
|
254
|
+
model.where(arel_field.eq(value))
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def handle_GreaterOrEqualNode(node, model)
|
259
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
260
|
+
model.where(arel_field.gteq(value))
|
261
|
+
end
|
262
|
+
|
263
|
+
def handle_LessOrEqualNode(node, model)
|
264
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
265
|
+
model.where(arel_field.lteq(value))
|
266
|
+
end
|
267
|
+
|
268
|
+
def handle_LessNode(node, model)
|
269
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
270
|
+
model.where(arel_field.lt(value))
|
271
|
+
end
|
272
|
+
|
273
|
+
def handle_GreaterNode(node, model)
|
274
|
+
arel_field, model, type, value = handle_operator_node(node, model)
|
275
|
+
model.where(arel_field.gt(value))
|
276
|
+
end
|
277
|
+
|
278
|
+
def valid_id?(id)
|
279
|
+
valid_uuid?(id) || id.is_a?(Integer)
|
280
|
+
end
|
281
|
+
|
282
|
+
def valid_uuid?(id)
|
283
|
+
id.to_s.match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
|
32
284
|
end
|
33
285
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
286
|
+
def get_assoc!(model, assoc_name)
|
287
|
+
assoc = model.reflect_on_association(assoc_name)
|
288
|
+
unless assoc.present?
|
289
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{assoc_name} is not an association of #{model}"
|
290
|
+
end
|
291
|
+
assoc
|
292
|
+
end
|
293
|
+
|
294
|
+
def get_field_type!(model, field_name)
|
295
|
+
field = model.column_for_attribute(field_name.to_sym)
|
296
|
+
unless field.present?
|
297
|
+
raise GraphQL::ExecutionError, "Invalid filter: #{field_name} is not a field of #{model}"
|
298
|
+
end
|
299
|
+
field.type
|
300
|
+
end
|
301
|
+
|
302
|
+
def deep_pluck_to_structs(irep_node)
|
303
|
+
plucked_attr_to_structs(
|
304
|
+
DeepPluck::Model.new(@model.visible_for(user: @user), user: @user).add(
|
305
|
+
((hash_to_array_of_hashes(parse_fields(irep_node), @model) || []) + [@to_select_to_add]).compact
|
306
|
+
).load_all,
|
307
|
+
model_name.singularize.camelize.constantize
|
308
|
+
)&.compact
|
38
309
|
end
|
39
310
|
|
40
311
|
def plucked_attr_to_structs(arr, parent_model)
|
@@ -56,7 +327,7 @@ module Graphql
|
|
56
327
|
def hash_to_array_of_hashes(hash, parent_class)
|
57
328
|
return if parent_class.nil? || hash.nil?
|
58
329
|
|
59
|
-
hash[
|
330
|
+
hash["id"] = nil if hash["id"].blank?
|
60
331
|
fetch_ids_from_relation(hash)
|
61
332
|
|
62
333
|
hash.each_with_object([]) do |(k, v), arr|
|
@@ -64,18 +335,17 @@ module Graphql
|
|
64
335
|
next arr << v if parent_class.new.attributes.key?(v)
|
65
336
|
|
66
337
|
klass = evaluate_model(parent_class, k)
|
67
|
-
@models << klass.to_s unless @models.include?(klass.to_s)
|
68
338
|
arr << { k.to_sym => hash_to_array_of_hashes(v, klass) } if klass.present? && v.present?
|
69
339
|
end
|
70
340
|
end
|
71
341
|
|
72
342
|
def fetch_ids_from_relation(hash)
|
73
|
-
hash.select { |k, _| k.ends_with?(
|
74
|
-
collection_name = k.gsub(
|
343
|
+
hash.select { |k, _| k.ends_with?("_ids") }.each do |(k, _)|
|
344
|
+
collection_name = k.gsub("_ids", "").pluralize
|
75
345
|
if hash[collection_name].blank?
|
76
|
-
hash[collection_name] = {
|
346
|
+
hash[collection_name] = { "id" => nil }
|
77
347
|
else
|
78
|
-
hash[collection_name][
|
348
|
+
hash[collection_name]["id"] = nil
|
79
349
|
end
|
80
350
|
end
|
81
351
|
end
|
@@ -104,9 +374,7 @@ module Graphql
|
|
104
374
|
|
105
375
|
def parse_fields(irep_node)
|
106
376
|
fields = irep_node&.scoped_children&.values&.first
|
107
|
-
if fields
|
108
|
-
fields = fields['edges'].scoped_children.values.first['node']&.scoped_children&.values&.first
|
109
|
-
end
|
377
|
+
fields = fields["edges"].scoped_children.values.first["node"]&.scoped_children&.values&.first if fields&.key?("edges")
|
110
378
|
return if fields.blank?
|
111
379
|
|
112
380
|
fields.each_with_object({}) do |(k, v), h|
|
@@ -115,8 +383,7 @@ module Graphql
|
|
115
383
|
end
|
116
384
|
|
117
385
|
def model_name
|
118
|
-
@model.class.to_s.split(
|
386
|
+
@model.class.to_s.split("::").first.underscore.pluralize
|
119
387
|
end
|
120
|
-
|
121
388
|
end
|
122
389
|
end
|
@@ -6,23 +6,22 @@ module Graphql
|
|
6
6
|
include Singleton
|
7
7
|
|
8
8
|
def self.query_resources
|
9
|
-
Dir.glob("#{
|
9
|
+
Dir.glob("#{::Rails.root}/app/graphql/*/type.rb").map do |dir|
|
10
10
|
dir.split('/').last(2).first
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.mutation_resources
|
15
|
-
mutations = Dir.glob("#{
|
15
|
+
mutations = Dir.glob("#{::Rails.root}/app/graphql/*/mutations/*.rb").reject do |e|
|
16
16
|
e.end_with?('type.rb', 'types.rb')
|
17
17
|
end
|
18
18
|
mutations = mutations.map { |e| e.split('/').last.gsub('.rb', '') }.uniq
|
19
19
|
mutations.each_with_object({}) do |meth, h|
|
20
|
-
h[meth] = Dir.glob("#{
|
20
|
+
h[meth] = Dir.glob("#{::Rails.root}/app/graphql/*/mutations/#{meth}.rb").map do |dir|
|
21
21
|
dir.split('/').last(3).first
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
26
25
|
end
|
27
26
|
end
|
28
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-rails-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- poilon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.12'
|
20
|
+
- - "<="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.12.13
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - "~>"
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
29
|
+
version: '1.12'
|
30
|
+
- - "<="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.12.13
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: deep_pluck_with_authorization
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,20 +50,14 @@ dependencies:
|
|
44
50
|
requirements:
|
45
51
|
- - ">="
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
48
|
-
- - "~>"
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: 6.0.0
|
53
|
+
version: 5.1.4
|
51
54
|
type: :runtime
|
52
55
|
prerelease: false
|
53
56
|
version_requirements: !ruby/object:Gem::Requirement
|
54
57
|
requirements:
|
55
58
|
- - ">="
|
56
59
|
- !ruby/object:Gem::Version
|
57
|
-
version:
|
58
|
-
- - "~>"
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: 6.0.0
|
60
|
+
version: 5.1.4
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: rkelly-remix
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -114,7 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
114
|
- !ruby/object:Gem::Version
|
115
115
|
version: '0'
|
116
116
|
requirements: []
|
117
|
-
rubygems_version: 3.
|
117
|
+
rubygems_version: 3.3.7
|
118
118
|
signing_key:
|
119
119
|
specification_version: 4
|
120
120
|
summary: Graphql rails api framework to create easily graphql api with rails
|