fastapi 0.1.25 → 0.1.26

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
  SHA1:
3
- metadata.gz: a6257aad18a7f5ccf31fd70bcb267a3976b83518
4
- data.tar.gz: 81a7955417c82b41deafc611138a1fe606d20d4f
3
+ metadata.gz: 5478a368c91cfadee25770397b95a7b24865166f
4
+ data.tar.gz: 6362f9b6f08b3f0111ae69e3ac5695c4bf96cc8e
5
5
  SHA512:
6
- metadata.gz: db70059eb226331aa57f6964612d7fde14e64b47671950882c2408ec1c0730740fa7fc60540f832ec713a449547727446fc715d32662bfc98f895b3f1f09681c
7
- data.tar.gz: d0a9bfffd1eed2b65288bcaad6eb01331e573df1932923e75d0b9d6d679c19b70ce5a065f852291c50b1fce4132259bab8e488a3aa921802ed635f54421a22d8
6
+ metadata.gz: da97e14f324c2772288c78626102adb6ce2fb9b0a4db07d678a5b3dbd24fd80a77bfb9d5e573322c81646868481e0d03e84d0d23ccb38535d5d1e75f83dfd01f
7
+ data.tar.gz: f885f7f223dfea56a927167c38acd8cea4b45e1b2166671f1cce6eeab8b28daaa60e98053bec391f917b073619f0ade693ec6d0233fc0a955405c66a8b5d060d
@@ -1,27 +1,24 @@
1
1
  require 'oj'
2
- require 'fastapi/active_record_extension.rb'
2
+ require 'fastapi/active_record_extension'
3
3
 
4
4
  class FastAPI
5
5
 
6
- @@result_types = {
7
- single: 0,
8
- multiple: 1,
9
- }
10
-
11
- @@api_comparator_list = [
12
- 'is',
13
- 'not',
14
- 'gt',
15
- 'gte',
16
- 'lt',
17
- 'lte',
18
- 'in',
19
- 'not_in',
20
- 'contains',
21
- 'icontains',
22
- 'is_null',
23
- 'not_null',
24
- ]
6
+ @@result_types = { single: 0, multiple: 1 }
7
+
8
+ @@api_comparator_list = %w(
9
+ is
10
+ not
11
+ gt
12
+ gte
13
+ lt
14
+ lte
15
+ in
16
+ not_in
17
+ contains
18
+ icontains
19
+ is_null
20
+ not_null
21
+ )
25
22
 
26
23
  def initialize(model)
27
24
  @model = model
@@ -40,11 +37,9 @@ class FastAPI
40
37
  # @param fields [Array] an array containing fields to whitelist for the SQL query. Can also pass in fields as arguments.
41
38
  # @return [FastAPI] the current instance
42
39
  def whitelist(fields = [])
43
-
44
- @whitelist_fields.concat fields
40
+ @whitelist_fields.concat(fields)
45
41
 
46
42
  self
47
-
48
43
  end
49
44
 
50
45
  # Create and execute an optimized SQL query based on specified filters
@@ -53,27 +48,13 @@ class FastAPI
53
48
  # @param meta [Hash] a hash containing custom metadata
54
49
  # @return [FastAPI] the current instance
55
50
  def filter(filters = {}, meta = {}, safe = false)
56
-
57
51
  result = fastapi_query(filters, safe)
58
52
 
59
- metadata = {}
60
-
61
- meta.each do |key, value|
62
- metadata[key] = value
63
- end
64
-
65
- metadata[:total] = result[:total]
66
- metadata[:offset] = result[:offset]
67
- metadata[:count] = result[:count]
68
- metadata[:error] = result[:error]
69
-
70
- @metadata = metadata
71
- @data = result[:data]
72
-
53
+ @metadata = meta.merge(result.slice(:total, :offset, :count, :error))
54
+ @data = result[:data]
73
55
  @result_type = @@result_types[:multiple]
74
56
 
75
57
  self
76
-
77
58
  end
78
59
 
79
60
  # Create and execute an optimized SQL query based on specified filters.
@@ -83,11 +64,7 @@ class FastAPI
83
64
  # @param meta [Hash] a hash containing custom metadata
84
65
  # @return [FastAPI] the current instance
85
66
  def safe_filter(filters = {}, meta = {})
86
-
87
67
  filter(filters, meta, true)
88
-
89
- self
90
-
91
68
  end
92
69
 
93
70
  # Create and execute an optimized SQL query based on specified object id.
@@ -97,15 +74,13 @@ class FastAPI
97
74
  # @param meta [Hash] a hash containing custom metadata
98
75
  # @return [FastAPI] the current instance
99
76
  def fetch(id, meta = {})
77
+ filter({ id: id }, meta)
100
78
 
101
- filter({id: id}, meta)
102
-
103
- if @metadata[:total] == 0
104
- @metadata[:error] = {message: @model.to_s + ' id does not exist'}
79
+ if @metadata[:total].zero?
80
+ @metadata[:error] = { message: "#{@model} id does not exist" }
105
81
  end
106
82
 
107
83
  self
108
-
109
84
  end
110
85
 
111
86
  # Returns the data from the most recently executed `filter` or `fetch` call.
@@ -140,10 +115,7 @@ class FastAPI
140
115
  #
141
116
  # @return [Hash] available data and metadata
142
117
  def to_hash
143
- {
144
- meta: @metadata,
145
- data: @data
146
- }
118
+ { meta: @metadata, data: @data }
147
119
  end
148
120
 
149
121
  # Intended to return the final API response
@@ -157,24 +129,11 @@ class FastAPI
157
129
  #
158
130
  # @return [String] JSON data and metadata
159
131
  def spoof(data = [], meta = {})
132
+ meta[:total] ||= data.count
133
+ meta[:count] ||= data.count
134
+ meta[:offset] ||= 0
160
135
 
161
- if not meta.has_key? :total
162
- meta[:total] = data.count
163
- end
164
-
165
- if not meta.has_key? :offset
166
- meta[:offset] = 0
167
- end
168
-
169
- if not meta.has_key? :count
170
- meta[:count] = data.count
171
- end
172
-
173
- Oj.dump({
174
- meta: meta,
175
- data: data
176
- }, mode: :compat)
177
-
136
+ Oj.dump({ meta: meta, data: data }, mode: :compat)
178
137
  end
