graphql-rails-api 0.9.1 → 0.9.3.pre.fix

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1751d6f6ae5edf98341bdf30a149cd8aa9ab4dbcb0a627cb25c380cfe15c676
4
- data.tar.gz: 90c9dc17368d908efb70a8ee55837c3d6b117709762c006eaee18ddcd3a820ff
3
+ metadata.gz: 28fb07b00c4ddc6fab6260937a8ab6b5edd025ae6703ab2334a3fba30e56619c
4
+ data.tar.gz: 5991b8d3475a620d0c4a0a6106795b8559123cfc65faf6f9cb3a5f44292778ec
5
5
  SHA512:
6
- metadata.gz: 98cd97012f608dea85e6a5417a4d1ab83ddf3cdae9a51473a014a3c4d45769ce928301a9e57ab32a9c65a76df5e3687cead2046e57a829da18a1309ecfc7ff06
7
- data.tar.gz: d297e02228b4a00c81a9f29eef88c92da12a23698e9e2dc3aa9eab52c3ccf97a471c04aa134d9cb11bb516bc1d2446464ba9b69b8cc0be8d1f84d465deaa2a91
6
+ metadata.gz: 95f60329781d38bf3275a24617868a2baa0fa82812fb4ecd5b79c36f45bbc9ad69a8f98270c797d76df384719417a60fb27a6cd31350c5fb1baa1ceecaae9ceb
7
+ data.tar.gz: 9c76d727e713a14110ea7e77314f446d5c996719abacd228f05202c8c1b7f379e9c1bdb2e67bdc6173bcfc2a6a84a704de6e32d60dd4155f68f769619aaeb822
data/README.md CHANGED
@@ -1,46 +1,87 @@
1
- # GraphqlRailsApi
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
- `graphql-rails-api` is a gem that provides generators to describe easily your graphql API in a domain driven design way.
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
- Need any help or wanna talk with me about it on discord : Poilon#5412
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.
6
13
 
7
14
  ## Installation
8
15
 
9
- Create a rails app via
10
- ```
11
- rails new project-name --api --database=postgresql
12
- cd project-name
13
- bundle
14
- rails db:create
15
- ```
16
-
17
- Add these lines to your application's Gemfile:
16
+ Add the gem to your application's Gemfile:
18
17
  ```ruby
19
18
  gem 'graphql-rails-api'
20
19
  ```
21
20
 
22
- And then execute:
21
+ Download and install the gem:
23
22
  ```bash
24
23
  $ bundle
25
24
  $ rails generate graphql_rails_api:install
26
25
  ```
27
26
 
27
+ ### Options
28
+ The following options to the `graphql_rails_api:install` command are available:
29
+
30
+
28
31
  To disable PostgreSQL uuid extension, add the option `--no-pg-uuid`
29
32
 
30
33
  To disable ActionCable websocket subscriptions, add the option `--no-action-cable-subs`
31
34
 
32
35
  To disable Apollo compatibility, add the option `--no-apollo-compatibility`
33
36
 
34
- Automatically, `post '/graphql', to: 'graphql#execute'` will be added to your `config/route.rb`
35
- 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`
36
38
 
37
- # Usage
39
+ ## Get Started
38
40
 
39
- ## Resources generation
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!
40
46
 
47
+ Now You can perform crud mutation on resources:
41
48
  ```bash
42
- $ rails generate graphql_resource resource_name field1:string field2:float belongs_to:other_resource_name has_many:other_resources_name many_to_many:other_resources_name
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"}}}
43
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
+
44
85
 
45
86
  To disable migration generation, add the option `--no-migration`
46
87
 
@@ -54,40 +95,236 @@ To disable graphql-type generation, add the option `--no-graphql-type`
54
95
 
55
96
  To disable graphql-input-type generation, add the option `--no-graphql-input-type`
56
97
 
57
- 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`
58
-
59
- I made the choice of migrate automatically after generating a resource, to avoid doing it each time.
60
- 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`
61
100
 
