apiql 0.4.8 → 1.0.5

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: f23382bfe0c9946640a70754b0111d109267e24a00b23e675d16f2d5f5685938
4
- data.tar.gz: d0fa0b8295cc1184b408e9817f7ba052b881abbefdab93533000af4d3fa1ea3c
3
+ metadata.gz: 50c3aec2e1f759145b3e6bf73e6bb9e810793360bf9c08aaec98095a83fa42c4
4
+ data.tar.gz: 331b529d03c24fc189ab17f1b9d9d7144a53c21b22812434ed116a602b927c6f
5
5
  SHA512:
6
- metadata.gz: e795fba519ff01d1b7e21ed99f1e8f58619600249a5cd03b57b3b8acba407799c43fd2170dfb0d45d7b1ce6e18754caf23bd6c44ef28579dc571b2d644b52c17
7
- data.tar.gz: a39ea52e53d9a8299fbaa0ae359ed25fa77112c0405279b1fa0cdbd089483d81894bf132466d197c32cee6c08415207a90c3d7763bec228258cfce9ff681db19
6
+ metadata.gz: 2249d7fb31711753c4db8dcd9ae711ba490bf587e99529f078e233b96de7f4827e3d01f920dc069382c172b2d98eed362a4f82b24a537d1776499445cbac0171
7
+ data.tar.gz: 332296fa978d1fa1e68dfe3e3e852f5e91cd3da8ed8dc344e368454572273a6a6988d4197d1454b1dbb51199f220158b623ef2f2049fb737740b3cb8d7fe88f0
data/README.md CHANGED
@@ -6,37 +6,12 @@ It compiles requests into Hashes for faster rendering.
6
6
 
7
7
  It automatically detects nested entities and eager-loads them for faster DB access!
8
8
 
9
- Define your responder (requested methods):
10
-
11
- ```ruby
12
- class UserAPIQL < ::APIQL
13
- def me
14
- authorize! :show, ::User
15
-
16
- current_user
17
- end
18
-
19
- def authenticate(email, password)
20
- user = ::User.find_by(email)
21
- user.authenticate(password)
22
-
23
- user
24
- end
25
-
26
- def logout
27
- current_user&.logout
28
-
29
- :ok
30
- end
31
- end
32
-
33
- ```
34
9
  In controller or Grape API endpoint, handler of POST /apiql method (or any other, see below APIQL.endpoint):
35
10
 
36
11
  ```ruby
37
12
  def apiql
38
13
  schema = APIQL.cache(params)
39
- UserAPIQL.new(self, :session, :current_user, :params).render(schema)
14
+ APIQL.new(self, :session, :current_user, :params).render(schema)
40
15
  end
41
16
 
42
17
  ```
@@ -46,12 +21,41 @@ Define presenters for your models:
46
21
 
47
22
  ```ruby
48
23
  class User < ApplicationRecord
24
+ after_commit { APIQL.drop_cache(self) }
25
+
49
26
  class Entity < ::APIQL::Entity
50
27
  attributes :full_name, :email, :token, :role, :roles # any attributes, methods or associations
51
28
 
52
29
  def token # if defined, method will be called instead of attribute
53
30
  object.token if object == current_user # directly access to current_user from context
54
31
  end
32
+
33
+ def roles
34
+ APIQL.cacheable([object, roles], :roles, expires_in: 3600) do
35
+ roles.pluck(:name).join(', ')
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def me
41
+ authorize! :show, ::User
42
+
43
+ current_user
44
+ end
45
+
46
+ def authenticate(email, password)
47
+ user = ::User.find_by(email)
48
+ user.authenticate(password)
49
+
50
+ user
51
+ end
52
+
53
+ def logout
54
+ current_user&.logout
55
+
56
+ :ok
57
+ end
58
+ end
55
59
  end
56
60
 
57
61
  has_many :roles
@@ -59,6 +63,14 @@ class User < ApplicationRecord
59
63
  ...
60
64
  end
61
65
 
66
+ class Role < ApplicationRecord
67
+ after_commit { APIQL.drop_cache(self) }
68
+
69
+ class Entity < ::APIQL::Entity
70
+ attributes :id, :name
71
+ end
72
+ end
73
+
62
74
  ```