179
138
 
180
139
  # Returns a JSONified string representing a rejected API response with invalid fields parameters
@@ -192,7 +151,7 @@ class FastAPI
192
151
  fields: fields
193
152
  }
194
153
  },
195
- data: [],
154
+ data: []
196
155
  }, mode: :compat)
197
156
  end
198
157
 
@@ -210,878 +169,533 @@ class FastAPI
210
169
  message: message.to_s
211
170
  }
212
171
  },
213
- data: [],
172
+ data: []
214
173
  }, mode: :compat)
215
174
  end
216
175
 
217
176
  private
218
177
 
219
- def fastapi_query(filters = {}, safe = false)
220
-
221
- if (not ActiveRecord::ConnectionAdapters.constants.include? :PostgreSQLAdapter or
222
- not ActiveRecord::Base.connection.instance_of? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
223
- raise 'Fast API only supports PostgreSQL at this time'
224
- end
225
-
226
- offset = 0
227
- count = 500
228
- order = nil
229
-
230
- if filters.has_key? :__offset
231
- offset = filters[:__offset].to_i
232
- filters.delete(:__offset)
233
- end
234
-
235
- if filters.has_key? :__count
236
- count = [1, [500, filters[:__count].to_i].min].max
237
- filters.delete(:__count)
238
- end
239
-
240
- begin
241
- prepared_data = api_generate_sql(filters, offset, count, safe)
242
- rescue Exception => error
243
- return {
244
- data: [],
245
- total: 0,
246
- count: 0,
247
- offset: offset,
248
- error: {message: error.message}
249
- }
250
- end
178
+ def clamp(value, min, max)
179
+ [min, value, max].sort[1]
180
+ end
251
181
 
252
- model_lookup = {}
253
- prepared_data[:models].each do |key, model|
254
- columns_hash = model.columns_hash
255
- model_lookup[key] = {
256
- model: model,
257
- fields: model.fastapi_fields_sub,
258
- types: model.fastapi_fields_sub.map { |field| (columns_hash.has_key? field.to_s) ? columns_hash[field.to_s].type : nil },
259
- }
260
- end
182
+ def error(offset, message)
183
+ { data: [], total: 0, count: 0, offset: offset, error: { message: message } }
184
+ end
261
185
 
262
- error = nil
263
-
264
- begin
265
- count_result = ActiveRecord::Base.connection.execute(prepared_data[:count_query])
266
- result = ActiveRecord::Base.connection.execute(prepared_data[:query])
267
- rescue
268
- return {
269
- data: [],
270
- total: 0,
271
- count: 0,
272
- offset: offset,
273
- error: {message: 'Query failed'}
274
- }
275
- end
186
+ def fastapi_query(filters = {}, safe = false)
276
187
 
277
- total_size = count_result.values().size > 0 ? count_result.values()[0][0].to_i : 0
188
+ unless ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQLAdapter) &&
189
+ ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
190
+ fail 'FastAPI only supports PostgreSQL at this time.'
191
+ end
278
192
 
279
- start = Time.now()
193
+ offset = filters.delete(:__offset).try(:to_i) || 0
194
+ cnt = filters.delete(:__count).try(:to_i) || 500
195
+ count = clamp(cnt, 1, 500)
280
196
 
281
- fields = result.fields()
282
- rows = result.values()
197
+ begin
198
+ prepared_data = api_generate_sql(filters, offset, count, safe)
199
+ rescue StandardError => exception
200
+ return error(offset, exception.message)
201
+ end
283
202
 
284
- dataset = Array.new(rows.size)
203
+ model_lookup = prepared_data[:models].each_with_object({}) do |(key, model), lookup|
204
+ columns = model.columns_hash
205
+ lookup[key] = {
206
+ model: model,
207
+ fields: model.fastapi_fields_sub,
208
+ types: model.fastapi_fields_sub.map { |field| columns[field.to_s].try(:type) }
209
+ }
210
+ end
285
211
 
286
- rows.each_with_index do |row, index|
287
- currow = {}
288
- row.each_with_index do |val, key_index|
212
+ begin
213
+ count_result = ActiveRecord::Base.connection.execute(prepared_data[:count_query])
214
+ result = ActiveRecord::Base.connection.execute(prepared_data[:query])
215
+ rescue
216
+ error(offset, 'Query failed')
217
+ end
289
218
 
290
- field = fields[key_index]
291
- split_index = field.rindex('__')
219
+ total_size = count_result.values.size > 0 ? count_result.values[0][0].to_i : 0
292
220
 
293
- if field[0..7] == '__many__'
221
+ fields = result.fields
222
+ rows = result.values
294
223
 
295
- field = field[8..-1]
296
- field_sym = field.to_sym
297
- model = model_lookup[field_sym]
224
+ dataset = rows.each_with_object([]) do |row, data|
225
+ datum = row.each_with_object({}).with_index do |(val, current), index|
226
+ field = fields[index]
227
+ split_index = field.rindex('__')
298
228
 
299
- currow[field_sym] = parse_many(
300
- val,
301
- model_lookup[field_sym][:fields],
302
- model_lookup[field_sym][:types]
303
- )
229
+ if field[0..7] == '__many__'
304
230
 
305
- elsif split_index
231
+ field = field[8..-1]
232
+ field_sym = field.to_sym
233
+ model = model_lookup[field_sym]
306
234
 
307
- obj_name = field[0..split_index - 1].to_sym
308
- field = field[split_index + 2..-1]
309
- model = model_lookup[obj_name][:model]
235
+ current[field_sym] = parse_many(val, model[:fields], model[:types])
310
236
 
311
- if !(currow.has_key? obj_name)
312
- currow[obj_name] = {}
313
- end
237
+ elsif split_index
314
238
 
315
- currow[obj_name][field.to_sym] = api_convert_type(
316
- val,
317
- model.columns_hash[field].type,
318
- (model.columns_hash[field].respond_to?('array') and model.columns_hash[field].array)
319
- )
239
+ obj_name = field[0..split_index - 1].to_sym
240
+ field = field[split_index + 2..-1]
241
+ model = model_lookup[obj_name][:model]
320
242
 
