graphql-rails-api 0.9.0 → 0.9.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c789b39c99efe639f6a7d035d012d28b4d6cff882c4bcfb7c1145ec03ffa7d9
4
- data.tar.gz: 27c85cd1193f34216fe52d9dde5f05e0a5ca87bec25efb24ffc9a72c0be447c0
3
+ metadata.gz: dfe867c4f64d230748b3d8a9d1aece95a16c6692d96491fd67298ad522ddd5ce
4
+ data.tar.gz: 0a407e15eff08e02e8a202da0d47d3fe6116af42eb591421e3b5a914964f7a48
5
5
  SHA512:
6
- metadata.gz: 8c73593751c6aff011ae081788c1072fbeca7a8b86c0dd736c00c635f4956db56a178dfe3518a6c39a5a7c286cb939675fc67862cfa36b09617a396998e086a9
7
- data.tar.gz: '069beded1ed21d5c0392e4daf48ceae733a6e95f8f022eb5341fd0541e89a5849a8acd3db0f8b59355c08d9ec3479f4860ddb96071fdd95acdd66bc2b8bd110a'
6
+ metadata.gz: e508676774b519da77b95483e1a0d28a91e1843925dc1669f674cb958d911ada161968923a385d86af51b86d47af56baecff3fe5a959cb1b727aed556834df6c
7
+ data.tar.gz: 3e1aefb503057898ba28eb3008fd2e8b4c63f792fd530ac0a3bcfdf8754ed168a5b5f7f6c1dbaf76f96961494bd03da7f905fc57ba19ccaabd26af1eb8dddffc
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,40 +1,310 @@
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
- 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
- @models = [model_name.singularize.camelize]
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
- @model = @model.where(transform_filter(@filter)) if @filter
21
- @model = @model.order(@order_by) if @order_by
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.count
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
+ @model = if %i[string text].include?(field_type)
76
+ @model.order(Arel.sql("upper(#{ordered_field}) #{sign}"))
77
+ else
78
+ @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
+ rescue RKelly::SyntaxError => e
97
+ raise GraphQL::ExecutionError, "Invalid filter: #{e.message}"
98
+ end
99
+
100
+ def handle_node(node, model)
101
+ if node.instance_of?(RKelly::Nodes::ParentheticalNode)
102
+ handle_ParentheticalNode(node, model)
103
+ elsif node.instance_of?(RKelly::Nodes::LogicalAndNode)
104
+ handle_LogicalAndNode(node, model)
105
+ elsif node.instance_of?(RKelly::Nodes::LogicalOrNode)
106
+ handle_LogicalOrNode(node, model)
107
+ elsif node.instance_of?(RKelly::Nodes::NotEqualNode)
108
+ handle_NotEqualNode(node, model)
109
+ elsif node.instance_of?(RKelly::Nodes::EqualNode)
110
+ handle_EqualNode(node, model)
111
+ elsif node.instance_of?(RKelly::Nodes::StrictEqualNode)
112
+ handle_StrictEqualNode(node, model)
113
+ elsif node.instance_of?(RKelly::Nodes::NotStrictEqualNode)
114
+ handle_NotStrictEqualNode(node, model)
115
+ elsif node.instance_of?(RKelly::Nodes::GreaterOrEqualNode)
116
+ handle_GreaterOrEqualNode(node, model)
117
+ elsif node.instance_of?(RKelly::Nodes::LessOrEqualNode)
118
+ handle_LessOrEqualNode(node, model)
119
+ elsif node.instance_of?(RKelly::Nodes::LessNode)
120
+ handle_LessNode(node, model)
121
+ elsif node.instance_of?(RKelly::Nodes::GreaterNode)
122
+ handle_GreaterNode(node, model)
123
+ else
124
+ raise GraphQL::ExecutionError, "Invalid filter: #{node.class} unknown operator"
125
+ end
126
+ end
127
+
128
+ def handle_ParentheticalNode(node, model)
129
+ handle_node(node.value, model)
130
+ end
131
+
132
+ def handle_LogicalAndNode(node, model)
133
+ handle_node(node.left, model).and(handle_node(node.value, model))
134
+ end
135
+
136
+ def handle_LogicalOrNode(node, model)
137
+ handle_node(node.left, model).or(handle_node(node.value, model))
138
+ end
22
139
 
23
- @model = @model.limit(@per_page) if @per_page
24
- @model = @model.offset(@per_page * (@page - 1)) if @page
140
+ def handle_dot_accessor_node(node, model)
141
+ associated_model = node.left.value.value
142
+ accessor = node.left.accessor
143
+ assoc = get_assoc!(model, associated_model)
144
+ field_type = get_field_type!(assoc.klass, accessor)
25
145
 