63
75
  # JS:
64
76
 
@@ -74,13 +86,13 @@ APIQL.endpoint = "/apiql"
74
86
 
75
87
  authenticate(email, password) {
76
88
  apiql(`
77
- logout()
89
+ User.logout()
78
90
 
79
- authenticate(email, password) {
91
+ User.authenticate(email, password) {
80
92
  token
81
93
  }
82
94
 
83
- me {
95
+ User.me {
84
96
  email full_name role token
85
97
 
86
98
  roles {
@@ -98,7 +110,7 @@ authenticate(email, password) {
98
110
 
99
111
  logout() {
100
112
  apiql(`
101
- logout
113
+ User.logout
102
114
  `)
103
115
  .then(response => {
104
116
  })
@@ -110,11 +122,11 @@ you can call methods on entities:
110
122
 
111
123
  ```javascript
112
124
  apiql(`
113
- authenticate(email, password) {
125
+ User.authenticate(email, password) {
114
126
  token
115
127
  }
116
128
 
117
- user: me.reload {
129
+ user: User.me.reload {
118
130
  email full_name role token
119
131
 
120
132
  roles(filter) {
@@ -157,20 +169,69 @@ class APIQL
157
169
  end
158
170
  ```
159
171
 
160
- you can add CRUD methods for your models:
172
+ CRUD methods available for all models:
161
173
 
162
- ```ruby
163
- class UserAPIQL < ::APIQL
164
- model ::User
165
- model ::Role
166
- end
174
+ ```js
175
+ apiql(`
176
+ User.create(user)
177
+ `, {
178
+ user: {
179
+ email: this.email,
180
+ full_name: this.full_name
181
+ }
182
+ })
183
+ .then(response => {
184
+ ...
185
+ })
186
+
187
+ apiql(`
188
+ User.find(id) {
189
+ id email full_name
190
+ }
191
+ `, {
192
+ id: this.id
193
+ })
194
+ .then(response => {
195
+ ...
196
+ })
197
+
198
+ apiql(`
199
+ User.all {
200
+ id email full_name
201
+ }
202
+ `)
203
+ .then(response => {
204
+ ...
205
+ })
206
+
207
+ apiql(`
208
+ User.update(id, user)
209
+ `, {
210
+ id: this.id,
211
+ user: {
212
+ email: this.email,
213
+ full_name: this.full_name
214
+ }
215
+ })
216
+ .then(response => {
217
+ ...
218
+ })
219
+
220
+ apiql(`
221
+ User.destroy(id)
222
+ `, {
223
+ id: this.id
224
+ })
225
+ .then(response => {
226
+ ...
227
+ })
167
228
  ```
168
229
 
169
230
  or mount methods from external modules:
170
231
 
171
232
  ```ruby
172
- class UserAPIQL < ::APIQL
173
- mount ::Services::User # all methouds could be called with "user" prefix like "user.logout()"
233
+ class APIQL < ::APIQL
234
+ mount ::Services::User # all methods could be called with "user" prefix like "user.logout()"
174
235
  mount ::Services::Common, as: '' # all methods could be called without prefixes
175
236
  mount ::Services::Employer, as: 'dashboard" # all methods could be called with specified prefix
176
237
  end
data/apiql.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'apiql'
7
- spec.version = '0.4.8'
7
+ spec.version = '1.0.5'
8
8
  spec.authors = ['Dmitry Silchenko']
9
9
  spec.email = ['dmitry@desofto.com']
10
10
 
@@ -1,9 +1,11 @@
1
1
  const APIQL = {
2
2
  on_error: null,
3
3
  endpoint: '',
4
+ CSRFToken: '',
5
+ http: null,
4
6
 
5
7
  hash: function(s) {
6
- let hash = 0, i, chr
8
+ var hash = 0, i, chr
7
9
  if(s.length === 0) return hash
8
10
  for(i = 0; i < s.length; i++) {
9
11
  chr = s.charCodeAt(i)
@@ -13,6 +15,35 @@ const APIQL = {
13
15
  return hash
14
16
  },
15
17
 
18
+ post: function(endpoint, data) {
19
+ return new Promise(function(resolve, reject) {
20
+ var http = new XMLHttpRequest()
21
+ http.open("POST", endpoint, true)
22
+ if(APIQL.CSRFToken.length > 0) {
23
+ http.setRequestHeader("X-CSRF-Token", APIQL.CSRFToken)
24
+ }
25
+ http.setRequestHeader("Content-Type", "application/json;charset=UTF-8")
26
+ http.onload = function() {
27
+ APIQL.http = http
28
+
29
+ if(http.status >= 200 && http.status < 300) {
30
+ resolve({
31
+ status: http.status,
32
+ body: JSON.parse(http.responseText),
33
+ http: http
34
+ })
35
+ } else {
36
+ reject({
37
+ status: http.status,
38
+ body: JSON.parse(http.responseText),
39
+ http: http
40
+ })
41
+ }
42
+ }
43
+ http.send(JSON.stringify(data))
44
+ })
45
+ },
46
+
16
47
  call: function(schema, params, form) {
17
48
  if(!params) params = {}
18
49
  if(!form) form = null
@@ -35,11 +66,10 @@ const APIQL = {
35
66
  params.apiql = APIQL.hash(schema)
36
67
  }
37
68
 
38
- Vue.http.post(APIQL.endpoint, form || params)
69
+ APIQL.post(APIQL.endpoint, form || params)
39
70
  .then(function(response) {
40
71
  resolve(response.body)
41
- })
42
- .catch(function(response) {
72
+ }, function(response) {
43
73
  if(response.status == 401 && APIQL.on_error) {
44
74
  APIQL.on_error(response)
45
75
  return
@@ -53,11 +83,10 @@ const APIQL = {
53
83
  params.apiql_request = schema
54
84
  }
55
85
 
56
- Vue.http.post(APIQL.endpoint, form || params)
86
+ APIQL.post(APIQL.endpoint, form || params)
57
87
  .then(function(response) {
58
88
  resolve(response.body)
59
- })
60
- .catch(function(response) {
89
+ }, function(response) {
61
90
  if(APIQL.on_error) {
62
91
  APIQL.on_error(response)
63
92
  } else {
data/lib/apiql.rb CHANGED
@@ -2,7 +2,7 @@ class APIQL
2
2
  module Rails
3
3
  class Engine < ::Rails::Engine
4
4
  initializer 'apiql.assets' do |app|
5
- app.config.assets.paths << root.join('assets', 'javascripts').to_s
5
+ app.config.assets.paths << root.join('assets', 'javascripts').to_s if app.config.respond_to? :assets
6
6
  end
7
7
  end
8
8
  end
@@ -14,69 +14,6 @@ class APIQL
14
14
  end
15
15
  end
16
16
 
17
- module CRUD
18
- def model(klass)
19
- define_method "#{klass.name.pluralize.underscore}" do |page = nil, page_size = 10|
20
- authorize! :read, klass
21
-
22
- if page.present?
23
- {
24
- total: klass.count,
25
- items: klass.eager_load(eager_load).offset(page * page_size).limit(page_size)
26
- }
27
- else
28
- klass.eager_load(eager_load).all
29
- end
30
- end
31
-
32
- define_method "#{klass.name.underscore}" do |id|
33
- item = klass.eager_load(eager_load).find(id)
34
-
35
- authorize! :read, item
36
-
37
- item
38
- end
39
-
40
- define_method "#{klass.name.underscore}.create" do |params|
41
- authorize! :create, klass
42
-
43
- klass_entity = "#{klass.name}::Entity".constantize
44
-
45
- if klass_entity.respond_to?(:create_params, params)
46
- params = klass_entity.send(:create_params, params)
47
- elsif klass_entity.respond_to?(:params, params)
48
- params = klass_entity.send(:params, params)
49
- end
50
-
51
- klass.create!(params)
52
- end
53
-
54
- define_method "#{klass.name.underscore}.update" do |id, params|
55
- item = klass.find(id)
56
-
57
- authorize! :update, item
58
-
59
- klass_entity = "#{klass.name}::Entity".constantize
60
-
61
- if klass_entity.respond_to?(:update_params, params)
62
- params = klass_entity.send(:update_params, params)
63
- elsif klass_entity.respond_to?(:params, params)
64
- params = klass_entity.send(:params, params)
65
- end
66
-
67
- item.update!(params)
68
- end
69
-
70
- define_method "#{klass.name.underscore}.destroy" do |id|
71
- item = klass.find(id)
72
-
73
- authorize! :destroy, item
74
-
75
- item.destroy!
76
- end
77
- end
78
- end
79
-
80
17
  class Error < StandardError; end
81
18
  class CacheMissed < StandardError; end
82
19
 
@@ -84,8 +21,6 @@ class APIQL
84
21
  delegate :authorize!, to: :@context
85
22
 
86
23
  class << self
87
- include ::APIQL::CRUD
88
-
89
24
  def mount(klass, as: nil)
90
25
  as ||= klass.name.split('::').last.underscore
91
26
  as += '.' if as.present?
@@ -106,18 +41,34 @@ class APIQL
106
41
 
107
42
  if request.present?
108
43
  request = compile(request)
109
- redis&.set("api-ql-cache-#{request_id}", request.to_json)
44
+ ::Rails.cache.write("api-ql-cache-#{request_id}", JSON.generate(request), expires_in: 31*24*60*60)
110
45
  @@cache[request_id] = request
111
46
  @@cache = {} if(@@cache.count > 1000)
112
47
  else
113
48
  request = @@cache[request_id]
114
- request ||= JSON.parse(redis.get("api-ql-cache-#{request_id}")) rescue nil
49
+ request ||= JSON.parse(::Rails.cache.fetch("api-ql-cache-#{request_id}")) rescue nil
115
50
  raise CacheMissed unless request.present? && request.is_a?(::Array)
116
51
  end
117
52
 
118
53
  request
119
54
  end
120
55
 
56
+ def cacheable(dependencies, attr, expires_in: 31*24*60*60)
57
+ dependencies = dependencies.flatten.map { |obj| [obj.class.name, obj.id] }
58
+ name = ['api-ql-cache', dependencies, attr].flatten.join('-')
59
+ begin
60
+ raise if expires_in <= 0
61
+ JSON.parse(::Rails.cache.fetch(name))
62
+ rescue
63
+ (block_given? ? yield : nil).tap { |data| expires_in > 0 && ::Rails.cache.write(name, JSON.generate(data), expires_in: expires_in) }
64
+ end
65
+ end
66
+
67
+ def drop_cache(obj)
68
+ return unless ::Rails.cache.respond_to? :delete_if
69
+ ::Rails.cache.delete_if { |k, v| k =~ /api-ql.*-#{obj.class.name}-#{obj.id}-.*/ }
70
+ end
71
+
121
72
  def simple_class?(value)
122
73
  value.nil? ||
123
74
  value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
@@ -151,15 +102,6 @@ class APIQL
151
102
 
152
103
  private
153
104
 
154
- def redis
155
- @redis ||=
156
- begin
157
- ::Redis.new(host: 'localhost')
158
- rescue
159
- nil
160
- end
161
- end
162
-
163
105
  def compile(schema)
164
106
  result = []
165
107
 
@@ -201,7 +143,7 @@ class APIQL
201
143
 
202
144
  ptr = pool.pop
203
145
  elsif pool.any?
204
- if reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s*)?(?<name>[\w\.]+)(\((?<params>.*?)\))?(?<rest>.*)\z/m)
146
+ if reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s*)?(?<name>[\w\.\:\!\?]+)(\((?<params>.*?)\))?(?<rest>.*)\z/m)
205
147
  schema = reg[:rest]
206
148
 
207
149
  key = reg[:alias].present? ? "#{reg[:alias]}: #{reg[:name]}" : reg[:name]
@@ -213,7 +155,7 @@ class APIQL
213
155
  else
214
156
  raise Error, schema
215
157
  end
216
- elsif reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s*)?(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\{(?<rest>.*)\z/m)
158
+ elsif reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s+)?(?<name>[\w\.\:\!\?]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\{(?<rest>.*)\z/m)
217
159
  schema = reg[:rest]
218
160
 
219
161
  key = "#{reg[:alias] || reg[:name]}: #{reg[:name]}(#{reg[:params]})"
@@ -221,7 +163,7 @@ class APIQL
221
163
  pool.push(ptr)
222
164
 
223
165
  ptr = push_key.call(key, true)
224
- elsif reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s*)?(?<name>[\w\.]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\n?(?<rest>.*)\z/m)
166
+ elsif reg = schema.match(/\A\s*((?<alias>[\w\.]+):\s+)?(?<name>[\w\.\:\!\?]+)(\((?<params>((\w+)(\s*\,\s*\w+)*))?\))?\s*\n?(?<rest>.*)\z/m)
225
167
  schema = reg[:rest]
226
168
 
227
169
  key = "#{reg[:alias] || reg[:name]}: #{reg[:name]}(#{reg[:params]})"
@@ -255,7 +197,7 @@ class APIQL
255
197
  schema.each do |call|
256
198
  if call.is_a? ::Hash
257
199
  call.each do |function, sub_schema|
258
- reg = function.match(/\A((?<alias>[\w\.\!]+):\s*)?(?<name>[\w\.\!]+)(\((?<params>.*?)\))?\z/)
200
+ reg = function.match(/\A((?<alias>[\w\.\:\!\?]+):\s+)?(?<name>[\w\.\:\!\?]+)(\((?<params>.*?)\))?\z/)
259
201
  raise Error, function unless reg.present?
260
202
 
261
203
  name = reg[:alias] || reg[:name]
@@ -263,9 +205,11 @@ class APIQL
263
205
  params = @context.parse_params(reg[:params].presence)
264
206
 
265
207
  @eager_load = APIQL::eager_loads(sub_schema)
266
- data = public_send(function, *params)
208
+
209
+ data = call_function(function, *params)
210
+
267
211
  if @eager_load.present? && !data.is_a?(::Hash) && !data.is_a?(::Array)
268
- if data.respond_to?(:eager_load)
212
+ if data.respond_to?(:includes)
269
213
  data = data.includes(eager_load)
270
214
  elsif data.respond_to?(:id)
271
215
  data = data.class.includes(eager_load).find(data.id)
@@ -281,7 +225,7 @@ class APIQL
281
225
  end
282
226
  end
283
227
  else
284
- reg = call.match(/\A((?<alias>[\w\.\!]+):\s*)?(?<name>[\w\.\!]+)(\((?<params>.*?)\))?\z/)
228
+ reg = call.match(/\A((?<alias>[\w\.\:\!\?]+):\s+)?(?<name>[\w\.\:\!\?]+)(\((?<params>.*?)\))?\z/)
285
229
  raise Error, call unless reg.present?
286
230
 
287
231
  name = reg[:alias] || reg[:name]
@@ -289,7 +233,9 @@ class APIQL
289
233
  params = @context.parse_params(reg[:params].presence)
290
234
 
291
235
  @eager_load = []
292
- data = public_send(function, *params)
236
+
237
+ data = call_function(function, *params)
238
+
293
239
  if data.is_a? Array
294
240
  if data.any? { |item| !APIQL::simple_class?(item) }
295
241
  data = nil
@@ -313,11 +259,112 @@ class APIQL
313
259
 
314
260
  private
315
261
 
262
+ def call_function(name, *params)
263
+ if respond_to?(name) && (methods - Object.methods).include?(name.to_sym)
264
+ public_send(name, *params)
265
+ else
266
+ o = nil
267
+
268
+ names = name.split('.')
269
+ names.each_with_index do |name, index|
270
+ if o.nil?
271
+ o = "#{name}::Entity".constantize
272
+ o.instance_variable_set('@context', @context)
273
+ o.instance_variable_set('@eager_load', @eager_load)
274
+ @context.inject_delegators(self)
275
+ elsif o.superclass == ::APIQL::Entity || o.superclass.superclass == ::APIQL::Entity
276
+ if o.respond_to?(name) && (o.methods - Object.methods).include?(name.to_sym)
277
+ o =
278
+ if index == names.count - 1
279
+ o.public_send(name, *params)
280
+ else
281
+ o.public_send(name)
282
+ end
283
+ else
284
+ o = nil
285
+ break
286
+ end
287
+ else
288
+ objects = o
289
+ o = "#{o.name}::Entity".constantize
290
+ if o.respond_to?(name) && (o.methods - Object.methods).include?(name.to_sym)
291
+ o.instance_variable_set("@objects", objects)
292
+ o.instance_variable_set('@context', @context)
293
+ o.instance_variable_set('@eager_load', @eager_load)
294
+ @context.inject_delegators(self)
295
+ o =
296
+ if index == names.count - 1
297
+ o.public_send(name, *params)
298
+ else
299
+ o.public_send(name)
300
+ end
301
+ else
302
+ o = nil
303
+ break
304
+ end
305
+ end
306
+ end
307
+
308
+ o
309
+ end
310
+ end
311
+
316
312
  class Entity
317
313
  delegate :authorize!, to: :@context
318
314
 
319
315
  class << self
320
- attr_reader :apiql_attributes
316
+ def all
317
+ authorize! :read, @apiql_entity_class
318
+
319
+ @apiql_entity_class.eager_load(eager_load).all
320
+ end
321
+
322
+ def find(id)
323
+ item = @apiql_entity_class.eager_load(eager_load).find(id)
324
+
325
+ authorize! :read, item
326
+
327
+ item
328
+ end
329
+
330
+ def create(params)
331
+ authorize! :create, @apiql_entity_class
332
+
333
+ if respond_to?(:create_params, params)
334
+ params = create_params(params)
335
+ elsif respond_to?(:params, params)
336
+ params = self.params(params)
337
+ end
338
+
339
+ @apiql_entity_class.create!(params)
340
+ end
341
+
342
+ def update(id, params)
343
+ item = @apiql_entity_class.find(id)
344
+
345
+ authorize! :update, item
346
+
347
+ if respond_to?(:update_params, params)
348
+ params = update_params(params)
349
+ elsif respond_to?(:params, params)
350
+ params = self.params(params)
351
+ end
352
+
353
+ item.update!(params)
354
+ end
355
+
356
+ def destroy(id)
357
+ item = @apiql_entity_class.find(id)
358
+
359
+ authorize! :destroy, item
360
+
361
+ item.destroy!
362
+ end
363
+
364
+ private
365
+
366
+ attr_reader :apiql_attributes, :objects
367
+ delegate :authorize!, to: :@context, private: true
321
368
 
322
369
  def inherited(child)
323
370
  super
@@ -328,6 +375,10 @@ class APIQL
328
375
 
329
376
  child.instance_eval do
330
377
  @apiql_attributes = attributes
378
+
379
+ names = child.name.split('::')
380
+ return unless names.last == 'Entity'
381
+ @apiql_entity_class = names[0..-2].join('::').constantize
331
382
  end
332
383
  end
333
384
 
@@ -335,6 +386,14 @@ class APIQL
335
386
  @apiql_attributes ||= []
336
387
  @apiql_attributes += attrs.map(&:to_sym)
337
388
  end
389
+
390
+ def eager_load
391
+ result = @eager_load
392
+
393
+ @eager_load = []
394
+
395
+ result
396
+ end
338
397
  end
339
398
 
340
399
  attr_reader :object
@@ -354,7 +413,7 @@ class APIQL
354
413
  schema.each do |field|
355
414
  if field.is_a? Hash
356
415
  field.each do |field, sub_schema|
357
- reg = field.match(/\A((?<alias>[\w\.\!]+):\s*)?(?<name>[\w\.\!]+)(\((?<params>.*?)\))?\z/)
416
+ reg = field.match(/\A((?<alias>[\w\.\!\?]+):\s*)?(?<name>[\w\.\!\?]+)(\((?<params>.*?)\))?\z/)
358
417
  raise Error, field unless reg.present?
359
418
 
360
419
  name = reg[:alias] || reg[:name]
@@ -368,7 +427,7 @@ class APIQL
368
427
  end
369
428
  end
370
429
  else
371
- reg = field.match(/\A((?<alias>[\w\.\!]+):\s*)?(?<name>[\w\.\!]+)(\((?<params>.*?)\))?\z/)
430
+ reg = field.match(/\A((?<alias>[\w\.\!\?]+):\s*)?(?<name>[\w\.\!\?]+)(\((?<params>.*?)\))?\z/)
372
431
  raise Error, field unless reg.present?
373
432
 
374
433
  name = reg[:alias] || reg[:name]
@@ -396,20 +455,31 @@ class APIQL
396
455
  names = field.split('.')
397
456
  if names.count > 1
398
457
  o = nil
399
- names.each do |field|
458
+ names.each_with_index do |field, index|
400
459
  if o.nil?
401
460
  return unless field.to_sym.in? self.class.apiql_attributes
402
461
 
403
- if respond_to? field
404
- o = public_send(field, *params)
462
+ if respond_to?(field) && (methods - Object.methods).include?(name.to_sym)
463
+ o = public_send(field)
405
464
  else
406
- o = object.public_send(field, *params)
465
+ o = object.public_send(field)
407
466
  end
408
467
 
409
468
  break if o.nil?
410
469
  else
411
- if o.respond_to? field
412
- o = o.public_send(field, *params)
470
+ objects = o
471
+ o = "#{o.name}::Entity".constantize
472
+ o.instance_variable_set("@objects", objects)
473
+ o.instance_variable_set('@context', @context)
474
+ o.instance_variable_set('@eager_load', @eager_load)
475
+ @context.inject_delegators(self)
476
+
477
+ if o.respond_to?(field) && (o.methods - Object.methods).include?(name.to_sym)
478
+ if index == names.count - 1
479
+ o = o.public_send(field, *params)
480
+ else
481
+ o = o.public_send(field)
482
+ end
413
483
  else
414
484
  o = nil
415
485
  break
@@ -419,9 +489,9 @@ class APIQL
419
489
 
420
490
  o
421
491
  else
422
- return unless field.to_sym.in? self.class.apiql_attributes
492
+ return unless field.to_sym.in? self.class.send(:apiql_attributes)
423
493
 
424
- if respond_to? field
494
+ if respond_to?(field) && (methods - Object.methods).include?(name.to_sym)
425
495
  public_send(field, *params)
426
496
  else
427
497
  object.public_send(field, *params)
@@ -473,10 +543,10 @@ class APIQL
473
543
 
474
544
  field.split('.').each do |name|
475
545
  if o.nil?
476
- o = object[name.to_sym]
546
+ o = object[name.to_sym] || object[name.to_s]
477
547
  break if o.nil?
478
548
  else
479
- if o.respond_to? name
549
+ if o.respond_to?(name) && (o.methods - Object.methods).include?(name.to_sym)
480
550
  o = o.public_send(name)
481
551
  else
482
552
  o = nil
@@ -504,6 +574,7 @@ class APIQL
504
574
 
505
575
  def inject_delegators(target)
506
576
  @fields.each do |field|
577
+ next if target.respond_to?(field)
507
578
  target.class_eval do
508
579
  delegate field, to: :@context
509
580
  end
@@ -537,11 +608,7 @@ class APIQL
537
608
  begin
538
609
  "#{value.class.name}::Entity".constantize.new(value, self).render(schema)
539
610
  rescue StandardError
540
- if ::Rails.env.development?
541
- raise
542
- else
543
- nil
544
- end
611
+ raise
545
612
  end
546
613
  end
547
614
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apiql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Silchenko
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-07 00:00:00.000000000 Z
11
+ date: 2022-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -69,7 +69,7 @@ homepage: https://github.com/desofto/apiql
69
69
  licenses:
70
70
  - MIT
71
71
  metadata: {}
72
- post_install_message:
72
+ post_install_message:
73
73
  rdoc_options: []
74
74
  require_paths:
75
75
  - lib
@@ -84,9 +84,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
86
  requirements: []
87
- rubyforge_project:
88
- rubygems_version: 2.7.9
89
- signing_key:
87
+ rubygems_version: 3.2.15
88
+ signing_key:
90
89
  specification_version: 4
91
90
  summary: Implementation of the API language similar to GraphQL for Ruby on Rails
92
91
  test_files: []