321
- elsif @model.columns_hash[field]
243
+ current[obj_name] ||= {}
322
244
 
323
- currow[field.to_sym] = api_convert_type(
324
- val,
325
- @model.columns_hash[field].type,
326
- (@model.columns_hash[field].respond_to?('array') and @model.columns_hash[field].array)
327
- )
245
+ current[obj_name][field.to_sym] = api_convert_type(val,
246
+ model.columns_hash[field].type,
247
+ (model.columns_hash[field].respond_to?('array') && model.columns_hash[field].array))
328
248
 
329
- end
249
+ elsif @model.columns_hash[field]
330
250
 
251
+ current[field.to_sym] = api_convert_type(val,
252
+ @model.columns_hash[field].type,
253
+ (@model.columns_hash[field].respond_to?('array') && @model.columns_hash[field].array))
331
254
  end
332
-
333
- dataset[index] = currow
334
-
335
255
  end
336
-
337
- my_end = Time.now()
338
-
339
- # puts dataset.size.to_s + '-length array parsed in ' + (my_end - start).to_s
340
-
341
- {
342
- data: dataset,
343
- total: total_size,
344
- count: dataset.size,
345
- offset: offset,
346
- error: nil
347
- }
348
-
256
+ data << datum
349
257
  end
258
+ { data: dataset, total: total_size, count: dataset.size, offset: offset, error: nil }
259
+ end
350
260
 
351
- # the two following methods are very similar, can reuse
352
-
353
- def parse_postgres_array(str)
354
-
355
- unless str.is_a? String
356
- return []
261
+ def parse_many(str, fields, types)
262
+ Oj.load(str).map do |row|
263
+ row.values.each_with_object({}).with_index do |(value, values), index|
264
+ values[fields[index]] = api_convert_type(value, types[index])
357
265
  end
266
+ end
267
+ end
358
268
 
359
- i = 0
360
- len = str.length
361
-
362
- values = []
363
-
364
- i = str.index('{')
365
-
366
- return values unless i
269
+ def api_comparison(comparator, value, field, type, is_array)
270
+ field_string = is_array ? "ANY(#{field})" : field
367
271
 
368
- i = i + 1
272
+ if comparator == 'is'
369
273
 
370
- while i < len
274
+ ActiveRecord::Base.connection.quote(value.to_s) + ' = ' + field_string
371
275
 
372
- c = str[i]
276
+ elsif comparator == 'not'
373
277
 
374
- if c == '}'
278
+ ActiveRecord::Base.connection.quote(value.to_s) + ' <> ' + field_string
375
279
 
376
- break
280
+ elsif comparator == 'gt'
377
281
 
378
- elsif c == '"'
282
+ ActiveRecord::Base.connection.quote(value.to_s) + ' < ' + field_string
379
283
 
380
- i += 1
381
- nextIndex = str.index('"', i)
284
+ elsif comparator == 'gte'
382
285
 
383
- while str[nextIndex - 1] == '\\'
286
+ ActiveRecord::Base.connection.quote(value.to_s) + ' <= ' + field_string
384
287
 
385
- j = 1
386
- while str[nextIndex - j] == '\\'
387
- j += 1
388
- end
288
+ elsif comparator == 'lt'
389
289
 
390
- if j & 1 == 1
391
- break
392
- end
290
+ ActiveRecord::Base.connection.quote(value.to_s) + ' > ' + field_string
393
291
 
394
- nextIndex = str.index('"', nextIndex + 1)
292
+ elsif comparator == 'lte'
395
293
 
396
- end
294
+ ActiveRecord::Base.connection.quote(value.to_s) + ' >= ' + field_string
397
295
 
398
- values.push str[i...nextIndex]
296
+ elsif comparator == 'in' || comparator == 'not_in'
399
297
 
400
- i = nextIndex + 1
298
+ unless value.is_a?(Array)
401
299
 
300
+ if value.is_a?(Range)
301
+ value = value.to_a
402
302
  else
403
-
404
- if c == ','
405
-
406
- values.push nil
407
- i += 1
408
- next
409
-
410
- end
411
-
412
- parensIndex = str.index('}', i)
413
- nextIndex = str.index(',', i)
414
-
415
- if nextIndex.nil? or nextIndex > parensIndex
416
-
417
- values.push str[i...parensIndex]
418
- break
419
-
420
- end
421
-
422
- values.push str[i...nextIndex]
423
-
424
- i = nextIndex + 1
425
-
303
+ value = [value.to_s]
426
304
  end
427
-
428
305
  end
429
306
 
430
- return values
431
-
432
- end
433
-
434
- def parse_many(str, fields = [], types = [])
435
-
436
- unless str.is_a? String
437
- return []
438
- end
439
-
440
- rows = []
441
- cur_row = {}
442
- entry_index = 0
443
-
444
- i = 0
445
- len = str.length
446
-
447
- i = str.index('(')
448
-
449
- if not i
450
- return rows
451
- end
452
-
453
- i = i + 1
454
-
455
- while i < len
456
-
457
- c = str[i]
458
-
459
- if c == ')'
460
-
461
- rows << cur_row
462
- cur_row = {}
463
- entry_index = 0
464
- i = i + 3
465
-
466
- elsif c == '"'
467
-
468
- i = i + 1
469
- nextIndex = str.index('"', i)
470
-
471
- while str[nextIndex - 1] == '\\'
472
-
473
- j = 1
474
- while str[nextIndex - j] == '\\'
475
- j = j + 1
476
- end
477
-
478
- if j & 1 == 1
479
- break
480
- end
481
-
482
- nextIndex = str.index('"', nextIndex + 1)
483
-
484
- end
485
-
486
- cur_row[fields[entry_index]] = api_convert_type(str[i...nextIndex], types[entry_index])
307
+ if is_array
487
308
 
488
- entry_index = entry_index + 1
309
+ type_convert = {
310
+ boolean: '::boolean',
311
+ integer: '::integer',
312
+ float: '::float',
313
+ string: '::varchar'
314
+ }[type]
489
315
 