26
- @model = @model.where(id: @id) if @id
27
- plucked = DeepPluck::Model.new(@model.visible_for(user: @user), user: @user).add(
28
- hash_to_array_of_hashes(parse_fields(@context&.irep_node), @model)
29
- ).load_all
30
- result = plucked_attr_to_structs(plucked, model_name.singularize.camelize.constantize)&.compact
31
- @id ? result.first : result
146
+ if assoc.association_class == ActiveRecord::Associations::HasManyAssociation
147
+ @need_distinct_results = true
148
+ end
149
+
150
+ model = model.left_joins(associated_model.to_sym)
151
+ # field = "#{associated_model.pluralize}.#{accessor}"
152
+ value = value_from_node(node.value, field_type, accessor.to_sym, model)
153
+ [assoc.klass.arel_table[accessor], model, field_type, value]
32
154
  end
33
155
 
34
- def transform_filter(filter)
35
- parsed_filter = RKelly::Parser.new.parse(filter.gsub('like', ' | ')).to_ecma
36
- parsed_filter.gsub(' | ', ' like ').
37
- gsub('||', 'OR').gsub('&&', 'AND').gsub('===', '=').gsub('==', '=').delete(';')
156
+ def handle_resolve_node(node, model)
157
+ field = node.left.value
158
+ field_type = get_field_type!(model, field)
159
+ value = value_from_node(node.value, field_type, field.to_sym, model)
160
+ [model.klass.arel_table[field], model, field_type, value]
161
+ end
162
+
163
+ def handle_operator_node(node, model)
164
+ if node.left.instance_of?(RKelly::Nodes::DotAccessorNode)
165
+ handle_dot_accessor_node(node, model)
166
+ elsif node.left.instance_of?(RKelly::Nodes::ResolveNode)
167
+ handle_resolve_node(node, model)
168
+ else
169
+ raise GraphQL::ExecutionError, "Invalid left value: #{node.left.class}"
170
+ end
171
+ end
172
+
173
+ def value_from_node(node, sym_type, sym, model)
174
+ if node.instance_of?(RKelly::Nodes::StringNode)
175
+ val = node.value.gsub(/^'|'$|^"|"$/, "")
176
+ if sym_type == :datetime
177
+ DateTime.parse(val)
178
+ elsif sym_type == :date
179
+ Date.parse(val)
180
+ elsif sym_type == :integer
181
+ # Enums are handled here : We are about to compare a string with an integer column
182
+ # If the symbol and the value correspond to an existing enum into the model
183
+ if model.klass.defined_enums[sym.to_s]&.keys&.include?(val)
184
+ # return the corresponding enum value
185
+ model.klass.defined_enums[sym.to_s][val]
186
+ else
187
+ raise GraphQL::ExecutionError, "Invalid value: #{val}, compare a string with an integer column #{sym}"
188
+ end
189
+ else
190
+ val
191
+ end
192
+ elsif node.instance_of?(RKelly::Nodes::NumberNode)
193
+ node.value
194
+ elsif node.instance_of?(RKelly::Nodes::TrueNode)
195
+ true
196
+ elsif node.instance_of?(RKelly::Nodes::FalseNode)
197
+ false
198
+ elsif node.instance_of?(RKelly::Nodes::NullNode)
199
+ nil
200
+ else
201
+ raise GraphQL::ExecutionError, "Invalid filter: #{node} unknown rvalue node"
202
+ end
203
+ end
204
+
205
+ def sanitize_sql_like(value)
206
+ ActiveRecord::Base.sanitize_sql_like(value)
207
+ end
208
+
209
+ def handle_NotEqualNode(node, model)
210
+ arel_field, model, type, value = handle_operator_node(node, model)
211
+
212
+ if value.nil?
213
+ model.where.not(arel_field.eq(nil))
214
+ elsif type == :text || type == :string
215
+ model.where.not(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
216
+ else
217
+ model.where.not(arel_field.eq(value))
218
+ end
219
+ end
220
+
221
+ def handle_NotStrictEqualNode(node, model)
222
+ arel_field, model, type, value = handle_operator_node(node, model)
223
+
224
+ if value.nil?
225
+ model.where.not(arel_field.eq(nil))
226
+ elsif type == :text || type == :string
227
+ model.where.not(arel_field.matches(sanitize_sql_like(value), false, true))
228
+ else
229
+ model.where.not(arel_field.eq(value))
230
+ end
231
+ end
232
+
233
+ def handle_EqualNode(node, model)
234
+ arel_field, model, type, value = handle_operator_node(node, model)
235
+
236
+ if value.nil?
237
+ model.where(arel_field.eq(nil))
238
+ elsif type == :text || type == :string
239
+ model.where(arel_field.lower.matches(sanitize_sql_like(value.downcase)))
240
+ else
241
+ model.where(arel_field.eq(value))
242
+ end
243
+ end
244
+
245
+ def handle_StrictEqualNode(node, model)
246
+ arel_field, model, type, value = handle_operator_node(node, model)
247
+
248
+ if value.nil?
249
+ model.where(arel_field.eq(nil))
250
+ elsif type == :text || type == :string
251
+ model.where(arel_field.matches(sanitize_sql_like(value), false, true))
252
+ else
253
+ model.where(arel_field.eq(value))
254
+ end
255
+ end
256
+
257
+ def handle_GreaterOrEqualNode(node, model)
258
+ arel_field, model, type, value = handle_operator_node(node, model)
259
+ model.where(arel_field.gteq(value))
260
+ end
261
+
262
+ def handle_LessOrEqualNode(node, model)
263
+ arel_field, model, type, value = handle_operator_node(node, model)
264
+ model.where(arel_field.lteq(value))
265
+ end
266
+
267
+ def handle_LessNode(node, model)
268
+ arel_field, model, type, value = handle_operator_node(node, model)
269
+ model.where(arel_field.lt(value))
270
+ end
271
+
272
+ def handle_GreaterNode(node, model)
273
+ arel_field, model, type, value = handle_operator_node(node, model)
274
+ model.where(arel_field.gt(value))
275
+ end
276
+
277
+ def valid_id?(id)
278
+ valid_uuid?(id) || id.is_a?(Integer)
279
+ end
280
+
281
+ def valid_uuid?(id)
282
+ 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/)
283
+ end
284
+
285
+ def get_assoc!(model, assoc_name)
286
+ assoc = model.reflect_on_association(assoc_name)
287
+ unless assoc.present?
288
+ raise GraphQL::ExecutionError, "Invalid filter: #{assoc_name} is not an association of #{model}"
289
+ end
290
+ assoc
291
+ end
292
+
293
+ def get_field_type!(model, field_name)
294
+ field = model.column_for_attribute(field_name.to_sym)
295
+ unless field.present?
296
+ raise GraphQL::ExecutionError, "Invalid filter: #{field_name} is not a field of #{model}"
297
+ end
298
+ field.type
299
+ end
300
+
301
+ def deep_pluck_to_structs(irep_node)
302
+ plucked_attr_to_structs(
303
+ DeepPluck::Model.new(@model.visible_for(user: @user), user: @user).add(
304
+ ((hash_to_array_of_hashes(parse_fields(irep_node), @model) || []) + [@to_select_to_add]).compact
305
+ ).load_all,
306
+ model_name.singularize.camelize.constantize
307
+ )&.compact
38
308
  end
