apiql 0.4.8 → 1.0.5

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: 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: []