490
- i = nextIndex + 1
491
-
492
- elsif c == ','
493
-
494
- i = i + 1
495
- cur_row[fields[entry_index]] = nil
496
- entry_index = entry_index + 1
316
+ type_convert = '::text' if type.nil?
497
317
 
318
+ if comparator == 'in'
319
+ 'ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
498
320
  else
321
+ 'NOT ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
322
+ end
323
+ else
499
324
 
500
- parensIndex = str.index(')', i)
501
- nextIndex = str.index(',', i)
325
+ if comparator == 'in'
326
+ field + ' IN(' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
327
+ else
328
+ field + ' NOT IN(' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
329
+ end
330
+ end
502
331
 
503
- if nextIndex.nil? or nextIndex > parensIndex
504
- nextIndex = parensIndex
505
- end
332
+ elsif comparator == 'contains'
506
333
 
507
- if i == nextIndex
508
- cur_row[fields[entry_index]] = nil
509
- else
510
- cur_row[fields[entry_index]] = api_convert_type(str[i...nextIndex], types[entry_index])
511
- end
334
+ field_string + ' LIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
512
335
 
513
- entry_index = entry_index + 1
336
+ elsif comparator == 'icontains'
514
337
 
515
- if nextIndex == parensIndex
516
- rows << cur_row
517
- cur_row = {}
518
- entry_index = 0
519
- i = nextIndex + 3
520
- else
521
- i = nextIndex + 1
522
- end
338
+ field_string + ' ILIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
523
339
 
524
- end
340
+ elsif comparator == 'is_null'
525
341
 
526
- end
342
+ "#{field_string} IS NULL"
527
343
 
528
- rows
344
+ elsif comparator == 'not_null'
529
345
 
346
+ "#{field_string} IS NOT NULL"
530
347
  end
348
+ end
531
349
 
350
+ def api_convert_type(val, type, is_array = false)
351
+ if val && is_array
352
+ Oj.load(val).map { |inner_value| api_convert_value(inner_value, type) }
353
+ else
354
+ api_convert_value(val, type)
355
+ end
356
+ end
532
357
 
533
- def api_comparison(comparator, value, field, type, is_array)
534
-
535
- unless is_array
536
- field_string = field
358
+ def api_convert_value(val, type)
359
+ if val
360
+ case type
361
+ when :integer
362
+ val.to_i
363
+ when :float
364
+ val.to_f
365
+ when :boolean
366
+ { 't' => true, 'f' => false }[val]
537
367
  else
538
- field_string = 'ANY(' + field + ')'
368
+ val
539
369
  end
370
+ end
371
+ end
540
372
 
541
- if comparator == 'is'
542
-
543
- ActiveRecord::Base.connection.quote(value.to_s) + ' = ' + field_string
544
-
545
- elsif comparator == 'not'
546
-
547
- ActiveRecord::Base.connection.quote(value.to_s) + ' <> ' + field_string
548
-
549
- elsif comparator == 'gt'
550
-
551
- ActiveRecord::Base.connection.quote(value.to_s) + ' < ' + field_string
552
-
553
- elsif comparator == 'gte'
554
-
555
- ActiveRecord::Base.connection.quote(value.to_s) + ' <= ' + field_string
556
-
557
- elsif comparator == 'lt'
558
-
559
- ActiveRecord::Base.connection.quote(value.to_s) + ' > ' + field_string
560
-
561
- elsif comparator == 'lte'
562
-
563
- ActiveRecord::Base.connection.quote(value.to_s) + ' >= ' + field_string
564
-
565
- elsif comparator == 'in' or comparator == 'not_in'
566
-
567
- if not value.is_a? Array
568
-
569
- if value.is_a? Range
570
- value = value.to_a
571
- else
572
- value = [value.to_s]
573
- end
574
-
575
- end
576
-
577
- if is_array
373
+ def parse_filters(filters, safe = false, model = nil)
578
374
 
579
- type_convert = {
580
- boolean: '::boolean',
581
- integer: '::integer',
582
- float: '::float'
583
- }[type]
375
+ self_obj = model ? model : @model
376
+ self_string_table = model ? "__#{model.to_s.tableize}" : @model.to_s.tableize
584
377
 
585
- type_convert = '::text' if type.nil?
378
+ filters = filters.clone.symbolize_keys
379
+ # if we're at the top level...
380
+ if model.nil?
586
381
 
587
- if comparator == 'in'
588
- 'ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
589
- else
590
- 'NOT ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
591
- end
592
-
593
- else
382
+ if safe
383
+ filters.each do |key, value|
384
+ found_index = key.to_s.rindex('__')
385
+ key_root = found_index ? key.to_s[0..found_index].to_sym : key
594
386
 
595
- if comparator == 'in'
596
- field + ' IN(' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
597
- else
598
- field + ' NOT IN(' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ')'
387
+ if [:__order, :__offset, :__count].exclude?(key) && self_obj.fastapi_filters_whitelist.exclude?(key_root)
388
+ fail %(Filter "#{key}" not supported.)
599
389
  end
600
-
601
390
  end
602
-
603
- elsif comparator == 'contains'
604
-
605
- field_string + ' LIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
606
-
607
- elsif comparator == 'icontains'
608
-
609
- field_string + ' ILIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
610
-
611
- elsif comparator == 'is_null'
612
-
613
- 'NULL = ' + field_string
614
-
615
- elsif comparator == 'not_null'
616
-
617
- 'NOT NULL = ' + field_string
618
-
619
391
  end
620
392
 
393
+ filters = @model.fastapi_filters.clone.merge(filters)
621
394
  end
622
395
 
623
- def api_convert_type(val, type, is_array = false)
396
+ params = filters.has_key?(:__params) ? [*filters.delete(:__params)] : []
397
+ filters[:__order] ||= [:created_at, :DESC]
624
398
 
625
- return api_convert_value(val, type) unless is_array
399
+ filters.each do |key, value|
626
400
 
627
- return parse_postgres_array(val).map { |inner_value| api_convert_value(inner_value, type) }
401
+ next if [:__order, :__offset, :__count, :__params].include?(key)
628
402
 