62
- ## On generating resources
63
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:
64
105
  ```bash
65
- $ rails generate graphql_resource computer code:string price:integer power_bench:float belongs_to:user has_many:hard_drives many_to_many:tags
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
66
119
  ```
67
120
 
68
- This line will create the data migration, the model and the graphql type of the Computer resource.
121
+ ## About queries
122
+ 3 types of queries are available.
69
123
 
70
- It will automatically add `has_many :computers` to the User model
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
+ ```
71
149
 
72
- It will add a `computer_id` to the `HardDrive` model, and
73
- respectively the `has_many :hard_drives` and `belongs_to :computer` to the `Computer` and `HardDrive` models.
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 : `==`, `!=`, `===`, `!==`, `>`, `<`, `>=`, `<=`
74
156
 
75
- The `many_to_many` option will make the `has_many through` association and create the join table between tag and computer.
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
+ ```
76
181
 
77
- All of these relations will be propagated to the graphql types.
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
+ ```
78
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
+ ```
79
220
 
221
+ destroy mutation:
222
+ ```gql
223
+ mutation($id: String!) {
224
+ destroy_city(id: $id) {
225
+ id
226
+ }
227
+ }
228
+ ```
80
229
 
81
- ## Graphql API example
230
+ bulk_create mutation:
231
+ ```gql
232
+ mutation($cities: [CityInputType]!) {
233
+ bulk_create_city(cities: $cities) {
234
+ id
235
+ }
236
+ }
237
+ ```
82
238
 
83
- Example of a backend API: https://github.com/Poilon/graphql-rails-api-example
239
+ bulk_update:
240
+ ```gql
241
+ mutation($cities: [CityInputType]!) {
242
+ bulk_update_city(cities: $cities) {
243
+ id
244
+ }
245
+ }
246
+ ```
84
247
 
248
+ You can override the default application service for all mutation by defining your own method into the corresponding graphql service:
85
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
86
321
 
87
322
  ## Contributing
88
323
 
89
324
  You can post any issue, and contribute by making pull requests.
90
325
  Don't hesitate, even the shortest pull request is great help. <3
91
326
 
327
+ Need any help or wanna talk with me on discord : Poilon#5412
328
+
92
329
  ## License
93
330
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -8,10 +8,10 @@ module GraphqlRailsApi
8
8
 
9
9
  def generate_files
10
10
  @app_name = File.basename(Rails.root.to_s).underscore
11
-
11
+
12
12
  folder = 'app/graphql/'
13
13
  FileUtils.mkdir_p(folder) unless File.directory?(folder)
14
-
14
+
15
15
  write_uuid_extensions_migration
16
16
 
17
17
  write_service
@@ -22,9 +22,7 @@ module GraphqlRailsApi
22
22
 
23
23
  write_controller
24
24
 
25
- write_websocket_models
26
- write_websocket_connection
27
- write_subscriptions_channel
25
+ system 'rails g graphql_resource user first_name:string last_name:string email:string'
28
26
 
29
27
  write_application_record_methods
30
28
  write_initializer
@@ -34,12 +32,6 @@ module GraphqlRailsApi
34
32
 
35
33
  private
36
34
 
37
- def write_websocket_models
38
- system 'rails g graphql_resource user first_name:string last_name:string email:string'
39
- system 'rails g graphql_resource websocket_connection belongs_to:user connection_identifier:string'
40
- system 'rails g graphql_resource subscribed_query belongs_to:websocket_connection result_hash:string query:string'
41
- end
42
-
43
35
  def write_route
44
36
  route_file = File.read('config/routes.rb')
45
37
  return if route_file.include?('graphql')
@@ -49,8 +41,7 @@ module GraphqlRailsApi
49
41
  route_file.gsub(
50
42
  "Rails.application.routes.draw do\n",
51
43
  "Rails.application.routes.draw do\n" \
52
- " post '/graphql', to: 'graphql#execute'\n" \
53
- " mount ActionCable.server => '/cable'\n"
44
+ " post '/graphql', to: 'graphql#execute'\n"
54
45
  )
