fastapi 0.1.25 → 0.1.26

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
  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