629
- end
403
+ found_index = key.to_s.rindex('__')
404
+ key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
630
405
 
631
- def api_convert_value(val, type)
632
-
633
- if not val.nil?
634
- if type == :integer
635
- val = val.to_i
636
- elsif type == :float
637
- val = val.to_f
638
- elsif type == :boolean
639
- val = {
640
- 't' => true,
641
- 'f' => false
642
- }[val]
406
+ if !self_obj.column_names.include?(key_root.to_s)
407
+ if !model.nil? || !(@model.reflect_on_all_associations(:has_many).map(&:name).include?(key_root) ||
408
+ @model.reflect_on_all_associations(:belongs_to).map(&:name).include?(key_root) ||
409
+ @model.reflect_on_all_associations(:has_one).map(&:name).include?(key_root))
410
+ fail %(Filter "#{key}" not supported)
643
411
  end
644
412
  end
645
-
646
- val
647
-
648
413
  end
649
414
 
650
- def parse_filters(filters, safe = false, model = nil)
651
415
 
652
- self_obj = model.nil? ? @model : model
653
- self_string_table = model.nil? ? @model.to_s.tableize : '__' + model.to_s.tableize
416
+ filter_array = []
417
+ filter_has_many = {}
418
+ filter_belongs_to = {}
654
419
 
655
- filters = filters.clone().symbolize_keys
656
- # if we're at the top level...
657
- if model.nil?
658
-
659
- if safe
660
- filters.each do |key, value|
661
- found_index = key.to_s.rindex('__')
662
- key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
663
- if not [:__order, :__offset, :__count].include? key and not self_obj.fastapi_filters_whitelist.include? key_root
664
- raise 'Filter "' + key.to_s + '" not supported'
665
- end
666
- end
667
- end
668
-
669
- all_filters = @model.fastapi_filters.clone()
670
-
671
- filters.each do |field, value|
672
- all_filters[field.to_sym] = value
673
- end
674
-
675
- filters = all_filters
676
-
677
- end
678
-
679
- if not filters.has_key? :__order
680
- filters[:__order] = [:created_at, :DESC]
681
- end
420
+ order = nil
421
+ order_has_many = {}
422
+ order_belongs_to = {}
682
423
 
683
- params = []
424
+ # get the order first
684
425
 
685
- if filters.has_key? :__params
686
- params = filters[:__params]
687
- filters.delete :__params
688
- end
426
+ if filters.has_key?(:__order)
689
427
 
690
- if not params.is_a? Array and not params.is_a? Hash
691
- params = [params]
692
- end
428
+ value = filters.delete(:__order)
693
429
 
694
- filters.each do |key, value|
430
+ order = value.clone()
695
431
 
696
- if [:__order, :__offset, :__count, :__params].include? key
697
- next
432
+ if order.is_a?(String)
433
+ order = order.split(',')
434
+ if order.size < 2
435
+ order << 'ASC'
698
436
  end
699
-
700
- found_index = key.to_s.rindex('__')
701
- key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
702
-
703
- if not self_obj.column_names.include? key_root.to_s
704
- if not model.nil? or not (
705
- @model.reflect_on_all_associations(:has_many).map(&:name).include? key_root or
706
- @model.reflect_on_all_associations(:belongs_to).map(&:name).include? key_root or
707
- @model.reflect_on_all_associations(:has_one).map(&:name).include? key_root
708
- )
709
- raise 'Filter "' + key.to_s + '" not supported'
710
- end
437
+ elsif order.is_a?(Array)
438
+ order = order.map { |v| v.to_s }
439
+ while order.size < 2
440
+ order << ''
711
441
  end
712
-
442
+ else
443
+ order = ['', '']
713
444
  end
714
445
 
446
+ order[1] = 'ASC' if ['ASC', 'DESC'].exclude?(order[1])
715
447
 
716
- filter_array = []
717
- filter_has_many = {}
718
- filter_belongs_to = {}
719
-
720
- order = nil
721
- order_has_many = {}
722
- order_belongs_to = {}
723
-
724
- # get the order first
448
+ if model.nil? && @model.fastapi_custom_order.has_key?(order[0].to_sym)
725
449
 
726
- if filters.has_key? :__order
450
+ order[0] = @model.fastapi_custom_order[order[0].to_sym].gsub('self.', self_string_table + '.')
727
451
 
728
- value = filters[:__order]
729
-
730
- order = value.clone()
731
-
732
- if order.is_a? String
733
- order = order.split(',')
734
- if order.size < 2
735
- order << 'ASC'
736
- end
737
- elsif order.is_a? Array
738
- order = order.map { |v| v.to_s }
739
- while order.size < 2
740
- order << ''
741
- end
452
+ if params.is_a?(Array)
453
+ order[0].gsub!(/\$params\[([\w-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1].to_i].to_s) }
742
454
  else
743
- order = ['', '']
455
+ order[0].gsub!(/\$params\[([\w-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1]].to_s) }
744
456
  end
745
457
 
746
- if not ['ASC', 'DESC'].include? order[1]
747
- order[1] = 'ASC'
748
- end
749
-
750
- if model.nil? and @model.fastapi_custom_order.has_key? order[0].to_sym
751
-
752
- order[0] = @model.fastapi_custom_order[order[0].to_sym].gsub('self.', self_string_table + '.')
753
-
754
- if params.is_a? Array
755
-
756
- order[0] = order[0].gsub(/\$params\[([\w\d_-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1].to_i].to_s) }
757
-
758
- else
759
-
760
- order[0] = order[0].gsub(/\$params\[([\w\d_-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1]].to_s) }
761
-
762
- end
763
-
764
- order[0] = '(' + order[0] + ')'
765
- order = order.join(' ')
458
+ order[0] = "(#{order[0]})"
459
+ order = order.join(' ')
460
+ else
766
461
 
462
+ if self_obj.column_names.exclude?(order[0])
463
+ order = nil
767
464
  else
768
-
769
- if not self_obj.column_names.include? order[0]
770
-
771
- order = nil
772
-
773
- else
774
-
775
- order[0] = self_string_table + '.' + order[0]
776
- order = order.join(' ')
777
-
778
- end
779
-
465
+ order[0] = "#{self_string_table}.#{order[0]}"
466
+ order = order.join(' ')
780
467
  end