55
46
  )
56
47
  end
@@ -91,25 +82,13 @@ module GraphqlRailsApi
91
82
  all
92
83
  end
93
84
 
94
- def self.broadcast_queries
95
- WebsocketConnection.all.each do |wsc|
96
- wsc.subscribed_queries.each do |sq|
97
- result = #{@app_name.camelize}Schema.execute(sq.query, context: { current_user: wsc.user })
98
- hex = Digest::SHA1.hexdigest(result.to_s)
99
- next if sq.result_hash == hex
100
85
 
101
- sq.update_attributes(result_hash: hex)
102
- SubscriptionsChannel.broadcast_to(wsc, query: sq.query, result: result.to_s)
103
- end
104
- end
105
- end
106
-
107
- STRING
108
- )
109
- end
86
+ STRING
87
+ )
88
+ end
110
89
 
111
90
  def write_require_application_rb
112
- 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")
113
92
  end
114
93
 
115
94
  def write_uuid_extensions_migration
@@ -121,8 +100,7 @@ module GraphqlRailsApi
121
100
  class UuidPgExtensions < ActiveRecord::Migration[5.2]
122
101
 
123
102
  def change
124
- execute 'CREATE EXTENSION "pgcrypto" SCHEMA pg_catalog;'
125
- execute 'CREATE EXTENSION "uuid-ossp" SCHEMA pg_catalog;'
103
+ enable_extension 'pgcrypto'
126
104
  end
127
105
 
128
106
  end
@@ -141,70 +119,6 @@ module GraphqlRailsApi
141
119
  )
142
120
  end
143
121
 
144
- def write_websocket_connection
145
- File.write(
146
- 'app/channels/application_cable/connection.rb',
147
- <<~'STRING'
148
- module ApplicationCable
149
- class Connection < ActionCable::Connection::Base
150
-
151
- identified_by :websocket_connection
152
-
153
- def connect
154
- # Check authentication, and define current user
155
- self.websocket_connection = WebsocketConnection.create(
156
- # user_id: current_user.id
157
- )
158
- end
159
-
160
- end
161
- end
162
- STRING
163
- )
164
- end
165
-
166
- def write_subscriptions_channel
167
- File.write(
168
- 'app/channels/subscriptions_channel.rb',
169
- <<~STRING
170
- class SubscriptionsChannel < ApplicationCable::Channel
171
-
172
- def subscribed
173
- stream_for(websocket_connection)
174
- websocket_connection.update_attributes(connection_identifier: connection.connection_identifier)
175
- ci = ActionCable.server.connections.map(&:connection_identifier)
176
- WebsocketConnection.all.each do |wsc|
177
- wsc.destroy unless ci.include?(wsc.connection_identifier)
178
- end
179
- end
180
-
181
- def subscribe_to_query(data)
182
- websocket_connection.subscribed_queries.find_or_create_by(query: data['query'])
183
- SubscriptionsChannel.broadcast_to(
184
- websocket_connection,
185
- query: data['query'],
186
- result: #{@app_name.camelize}Schema.execute(data['query'], context: { current_user: websocket_connection.user })
187
- )
188
- end
189
-
190
- def unsubscribe_to_query(data)
191
- websocket_connection.subscribed_queries.find_by(query: data['query'])&.destroy
192
- end
193
-
194
- def unsubscribed
195
- websocket_connection.destroy
196
- ci = ActionCable.server.connections.map(&:connection_identifier)
197
- WebsocketConnection.all.each do |wsc|
198
- wsc.destroy unless ci.include?(wsc.connection_identifier)
199
- end
200
- end
201
-
202
- end
203
-
204
- STRING
205
- )
206
- end
207
-
208
122
  def write_controller
209
123
  File.write(
210
124
  'app/controllers/graphql_controller.rb',
@@ -219,7 +133,6 @@ module GraphqlRailsApi
219
133
  context: { current_user: authenticated_user },
220
134
  operation_name: params[:operationName]
221
135
  )