39
309
 
40
310
  def plucked_attr_to_structs(arr, parent_model)
@@ -56,7 +326,7 @@ module Graphql
56
326
  def hash_to_array_of_hashes(hash, parent_class)
57
327
  return if parent_class.nil? || hash.nil?
58
328
 
59
- hash['id'] = nil if hash['id'].blank?
329
+ hash["id"] = nil if hash["id"].blank?
60
330
  fetch_ids_from_relation(hash)
61
331
 
62
332
  hash.each_with_object([]) do |(k, v), arr|
@@ -64,18 +334,17 @@ module Graphql
64
334
  next arr << v if parent_class.new.attributes.key?(v)
65
335
 
66
336
  klass = evaluate_model(parent_class, k)
67
- @models << klass.to_s unless @models.include?(klass.to_s)
68
- arr << { k.to_sym => hash_to_array_of_hashes(v, klass) } if klass.present? && v.present?
337
+ arr << {k.to_sym => hash_to_array_of_hashes(v, klass)} if klass.present? && v.present?
69
338
  end
70
339
  end
71
340
 
72
341
  def fetch_ids_from_relation(hash)
73
- hash.select { |k, _| k.ends_with?('_ids') }.each do |(k, _)|
74
- collection_name = k.gsub('_ids', '').pluralize
342
+ hash.select { |k, _| k.ends_with?("_ids") }.each do |(k, _)|
343
+ collection_name = k.gsub("_ids", "").pluralize
75
344
  if hash[collection_name].blank?
76
- hash[collection_name] = { 'id' => nil }
345
+ hash[collection_name] = {"id" => nil}
77
346
  else
78
- hash[collection_name]['id'] = nil
347
+ hash[collection_name]["id"] = nil
79
348
  end
80
349
  end
81
350
  end
@@ -104,9 +373,7 @@ module Graphql
104
373
 
105
374
  def parse_fields(irep_node)
106
375
  fields = irep_node&.scoped_children&.values&.first
107
- if fields.key?('edges')
108
- fields = fields['edges'].scoped_children.values.first['node']&.scoped_children&.values&.first
109
- end
376
+ fields = fields["edges"].scoped_children.values.first["node"]&.scoped_children&.values&.first if fields&.key?("edges")
110
377
  return if fields.blank?
111
378
 
112
379
  fields.each_with_object({}) do |(k, v), h|
@@ -115,8 +382,7 @@ module Graphql
115
382
  end
116
383
 
117
384
  def model_name
118
- @model.class.to_s.split('::').first.underscore.pluralize
385
+ @model.class.to_s.split("::").first.underscore.pluralize
119
386
  end
120
-
121
387
  end
122
388
  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.0'.freeze
4
+ VERSION = "0.9.3".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.0
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - poilon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-12 00:00:00.000000000 Z
11
+ date: 2022-09-06 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
@@ -42,22 +48,16 @@ dependencies:
42
48
  name: rails
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 7.0.0
48
51
  - - ">="
49
52
  - !ruby/object:Gem::Version
50
- version: 6.1.4
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
- - - "~>"
56
- - !ruby/object:Gem::Version
57
- version: 7.0.0
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 6.1.4
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.1.2
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