781
-
782
- filters.delete :__order
783
-
784
468
  end
469
+ end
785
470
 
786
- if filters.size > 0
787
-
788
- filters.each do |key, value|
789
-
790
- field = key.to_s
791
-
792
- if field.rindex('__').nil?
793
-
794
- comparator = 'is'
471
+ if filters.size > 0
795
472
 
796
- else
473
+ filters.each do |key, data|
474
+ field = key.to_s
797
475
 
798
- comparator = field[(field.rindex('__') + 2)..-1]
799
- field = field[0...field.rindex('__')]
476
+ if field.rindex('__').nil?
477
+ comparator = 'is'
478
+ else
800
479
 
801
- if not @@api_comparator_list.include? comparator
802
- next # skip dis bro
803
- end
480
+ comparator = field[(field.rindex('__') + 2)..-1]
481
+ field = field[0...field.rindex('__')]
804
482
 
483
+ if @@api_comparator_list.exclude?(comparator)
484
+ next # skip dis bro
805
485
  end
486
+ end
806
487
 
807
- if model.nil? and (self_obj.reflect_on_all_associations(:has_many).map(&:name).include? key)
488
+ if model.nil? && self_obj.reflect_on_all_associations(:has_many).map(&:name).include?(key)
808
489
 
809
- filter_result = parse_filters(value, safe, field.singularize.classify.constantize)
810
- # puts filter_result
811
- filter_has_many[key] = filter_result[:main]
812
- order_has_many[key] = filter_result[:main_order]
490
+ filter_result = parse_filters(data, safe, field.singularize.classify.constantize)
491
+ filter_has_many[key] = filter_result[:main]
492
+ order_has_many[key] = filter_result[:main_order]
813
493
 
814
- elsif model.nil? and (self_obj.reflect_on_all_associations(:belongs_to).map(&:name).include? key or
815
- self_obj.reflect_on_all_associations(:has_one).map(&:name).include? key)
494
+ elsif model.nil? && (self_obj.reflect_on_all_associations(:belongs_to).map(&:name).include?(key) ||
495
+ self_obj.reflect_on_all_associations(:has_one).map(&:name).include?(key))
816
496
 
817
- filter_result = parse_filters(value, safe, field.singularize.classify.constantize)
818
- # puts filter_result
819
- filter_belongs_to[key] = filter_result[:main]
820
- order_belongs_to[key] = filter_result[:main_order]
497
+ filter_result = parse_filters(data, safe, field.singularize.classify.constantize)
498
+ filter_belongs_to[key] = filter_result[:main]
499
+ order_belongs_to[key] = filter_result[:main_order]
821
500
 
822
- elsif self_obj.column_names.include? field
501
+ elsif self_obj.column_names.include?(field)
823
502
 
824
- base_field = self_string_table + '.' + field
825
- field_string = base_field
826
- is_array = false
503
+ base_field = "#{self_string_table}.#{field}"
504
+ field_string = base_field
505
+ is_array = false
827
506
 
828
- if self_obj.columns_hash[field].respond_to?('array') and self_obj.columns_hash[field].array == true
507
+ if self_obj.columns_hash[field].respond_to?('array') && self_obj.columns_hash[field].array == true
508
+ field_string = "ANY(#{field_string})"
509
+ is_array = true
510
+ end
829
511
 
830
- field_string = 'ANY(' + field_string + ')'
831
- is_array = true
512
+ if self_obj.columns_hash[field].type == :boolean
832
513
 
514
+ # if data is not a boolean
515
+ if !!data != data
516
+ data = ['f', 'false'].include?(data) ? false : true
833
517
  end
834
518
 
835
- if self_obj.columns_hash[field].type == :boolean
836
-
837
- if !!value != value
838
-
839
- bool_lookup = {
840
- 't' => true,
841
- 'f' => false,
842
- 'true' => true,
843
- 'false' => false
844
- }
845
-
846
- value = value.to_s.downcase
847
-
848
- if bool_lookup.has_key? value
849
- value = bool_lookup[value]
850
- else
851
- value = true
852
- end
853
-
854
- end
855
-
856
- if !!value == value
857
-
858
- if comparator == 'is'
859
- filter_array << value.to_s.upcase + ' = ' + field_string
860
- elsif comparator == 'not'
861
- filter_array << 'NOT ' + value.to_s.upcase + ' = ' + field_string
862
- end
863
-
864
- end
865
-
866
- elsif value == nil and comparator != 'is_null' and comparator != 'not_null'
867
-
868
- if comparator == 'is'
869
- filter_array << 'NULL = ' + field_string
870
- elsif comparator == 'not'
871
- filter_array << 'NOT NULL = ' + field_string
872
- end
873
-
874
- elsif value.is_a? Range and comparator == 'is'
875
-
876
- filter_array << ActiveRecord::Base.connection.quote(value.first.to_s) + ' <= ' + field_string
877
- filter_array << ActiveRecord::Base.connection.quote(value.last.to_s) + ' >= ' + field_string
878
-
879
- else
880
-
881
- filter_array << api_comparison(comparator, value, base_field, self_obj.columns_hash[field].type, is_array)
519
+ if comparator == 'is'
520
+ filter_array << "#{data.to_s.upcase} = #{field_string}"
521
+ elsif comparator == 'not'
522
+ filter_array << "NOT #{data.to_s.upcase} = #{field_string}"
523
+ end
882
524
 
525
+ elsif data == nil && comparator != 'is_null' && comparator != 'not_null'
526
+ if comparator == 'is'
527
+ filter_array << "#{field_string} IS NULL"
528
+ elsif comparator == 'not'
529
+ filter_array << "#{field_string} IS NOT NULL"
883
530
  end
884
531
 
532
+ elsif data.is_a?(Range) && comparator == 'is'
533
+ filter_array << "#{ActiveRecord::Base.connection.quote(data.first.to_s)} <= #{field_string}"
534
+ filter_array << "#{ActiveRecord::Base.connection.quote(data.last.to_s)} >= #{field_string}"
535
+ else
536
+ filter_array << api_comparison(comparator, data, base_field, self_obj.columns_hash[field].type, is_array)
885
537
  end