222
- ApplicationRecord.broadcast_queries
223
136
  render json: result
224
137
  end
225
138
 
@@ -1,20 +1,24 @@
1
- require 'deep_pluck'
2
- require 'rkelly'
1
+ require "deep_pluck"
2
+ require "rkelly"
3
3
 
4
4
  module Graphql
5
5
  class HydrateQuery
6
-
7
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
- @models = [model_name.singularize.camelize]
13
11
  @check_visibility = check_visibility
12
+
13
+ if id.present? && !valid_id?(id)
14
+ raise GraphQL::ExecutionError, "Invalid id: #{id}"
15
+ end
16
+
14
17
  @id = id
15
18
  @user = user
16
- @page = page || 1
17
- @per_page = per_page || 1000
19
+ @page = page&.to_i || 1
20
+ @per_page = per_page&.to_i || 1000
21
+ @per_page = 1000 if @per_page > 1000
18
22
  end
19
23
 
20
24
  def run
@@ -24,21 +28,24 @@ module Graphql
24
28
  else
25
29
  @model = @model.limit(@per_page)
26
30
  @model = @model.offset(@per_page * (@page - 1))
27
- filter_and_order
31
+
32
+ transform_filter if @filter.present?
33
+ transform_order if @order_by.present?
34
+
28
35
  deep_pluck_to_structs(@context&.irep_node)
29
36
  end
30
37
  end
31
38
 
32
39
  def paginated_run
33
- filter_and_order
40
+ transform_filter if @filter.present?
41
+ transform_order if @order_by.present?
34
42
 
35
- @total = @model.length
43
+ @total = @model.count
36
44
  @model = @model.limit(@per_page)
37
45
  @model = @model.offset(@per_page * (@page - 1))
38
46
 
39
- ::Rails.logger.info(@model.to_sql)
40
47
  OpenStruct.new(
41
- data: deep_pluck_to_structs(@context&.irep_node&.typed_children&.values&.first.try(:[], 'data')),
48
+ data: deep_pluck_to_structs(@context&.irep_node&.typed_children&.values&.first.try(:[], "data")),
42
49
  total_count: @total,
43
50
  per_page: @per_page,
44
51
  page: @page
@@ -47,58 +54,251 @@ module Graphql
47
54
 
48
55
  private
49
56
 
50
- def filter_and_order
51
- if @filter
52
- transformed_filter = transform_filter(@filter)
53
- to_join = transformed_filter.split(/AND|OR|like|ilike/).map do |expression|
54
- expression.strip.split(/!=|=|IS/).first.strip
55
- end.select { |e| e.include?('.') }.map { |e| e.split('.').first }.map(&:to_sym)
56
- to_join.reject { |j| j.to_s.pluralize.to_sym == @model.klass.to_s.pluralize.underscore.to_sym }.each do |j|
57
- @model = @model.left_joins(j).distinct
57
+ def transform_order
58
+ return if @order_by.blank?
59
+
60
+ @order_by.split(",").each do |order|
61
+
62
+ sign = order.split(" ").last.downcase == "desc" ? "desc" : "asc"
63
+ column = order.split(" ").first.strip
64
+
65
+ if column.include?(".")
66
+ associated_model = column.split(".").first
67
+ accessor = column.split(".").last
68
+ assoc = get_assoc!(@model, associated_model)
69
+ field_type = get_field_type!(assoc.klass, accessor)
70
+ @model = @model.left_joins(associated_model.to_sym)
71
+ ordered_field = "#{associated_model.pluralize}.#{accessor}"
72
+ else
73
+ field_type = get_field_type!(@model, column)
74
+ ordered_field = "#{model_name.pluralize}.#{column}"
58
75
  end
59
- transformed_filter = transformed_filter.split(/(AND|OR|like|ilike)/).map do |e|
60
- arr = e.split(/(!=|=|IS)/)
61
- if arr.first.include?('.')
62
- arr.first.split('.').first.pluralize + '.' + arr.first.split('.').last + arr[1].to_s + arr[2].to_s
76
+
77
+ @model = if %i[string text].include?(field_type)
78
+ @model.order(Arel.sql("upper(#{ordered_field}) #{sign}"))
79
+ else
80
+ @model.order(Arel.sql("#{ordered_field} #{sign}"))
81
+ end
82
+ end
83
+ end
84
+
85
+ def transform_filter
86
+ return if @filter.blank?
87
+
88
+ ast = RKelly::Parser.new.parse(@filter)
89
+ exprs = ast.value
90
+ if exprs.count != 1
91
+ raise GraphQL::ExecutionError, "Invalid filter: #{@filter}, only one expression allowed"
92
+ end
93
+
94
+ @model = handle_node(exprs.first.value, @model)
95
+
96
+ if @need_distinct_results
97
+ @model = @model.distinct
98
+ end
99
+ rescue RKelly::SyntaxError => e
100
+ raise GraphQL::ExecutionError, "Invalid filter: #{e.message}"
101
+ end
102
+
103
+ def handle_node(node, model)
104
+ if node.instance_of?(RKelly::Nodes::ParentheticalNode)
105
+ handle_ParentheticalNode(node, model)
106
+ elsif node.instance_of?(RKelly::Nodes::LogicalAndNode)
107
+ handle_LogicalAndNode(node, model)
108
+ elsif node.instance_of?(RKelly::Nodes::LogicalOrNode)
109
+ handle_LogicalOrNode(node, model)
110
+ elsif node.instance_of?(RKelly::Nodes::NotEqualNode)
111
+ handle_NotEqualNode(node, model)
112
+ elsif node.instance_of?(RKelly::Nodes::EqualNode)
113
+ handle_EqualNode(node, model)
114
+ elsif node.instance_of?(RKelly::Nodes::StrictEqualNode)
115
+ handle_StrictEqualNode(node, model)
116
+ elsif node.instance_of?(RKelly::Nodes::NotStrictEqualNode)
117
+ handle_NotStrictEqualNode(node, model)
118
+ elsif node.instance_of?(RKelly::Nodes::GreaterOrEqualNode)
119
+ handle_GreaterOrEqualNode(node, model)
120
+ elsif node.instance_of?(RKelly::Nodes::LessOrEqualNode)
121
+ handle_LessOrEqualNode(node, model)
122
+ elsif node.instance_of?(RKelly::Nodes::LessNode)
123
+ handle_LessNode(node, model)
124
+ elsif node.instance_of?(RKelly::Nodes::GreaterNode)
125
+ handle_GreaterNode(node, model)
126
+ else
127
+ raise GraphQL::ExecutionError, "Invalid filter: #{node.class} unknown operator"
128
+ end
129
+ end
130
+
131
+ def handle_ParentheticalNode(node, model)
132
+ handle_node(node.value, model)
133
+ end
134
+
135
+ def handle_LogicalAndNode(node, model)
136
+ handle_node(node.left, model).and(handle_node(node.value, model))
137
+ end
138
+
139
+ def handle_LogicalOrNode(node, model)
140
+ handle_node(node.left, model).or(handle_node(node.value, model))
141
+ end
142
+
143
+ def handle_dot_accessor_node(node, model)
144
+ associated_model = node.left.value.value
145
+ accessor = node.left.accessor
146
+ assoc = get_assoc!(model, associated_model)
147
+ field_type = get_field_type!(assoc.klass, accessor)
148
+
149
+ if assoc.association_class == ActiveRecord::Associations::HasManyAssociation
150
+ @need_distinct_results = true
151
+ end
152
+
153
+ model = model.left_joins(associated_model.to_sym)
154
+ # field = "#{associated_model.pluralize}.#{accessor}"
155
+ value = value_from_node(node.value, field_type, accessor.to_sym, model)
156
+ [assoc.klass.arel_table[accessor], model, field_type, value]
157
+ end
158
+
159
+ def handle_resolve_node(node, model)
160
+ field = node.left.value
161
+ field_type = get_field_type!(model, field)
162
+ value = value_from_node(node.value, field_type, field.to_sym, model)
163
+ [model.klass.arel_table[field], model, field_type, value]
164
+ end
165
+
166
+ def handle_operator_node(node, model)
167
+ if node.left.instance_of?(RKelly::Nodes::DotAccessorNode)
168
+ handle_dot_accessor_node(node, model)
169
+ elsif node.left.instance_of?(RKelly::Nodes::ResolveNode)
170
+ handle_resolve_node(node, model)
171
+ else
172
+ raise GraphQL::ExecutionError, "Invalid left value: #{node.left.class}"
173
+ end
174
+ end
175
+
176
+ def value_from_node(node, sym_type, sym, model)
177
+ if node.instance_of?(RKelly::Nodes::StringNode)
178
+ val = node.value.gsub(/^'|'$|^"|"$/, "")
179
+ if sym_type == :datetime
180
+ DateTime.parse(val)
181
+ elsif sym_type == :date
182
+ Date.parse(val)
183
+ elsif sym_type == :integer
184
+ # Enums are handled here : We are about to compare a string with an integer column
185
+ # If the symbol and the value correspond to an existing enum into the model
186
+ if model.klass.defined_enums[sym.to_s]&.keys&.include?(val)
187
+ # return the corresponding enum value
188
+ model.klass.defined_enums[sym.to_s][val]
63
189
  else
64
- arr.join
190
+ raise GraphQL::ExecutionError, "Invalid value: #{val}, compare a string with an integer column #{sym}"
65
191
  end
66
- end.join
67
- @model = @model.where(transformed_filter)
192
+ else
193
+ val
194
+ end
195
+ elsif node.instance_of?(RKelly::Nodes::NumberNode)
196
+ node.value
197
+ elsif node.instance_of?(RKelly::Nodes::TrueNode)
198
+ true
199
+ elsif node.instance_of?(RKelly::Nodes::FalseNode)
200
+ false
201
+ elsif node.instance_of?(RKelly::Nodes::NullNode)
202
+ nil
203
+ else
204
+ raise GraphQL::ExecutionError, "Invalid filter: #{node} unknown rvalue node"
68
205
  end
206
+ end
69
207
 
70
- return unless @order_by
208
+ def sanitize_sql_like(value)
209
+ ActiveRecord::Base.sanitize_sql_like(value)
210
+ end
71
211
 
72
- sign = @order_by.split(' ').last.downcase == 'desc' ? 'desc' : 'asc'
73
- column = @order_by.split(' ').first
74
- if column.include?('.')
75
- @model = @model.left_joins(column.split('.').first.to_sym)
76
- string_type = %i[string text].include?(
77
- evaluate_model(@model, column.split('.').first).columns_hash[column.split('.').last]&.type
78
- )
212
+ def handle_NotEqualNode(node, model)
213
+ arel_field, model, type, value = handle_operator_node(node, model)
79
214
 
80
- @to_select_to_add = if string_type
81
- "upper(#{column.split('.').first.pluralize}.#{column.split('.').last})"
82
- else
83
- column.split('.').first.pluralize + '.' + column.split('.').last
84
- end
85
- @model = @model.select(@to_select_to_add)
86
- column = "#{column.split('.').first.pluralize}.#{column.split('.').last}"
87
- @model = @model.order(Arel.sql("#{string_type ? "upper(#{column})" : column} #{sign}"))
88
- elsif @order_by
89
- column = "upper(#{model_name}.#{column})" if %i[string text].include?(@model.columns_hash[column]&.type)
90
- @model = @model.order("#{column} #{sign}")
215
+ if value.nil?
216
+ model.where.not(arel_field.eq(nil))
217
+ elsif type == :text || type == :string
218
+ model.where.not(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
219
+ else
220
+ model.where.not(arel_field.eq(value))
91
221
  end
92
222
  end
93
223
 
94
- def transform_filter(filter)
95
- parsed_filter = RKelly::Parser.new.parse(filter.gsub('like', ' | '))&.to_ecma
96
- return '' unless parsed_filter
224
+ def handle_NotStrictEqualNode(node, model)
225
+ arel_field, model, type, value = handle_operator_node(node, model)
97
226
 
98
- @model.klass.defined_enums.values.reduce(:merge)&.each { |k, v| parsed_filter.gsub!("= #{k}", "= #{v}") }
99
- parsed_filter.gsub(' | ', ' ilike ').gsub('||', 'OR').gsub('&&', 'AND').gsub('===', '=').gsub('==', '=').gsub(
100
- '!= null', 'IS NOT NULL'
101
- ).gsub('= null', 'IS NULL').delete(';')
227
+ if value.nil?
228
+ model.where.not(arel_field.eq(nil))
229
+ elsif type == :text || type == :string
230
+ model.where.not(arel_field.matches(sanitize_sql_like(value), false, true))
231
+ else
232
+ model.where.not(arel_field.eq(value))
233
+ end
234
+ end
235
+
236
+ def handle_EqualNode(node, model)
237
+ arel_field, model, type, value = handle_operator_node(node, model)
238
+
239
+ if value.nil?
240
+ model.where(arel_field.eq(nil))
241
+ elsif type == :text || type == :string
242
+ model.where(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
243
+ else
244
+ model.where(arel_field.eq(value))
245
+ end
246
+ end
247
+
248
+ def handle_StrictEqualNode(node, model)
249
+ arel_field, model, type, value = handle_operator_node(node, model)
250
+
251
+ if value.nil?
252
+ model.where(arel_field.eq(nil))
253
+ elsif type == :text || type == :string
254
+ model.where(arel_field.matches(sanitize_sql_like(value), false, true))
255
+ else
256
+ model.where(arel_field.eq(value))
257
+ end
258
+ end
259
+
260
+ def handle_GreaterOrEqualNode(node, model)
261
+ arel_field, model, type, value = handle_operator_node(node, model)
262
+ model.where(arel_field.gteq(value))
263
+ end
264
+
265
+ def handle_LessOrEqualNode(node, model)
266
+ arel_field, model, type, value = handle_operator_node(node, model)
267
+ model.where(arel_field.lteq(value))
268
+ end
269
+
270
+ def handle_LessNode(node, model)
271
+ arel_field, model, type, value = handle_operator_node(node, model)
272
+ model.where(arel_field.lt(value))
273
+ end
274
+
275
+ def handle_GreaterNode(node, model)
276
+ arel_field, model, type, value = handle_operator_node(node, model)
277
+ model.where(arel_field.gt(value))
278
+ end
279
+
280
+ def valid_id?(id)
281
+ valid_uuid?(id) || id.is_a?(Integer)
282
+ end
283
+
284
+ def valid_uuid?(id)
285
+ 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/)
286
+ end
287
+
288
+ def get_assoc!(model, assoc_name)
289
+ assoc = model.reflect_on_association(assoc_name)
290
+ unless assoc.present?
291
+ raise GraphQL::ExecutionError, "Invalid filter: #{assoc_name} is not an association of #{model}"
292
+ end
293
+ assoc
294
+ end
295
+
296
+ def get_field_type!(model, field_name)
297
+ field = model.column_for_attribute(field_name.to_sym)
298
+ unless field.present?
299
+ raise GraphQL::ExecutionError, "Invalid filter: #{field_name} is not a field of #{model}"
300
+ end
301
+ field.type
102
302
  end
103
303
 
104
304
  def deep_pluck_to_structs(irep_node)
@@ -129,7 +329,7 @@ module Graphql
129
329
  def hash_to_array_of_hashes(hash, parent_class)
130
330
  return if parent_class.nil? || hash.nil?
131
331
 
132
- hash['id'] = nil if hash['id'].blank?
332
+ hash["id"] = nil if hash["id"].blank?
133
333
  fetch_ids_from_relation(hash)
134
334
 
135
335
  hash.each_with_object([]) do |(k, v), arr|
@@ -137,18 +337,17 @@ module Graphql
137
337
  next arr << v if parent_class.new.attributes.key?(v)
138
338
 
139
339
  klass = evaluate_model(parent_class, k)
140
- @models << klass.to_s unless @models.include?(klass.to_s)
141
- arr << { k.to_sym => hash_to_array_of_hashes(v, klass) } if klass.present? && v.present?
340
+ arr << {k.to_sym => hash_to_array_of_hashes(v, klass)} if klass.present? && v.present?
142
341
  end
143
342
  end
144
343
 
145
344
  def fetch_ids_from_relation(hash)
146
- hash.select { |k, _| k.ends_with?('_ids') }.each do |(k, _)|
147
- collection_name = k.gsub('_ids', '').pluralize
345
+ hash.select { |k, _| k.ends_with?("_ids") }.each do |(k, _)|
346
+ collection_name = k.gsub("_ids", "").pluralize
148
347
  if hash[collection_name].blank?
149
- hash[collection_name] = { 'id' => nil }
348
+ hash[collection_name] = {"id" => nil}
150
349
  else
151
- hash[collection_name]['id'] = nil
350
+ hash[collection_name]["id"] = nil
152
351
  end
153
352
  end
154
353
  end
@@ -177,7 +376,7 @@ module Graphql
177
376
 
178
377
  def parse_fields(irep_node)
179
378
  fields = irep_node&.scoped_children&.values&.first
180
- fields = fields['edges'].scoped_children.values.first['node']&.scoped_children&.values&.first if fields&.key?('edges')
379
+ fields = fields["edges"].scoped_children.values.first["node"]&.scoped_children&.values&.first if fields&.key?("edges")
181
380
  return if fields.blank?
182
381
 
183
382
  fields.each_with_object({}) do |(k, v), h|
@@ -186,8 +385,7 @@ module Graphql
186
385
  end
187
386
 
188
387
  def model_name
189
- @model.class.to_s.split('::').first.underscore.pluralize
388
+ @model.class.to_s.split("::").first.underscore.pluralize
190
389
  end
191
-
192
390
  end
193
391
  end
@@ -6,23 +6,22 @@ module Graphql
6
6
  include Singleton
7
7
 
8
8
  def self.query_resources
9
- Dir.glob("#{File.expand_path('.')}/app/graphql/*/type.rb").map do |dir|
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("#{File.expand_path('.')}/app/graphql/*/mutations/*.rb").reject do |e|
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("#{File.expand_path('.')}/app/graphql/*/mutations/#{meth}.rb").map do |dir|
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
@@ -1,7 +1,7 @@
1
1
  module Graphql
2
2
  module Rails
3
3
  module Api
4
- VERSION = '0.9.1'.freeze
4
+ VERSION = "0.9.3-fix".freeze
5
5
  end
6
6
  end
7
7
  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.9.1
4
+ version: 0.9.3.pre.fix
5
5
  platform: ruby
6
6
  authors:
7
7
  - poilon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-30 00:00:00.000000000 Z
11
+ date: 2023-04-14 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.7'
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.7'
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: 6.1.4
48
- - - "~>"
49
- - !ruby/object:Gem::Version
50
- version: 7.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: 6.1.4
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: 7.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
@@ -110,11 +110,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
110
  version: '0'
111
111
  required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  requirements:
113
- - - ">="
113
+ - - ">"
114
114
  - !ruby/object:Gem::Version
115
- version: '0'
115
+ version: 1.3.1
116
116
  requirements: []
117
- rubygems_version: 3.0.8
117
+ rubygems_version: 3.1.2
118
118
  signing_key:
119
119
  specification_version: 4
120
120
  summary: Graphql rails api framework to create easily graphql api with rails