886
-
887
538
  end
888
-
889
539
  end
540
+ end
890
541
 
891
- {
892
- main: filter_array,
893
- main_order: order,
894
- has_many: filter_has_many,
895
- has_many_order: order_has_many,
896
- belongs_to: filter_belongs_to,
897
- belongs_to_order: order_belongs_to
898
- }
542
+ {
543
+ main: filter_array,
544
+ main_order: order,
545
+ has_many: filter_has_many,
546
+ has_many_order: order_has_many,
547
+ belongs_to: filter_belongs_to,
548
+ belongs_to_order: order_belongs_to
549
+ }
550
+ end
899
551
 
900
- end
552
+ def api_generate_sql(filters, offset, count, safe = false)
901
553
 
902
- def api_generate_sql(filters, offset, count, safe = false)
554
+ filters = parse_filters(filters, safe)
903
555
 
904
- filters = parse_filters(filters, safe)
556
+ belongs = []
557
+ has_many = []
905
558
 
906
- fields = []
907
- belongs = []
908
- has_many = []
559
+ model_lookup = {}
909
560
 
910
- model_lookup = {}
561
+ filter_fields = []
562
+ filter_fields.concat(@model.fastapi_fields)
563
+ filter_fields.concat(@whitelist_fields)
911
564
 
912
- filter_fields = []
913
- filter_fields.concat @model.fastapi_fields
914
- filter_fields.concat @whitelist_fields
565
+ fields = filter_fields.each_with_object([]) do |field, field_list|
566
+ if @model.reflect_on_all_associations(:belongs_to).map(&:name).include?(field)
567
+ class_name = @model.reflect_on_association(field).options[:class_name]
915
568
 
916
- filter_fields.each do |field|
569
+ if class_name
570
+ model = class_name.constantize
571
+ else
572
+ model = field.to_s.classify.constantize
573
+ end
917
574
 
918
- if (@model.reflect_on_all_associations(:belongs_to).map(&:name).include? field or
919
- @model.reflect_on_all_associations(:has_one).map(&:name).include? field)
575
+ model_lookup[field] = model
576
+ belongs << { model: model, alias: field, type: :belongs_to }
920
577
 
921
- class_name = @model.reflect_on_association(field).options[:class_name]
578
+ elsif @model.reflect_on_all_associations(:has_one).map(&:name).include?(field)
922
579
 
923
- if class_name.nil?
924
- model = field.to_s.classify.constantize
925
- else
926
- model = class_name.constantize
927
- end
580
+ class_name = @model.reflect_on_association(field).options[:class_name]
928
581
 
929
- model_lookup[field] = model
930
- belongs << {model: model, alias: field}
582
+ if class_name
583
+ model = class_name.constantize
584
+ else
585
+ model = field.to_s.classify.constantize
586
+ end
931
587
 
932
- elsif @model.reflect_on_all_associations(:has_many).map(&:name).include? field
588
+ model_lookup[field] = model
933
589
 
934
- model = field.to_s.singularize.classify.constantize
935
- model_lookup[field] = model
936
- has_many << model
590
+ belongs << { model: model, alias: field, type: :has_one }
937
591
 
938
- elsif @model.column_names.include? field.to_s
592
+ elsif @model.reflect_on_all_associations(:has_many).map(&:name).include?(field)
939
593
 
940
- fields << field
594
+ model = field.to_s.singularize.classify.constantize
595
+ model_lookup[field] = model
596
+ has_many << model
941
597
 
942
- end
598
+ elsif @model.column_names.include?(field.to_s)
943
599
 
600
+ field_list << field
944
601
  end
602
+ end
945
603
 
946
- self_string = @model.to_s.tableize.singularize
947
- self_string_table = @model.to_s.tableize
604
+ self_string = @model.to_s.tableize.singularize
605
+ self_string_table = @model.to_s.tableize
948
606
 
949
- field_list = []
950
- joins = []
607
+ # Base fields
608
+ field_list = fields.each_with_object([]) do |field, list|
609
+ if @model.columns_hash[field.to_s].array
610
+ list << "ARRAY_TO_JSON(#{self_string_table}.#{field}) AS #{field}"
611
+ else
612
+ list << "#{self_string_table}.#{field} AS #{field}"
613
+ end
614
+ end
951
615
 
952
- # array_to_string: (ActiveRecord::Base.connection.instance_of? ActiveRecord::ConnectionAdapters::SQLite3Adapter) ? 'GROUP_CONCAT' : 'ARRAY_TO_STRING',
616
+ # Belongs fields (1 to 1)
617
+ joins = belongs.each_with_object([]) do |model_data, join_list|
953
618
 
954
- # Base fields
955
- fields.each do |field|
619
+ model_string_table = model_data[:model].to_s.tableize
620
+ model_string_table_alias = model_data[:alias].to_s.pluralize
956
621
 
957
- field_string = field.to_s
958
- field_list << [
959
- self_string_table,
960
- '.',
961
- field_string,
962
- ' as ',
963
- field_string
964
- ].join('')
622
+ model_string_field = model_data[:alias].to_s
623
+ singular_self_table = self_string_table.singularize
965
624
 
966
- end
967
-
968
- # Belongs fields (1 to 1)
969
- belongs.each do |model_data|
970
-
971
- model_string_table = model_data[:model].to_s.tableize
972
- model_string_table_alias = model_data[:alias].to_s.pluralize
973
-
974
- model_string_field = model_data[:alias].to_s
975
-
976
- # fields
977
- model_data[:model].fastapi_fields_sub.each do |field|
978
- field_string = field.to_s
979
- field_list << [
980
- model_string_table_alias,
981
- '.',
982
- field_string,
983
- ' as ',
984
- model_string_field,
985
- '__',
986
- field_string
987
- ].join('')
625
+ model_data[:model].fastapi_fields_sub.each do |field|
626
+ if model_data[:model].columns_hash[field.to_s].array
627
+ field_list << "ARRAY_TO_JSON(#{model_string_table_alias}.#{field}) AS #{model_string_field}__#{field}"
628
+ else
629
+ field_list << "#{model_string_table_alias}.#{field} AS #{model_string_field}__#{field}"
988
630
  end
631
+ end
989
632
 
633
+ # fields
634
+ if model_data[:type] == :belongs_to
990
635
  # joins
991
- joins << [
992
- 'LEFT JOIN',
993
- model_string_table,
994
- 'AS',
995
- model_string_table_alias,
996
- 'ON',
997
- model_string_table_alias + '.id',
998
- '=',
999
- self_string_table + '.' + model_string_field + '_id'
1000
- ].join(' ')
1001
-
636
+ join_list << "LEFT JOIN #{model_string_table} AS #{model_string_table_alias} " \
637
+ "ON #{model_string_table_alias}.id = #{self_string_table}.#{model_string_field}_id"
638
+ elsif model_data[:type] == :has_one
639
+ join_list << "LEFT JOIN #{model_string_table} AS #{model_string_table_alias} " \
640
+ "ON #{model_string_table_alias}.#{singular_self_table}_id = #{self_string_table}.id"
1002
641
  end
642
+ end
1003
643
 
1004
- # Many fields (Many to 1)
1005
- has_many.each do |model|
644
+ # Many fields (1 to many)
645
+ has_many.each do |model|
1006
646
 
1007
- model_string = model.to_s.tableize.singularize
1008
- model_string_table = model.to_s.tableize
1009
- model_symbol = model_string_table.to_sym
647
+ model_string_table = model.to_s.tableize
648
+ model_symbol = model_string_table.to_sym
1010
649
 
1011
- model_fields = []
650
+ model_fields = model.fastapi_fields_sub.each_with_object([]) do |field, m_fields|
651
+ m_fields << "__#{model_string_table}.#{field}"
652
+ end
1012
653
 
1013
- model.fastapi_fields_sub.each do |field|
1014
- field_string = field.to_s
1015
- model_fields << [
1016
- '__' + model_string_table + '.' + field_string,
1017
- # 'as',
1018
- # field_string
1019
- ].join(' ')
654
+ if filters[:has_many].has_key?(model_symbol)
655
+ if filters[:has_many][model_symbol].count > 0
656
+ has_many_filters = "AND #{filters[:has_many][model_symbol].join(' AND ')}"
657
+ else
658
+ has_many_filters = nil
1020
659
  end
1021
660
 
1022
- has_many_filters = ''
1023
- has_many_order = ''
1024
- if filters[:has_many].has_key? model_symbol
1025
-
1026
- if filters[:has_many][model_symbol].count > 0
1027
- has_many_filters = 'AND ' + filters[:has_many][model_symbol].join(' AND ')
1028
- end
1029
-
1030
- if not filters[:has_many_order][model_symbol].nil?
1031
- has_many_order = 'ORDER BY ' + filters[:has_many_order][model_symbol]
1032
- end
1033
-
661
+ if filters[:has_many_order][model_symbol]
662
+ has_many_order = "ORDER BY #{filters[:has_many_order][model_symbol]}"
663
+ else
664
+ has_many_filters = nil
1034
665
  end
1035
-
1036
- field_list << [
1037
- 'ARRAY_TO_STRING(ARRAY(',
1038
- 'SELECT',
1039
- 'ROW(',
1040
- model_fields.join(', '),
1041
- ')',
1042
- 'FROM',
1043
- model_string_table,
1044
- 'as',
1045
- '__' + model_string_table,
1046
- 'WHERE',
1047
- '__' + model_string_table + '.' + self_string + '_id IS NOT NULL',
1048
- 'AND __' + model_string_table + '.' + self_string + '_id',
1049
- '=',
1050
- self_string_table + '.id',
1051
- has_many_filters,
1052
- has_many_order,
1053
- '), \',\')',
1054
- 'as',
1055
- '__many__' + model_string_table
1056
- ].join(' ')
1057
-
1058
666
  end
1059
667
 
1060
- filter_string = (filters[:main].size > 0 ? ('WHERE ' + filters[:main].join(' AND ')) : '')
1061
- order_string = (filters[:main_order].nil? ? '' : 'ORDER BY ' + filters[:main_order])
1062
-
1063
- {
1064
- query: [
1065
- 'SELECT',
1066
- field_list.join(', '),
1067
- 'FROM',
1068
- self_string_table,
1069
- joins.join(' '),
1070
- filter_string,
1071
- order_string,
1072
- 'LIMIT',
1073
- count.to_s,
1074
- 'OFFSET',
1075
- offset.to_s,
1076
- ].join(' '),
1077
- count_query: [
1078
- 'SELECT COUNT(id) FROM',
1079
- self_string_table,
1080
- filter_string
1081
- ].join(' '),
1082
- models: model_lookup
1083
- }
1084
-
668
+ field_list << [
669
+ "ARRAY_TO_JSON(ARRAY(SELECT ROW(#{model_fields.join(', ')})",
670
+ "FROM #{model_string_table}",
671
+ "AS __#{model_string_table}",
672
+ "WHERE __#{model_string_table}.#{self_string}_id IS NOT NULL",
673
+ "AND __#{model_string_table}.#{self_string}_id",
674
+ "= #{self_string_table}.id",
675
+ has_many_filters,
676
+ has_many_order,
677
+ ")) AS __many__#{model_string_table}"
678
+ ].compact.join(' ')
1085
679
  end
1086
680
 
681
+ filter_string = filters[:main].size > 0 ? "WHERE #{filters[:main].join(' AND ')}" : nil
682
+ order_string = filters[:main_order] ? "ORDER BY #{filters[:main_order]}" : nil
683
+
684
+ {
685
+ query: [
686
+ "SELECT #{field_list.join(', ')}",
687
+ "FROM #{self_string_table}",
688
+ joins.join(' '),
689
+ filter_string,
690
+ order_string,
691
+ "LIMIT #{count}",
692
+ "OFFSET #{offset}"
693
+ ].compact.join(' '),
694
+ count_query: [
695
+ "SELECT COUNT(id) FROM #{self_string_table}",
696
+ filter_string
697
+ ].compact.join(' '),
698
+ models: model_lookup
699
+ }
700
+ end
1087
701
  end