fastapi 0.1.27 → 0.2.0

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: 7043a3b2bde04b33d7375e944d36acc522663792
4
- data.tar.gz: 974c677ed00ef6948df0cff76c09b64e41ed5638
3
+ metadata.gz: a39886af522fab889ef06e2c54bdee7f323df410
4
+ data.tar.gz: abf090555863c01150f1562e6c596390a346fb2b
5
5
  SHA512:
6
- metadata.gz: b7621446aff6279bc34f19a016cf57a5083b63c2c88a920624b289a58742f428e33c151ea508f6e38ce3c28907a3d574aedfc34ffbaaa5f63ca2c483062ed810
7
- data.tar.gz: 1400d2bfc88b06a9da1f2baa48493987faa2abad1fd424f3443b27f33099f94d8a3de51a98778aed90be8463ba15fcf2c6cf72fe4fea1072847449613a43a23e
6
+ metadata.gz: 7a27ad46c34689bffa30355e4432019d858473c8ba581c3dc0fecf149075eb4098cbbb681ba52dba670d4080893d235cce491913eee8228745caf1aad471b465
7
+ data.tar.gz: be109289004c0c3315f9e05d4fddab3ec643fb8ddeb56ee25047b91090bba50d025a4352abcf35d7f2d4e03d0db8396716c2aa4a1b5aedfee37be6148bd4c35d
data/lib/fastapi.rb CHANGED
@@ -1,488 +1,367 @@
1
1
  require 'oj'
2
- require 'fastapi/active_record_extension'
3
-
4
- class FastAPI
5
-
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
- )
22
-
23
- def initialize(model)
24
- @model = model
25
- @data = nil
26
- @metadata = nil
27
- @result_type = 0
28
- @whitelist_fields = []
29
- end
30
-
31
- def inspect
32
- "<#{self.class}: #{@model}>"
33
- end
34
-
35
- # Create and execute an optimized SQL query based on specified filters
36
- #
37
- # @param fields [Array] an array containing fields to whitelist for the SQL query. Can also pass in fields as arguments.
38
- # @return [FastAPI] the current instance
39
- def whitelist(fields = [])
40
- @whitelist_fields.concat(fields)
41
-
42
- self
43
- end
44
-
45
- # Create and execute an optimized SQL query based on specified filters
46
- #
47
- # @param filters [Hash] a hash containing the intended filters
48
- # @param meta [Hash] a hash containing custom metadata
49
- # @return [FastAPI] the current instance
50
- def filter(filters = {}, meta = {}, safe = false)
51
- result = fastapi_query(filters, safe)
52
-
53
- @metadata = meta.merge(result.slice(:total, :offset, :count, :error))
54
- @data = result[:data]
55
- @result_type = @@result_types[:multiple]
56
-
57
- self
58
- end
59
-
60
- # Create and execute an optimized SQL query based on specified filters.
61
- # Runs through mode fastapi_safe_fields list
62
- #
63
- # @param filters [Hash] a hash containing the intended filters
64
- # @param meta [Hash] a hash containing custom metadata
65
- # @return [FastAPI] the current instance
66
- def safe_filter(filters = {}, meta = {})
67
- filter(filters, meta, true)
68
- end
69
-
70
- # Create and execute an optimized SQL query based on specified object id.
71
- # Provides customized error response if not found.
72
- #
73
- # @param id [Integer] the id of the object to retrieve
74
- # @param meta [Hash] a hash containing custom metadata
75
- # @return [FastAPI] the current instance
76
- def fetch(id, meta = {})
77
- filter({ id: id }, meta)
78
-
79
- if @metadata[:total].zero?
80
- @metadata[:error] = { message: "#{@model} id does not exist" }
2
+ require 'active_record'
3
+ require 'active_support'
4
+ require 'fastapi/extension'
5
+ require 'fastapi/comparison'
6
+ require 'fastapi/conversions'
7
+ require 'fastapi/sql'
8
+ require 'fastapi/utilities'
9
+
10
+ module FastAPI
11
+
12
+ Oj.default_options = { mode: :compat }
13
+
14
+ class Wrapper
15
+ include FastAPI::Utilities
16
+
17
+ def initialize(model)
18
+ @model = model
19
+ @data = nil
20
+ @metadata = nil
21
+ @whitelist_fields = []
81
22
  end
82
23
 
83
- self
84
- end
85
-
86
- # Returns the data from the most recently executed `filter` or `fetch` call.
87
- #
88
- # @return [Array] available data
89
- def data
90
- @data
91
- end
92
-
93
- # Returns JSONified data from the most recently executed `filter` or `fetch` call.
94
- #
95
- # @return [String] available data in JSON format
96
- def data_json
97
- Oj.dump(@data, mode: :compat)
98
- end
99
-
100
- # Returns the metadata from the most recently executed `filter` or `fetch` call.
101
- #
102
- # @return [Hash] available metadata
103
- def meta
104
- @metadata
105
- end
106
-
107
- # Returns JSONified metadata from the most recently executed `filter` or `fetch` call.
108
- #
109
- # @return [String] available metadata in JSON format
110
- def meta_json
111
- Oj.dump(@metadata, mode: :compat)
112
- end
113
-
114
- # Returns both the data and metadata from the most recently executed `filter` or `fetch` call.
115
- #
116
- # @return [Hash] available data and metadata
117
- def to_hash
118
- { meta: @metadata, data: @data }
119
- end
120
-
121
- # Intended to return the final API response
122
- #
123
- # @return [String] JSON data and metadata
124
- def response
125
- Oj.dump(self.to_hash, mode: :compat)
126
- end
127
-
128
- # Spoofs data from Model
129
- #
130
- # @return [String] JSON data and metadata
131
- def spoof(data = [], meta = {})
132
- meta[:total] ||= data.count
133
- meta[:count] ||= data.count
134
- meta[:offset] ||= 0
24
+ def inspect
25
+ "<#{self.class}: #{@model}>"
26
+ end
135
27
 
136
- Oj.dump({ meta: meta, data: data }, mode: :compat)
137
- end
28
+ # Create and execute an optimized SQL query based on specified filters
29
+ #
30
+ # @param fields [Array] an array containing fields to whitelist for the SQL query. Can also pass in fields as arguments.
31
+ # @return [FastAPI] the current instance
32
+ def whitelist(fields = [])
33
+ @whitelist_fields.concat(fields)
138
34
 
139
- # Returns a JSONified string representing a rejected API response with invalid fields parameters
140
- #
141
- # @param fields [Hash] Hash containing fields and their related errors
142
- # @return [String] JSON data and metadata, with error
143
- def invalid(fields)
144
- Oj.dump({
145
- meta: {
146
- total: 0,
147
- offset: 0,
148
- count: 0,
149
- error: {
150
- message: 'invalid',
151
- fields: fields
152
- }
153
- },
154
- data: []
155
- }, mode: :compat)
156
- end
35
+ self
36
+ end
157
37
 
158
- # Returns a JSONified string representing a standardized empty API response, with a provided error message
159
- #
160
- # @param message [String] Error message to be used in response
161
- # @return [String] JSON data and metadata, with error
162
- def reject(message = 'Access denied')
163
- Oj.dump({
164
- meta: {
165
- total: 0,
166
- offset: 0,
167
- count: 0,
168
- error: {
169
- message: message.to_s
170
- }
171
- },
172
- data: []
173
- }, mode: :compat)
174
- end
38
+ # Create and execute an optimized SQL query based on specified filters
39
+ #
40
+ # @param filters [Hash] a hash containing the intended filters
41
+ # @param meta [Hash] a hash containing custom metadata
42
+ # @return [FastAPI] the current instance
43
+ def filter(filters = {}, meta = {}, safe = false)
44
+ result = fastapi_query(filters, safe)
175
45
 
176
- private
46
+ @metadata = meta.merge(result.slice(:total, :offset, :count, :error))
47
+ @data = result[:data]
177
48
 
178
- def clamp(value, min, max)
179
- [min, value, max].sort[1]
180
- end
49
+ self
50
+ end
181
51
 
182
- def error(offset, message)
183
- { data: [], total: 0, count: 0, offset: offset, error: { message: message } }
184
- end
52
+ # Create and execute an optimized SQL query based on specified filters.
53
+ # Runs through mode fastapi_safe_fields list
54
+ #
55
+ # @param filters [Hash] a hash containing the intended filters
56
+ # @param meta [Hash] a hash containing custom metadata
57
+ # @return [FastAPI] the current instance
58
+ def safe_filter(filters = {}, meta = {})
59
+ filter(filters, meta, true)
60
+ end
185
61
 
186
- def fastapi_query(filters = {}, safe = false)
62
+ # Create and execute an optimized SQL query based on specified object id.
63
+ # Provides customized error response if not found.
64
+ #
65
+ # @param id [Integer] the id of the object to retrieve
66
+ # @param meta [Hash] a hash containing custom metadata
67
+ # @return [FastAPI] the current instance
68
+ def fetch(id, meta = {})
69
+ filter({ id: id }, meta)
70
+
71
+ if @metadata[:total].zero?
72
+ @metadata[:error] = { message: "#{@model} id does not exist" }
73
+ end
187
74
 
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.'
75
+ self
191
76
  end
192
77
 
193
- offset = filters.delete(:__offset).try(:to_i) || 0
194
- cnt = filters.delete(:__count).try(:to_i) || 500
195
- count = clamp(cnt, 1, 500)
196
-
197
- begin
198
- prepared_data = api_generate_sql(filters, offset, count, safe)
199
- rescue StandardError => exception
200
- return error(offset, exception.message)
78
+ # Returns the data from the most recently executed `filter` or `fetch` call.
79
+ #
80
+ # @return [Array] available data
81
+ def data
82
+ @data
201
83
  end
202
84
 
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
- }
85
+ # Returns JSONified data from the most recently executed `filter` or `fetch` call.
86
+ #
87
+ # @return [String] available data in JSON format
88
+ def data_json
89
+ Oj.dump(@data)
210
90
  end
211
91
 
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
- return error(offset, 'Query failed')
92
+ # Returns the metadata from the most recently executed `filter` or `fetch` call.
93
+ #
94
+ # @return [Hash] available metadata
95
+ def meta
96
+ @metadata
217
97
  end
218
98
 
219
- total_size = count_result.values.size > 0 ? count_result.values[0][0].to_i : 0
99
+ # Returns JSONified metadata from the most recently executed `filter` or `fetch` call.
100
+ #
101
+ # @return [String] available metadata in JSON format
102
+ def meta_json
103
+ Oj.dump(@metadata)
104
+ end
220
105
 
221
- fields = result.fields
222
- rows = result.values
106
+ # Returns both the data and metadata from the most recently executed `filter` or `fetch` call.
107
+ #
108
+ # @return [Hash] available data and metadata
109
+ def to_hash
110
+ { meta: @metadata, data: @data }
111
+ end
223
112
 
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('__')
113
+ # Intended to return the final API response
114
+ #
115
+ # @return [String] JSON data and metadata
116
+ def response
117
+ Oj.dump(self.to_hash)
118
+ end
228
119
 
229
- if field[0..7] == '__many__'
120
+ # Spoofs data from Model
121
+ #
122
+ # @return [String] JSON data and metadata
123
+ def spoof(data = [], meta = {})
124
+ meta[:total] ||= data.count
125
+ meta[:count] ||= data.count
126
+ meta[:offset] ||= 0
230
127
 
231
- field = field[8..-1]
232
- field_sym = field.to_sym
233
- model = model_lookup[field_sym]
128
+ Oj.dump({ meta: meta, data: data })
129
+ end
234
130
 
235
- current[field_sym] = parse_many(val, model[:fields], model[:types])
131
+ # Returns a JSONified string representing a rejected API response with invalid fields parameters
132
+ #
133
+ # @param fields [Hash] Hash containing fields and their related errors
134
+ # @return [String] JSON data and metadata, with error
135
+ def invalid(fields)
136
+ Oj.dump({
137
+ meta: {
138
+ total: 0,
139
+ offset: 0,
140
+ count: 0,
141
+ error: {
142
+ message: 'invalid',
143
+ fields: fields
144
+ }
145
+ },
146
+ data: []
147
+ })
148
+ end
236
149
 
237
- elsif split_index
150
+ # Returns a JSONified string representing a standardized empty API response, with a provided error message
151
+ #
152
+ # @param message [String] Error message to be used in response
153
+ # @return [String] JSON data and metadata, with error
154
+ def reject(message = 'Access denied')
155
+ Oj.dump({
156
+ meta: {
157
+ total: 0,
158
+ offset: 0,
159
+ count: 0,
160
+ error: {
161
+ message: message
162
+ }
163
+ },
164
+ data: []
165
+ })
166
+ end
238
167
 
239
- obj_name = field[0..split_index - 1].to_sym
240
- field = field[split_index + 2..-1]
241
- model = model_lookup[obj_name][:model]
168
+ private
169
+ def error(offset, message)
170
+ { data: [], total: 0, count: 0, offset: offset, error: { message: message } }
171
+ end
242
172
 
243
- current[obj_name] ||= {}
173
+ def fastapi_query(filters = {}, safe = false)
244
174
 
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))
175
+ unless ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQLAdapter) &&
176
+ ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
177
+ fail 'FastAPI only supports PostgreSQL at this time.'
178
+ end
248
179
 
249
- elsif @model.columns_hash[field]
180
+ offset = filters.delete(:__offset).try(:to_i) || 0
181
+ cnt = filters.delete(:__count).try(:to_i) || 500
182
+ count = clamp(cnt, 1, 500)
250
183
 
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))
254
- end
184
+ begin
185
+ parsed_filters = parse_filters(filters, safe)
186
+ prepared_data = FastAPI::SQL.new(parsed_filters, offset, count, @model, @whitelist_fields, safe)
187
+ rescue StandardError => exception
188
+ return error(offset, exception.message)
255
189
  end
256
- data << datum
257
- end
258
- { data: dataset, total: total_size, count: dataset.size, offset: offset, error: nil }
259
- end
260
190
 
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])
191
+ model_lookup = prepared_data[:models].each_with_object({}) do |(key, model), lookup|
192
+ columns = model.columns_hash
193
+ lookup[key] = {
194
+ model: model,
195
+ fields: model.fastapi_fields_sub,
196
+ types: model.fastapi_fields_sub.map { |field| columns[field.to_s].try(:type) }
197
+ }
265
198
  end
266
- end
267
- end
268
-
269
- def api_comparison(comparator, value, field, type, is_array)
270
- field_string = is_array ? "ANY(#{field})" : field
271
199
 
272
- if comparator == 'is'
273
-
274
- ActiveRecord::Base.connection.quote(value.to_s) + ' = ' + field_string
275
-
276
- elsif comparator == 'not'
277
-
278
- ActiveRecord::Base.connection.quote(value.to_s) + ' <> ' + field_string
200
+ begin
201
+ count_result = ActiveRecord::Base.connection.execute(prepared_data[:count_query])
202
+ result = ActiveRecord::Base.connection.execute(prepared_data[:query])
203
+ rescue StandardError
204
+ return error(offset, 'Query failed')
205
+ end
279
206
 
280
- elsif comparator == 'gt'
207
+ total_size = count_result.values.size > 0 ? count_result.values[0][0].to_i : 0
281
208
 
282
- ActiveRecord::Base.connection.quote(value.to_s) + ' < ' + field_string
209
+ fields = result.fields
210
+ rows = result.values
283
211
 
284
- elsif comparator == 'gte'
212
+ dataset = rows.each_with_object([]) do |row, data|
213
+ datum = row.each_with_object({}).with_index do |(val, current), index|
214
+ field = fields[index]
215
+ split_index = field.rindex('__')
285
216
 
286
- ActiveRecord::Base.connection.quote(value.to_s) + ' <= ' + field_string
217
+ if field[0..7] == '__many__'
287
218
 
288
- elsif comparator == 'lt'
219
+ field = field[8..-1]
220
+ field_sym = field.to_sym
221
+ model = model_lookup[field_sym]
289
222
 
290
- ActiveRecord::Base.connection.quote(value.to_s) + ' > ' + field_string
223
+ current[field_sym] = parse_many(val, model[:fields], model[:types])
291
224
 
292
- elsif comparator == 'lte'
225
+ elsif split_index
293
226
 
294
- ActiveRecord::Base.connection.quote(value.to_s) + ' >= ' + field_string
227
+ obj_name = field[0..split_index - 1].to_sym
228
+ field = field[split_index + 2..-1]
229
+ model = model_lookup[obj_name][:model]
295
230
 
296
- elsif comparator == 'in' || comparator == 'not_in'
231
+ current[obj_name] ||= {}
297
232
 
298
- unless value.is_a?(Array)
233
+ model_field = model.columns_hash[field]
234
+ current[obj_name][field.to_sym] = FastAPI::Conversions.convert_type(val, model_field.type, model_field)
299
235
 
300
- if value.is_a?(Range)
301
- value = value.to_a
302
- else
303
- value = [value.to_s]
236
+ elsif @model.columns_hash[field]
237
+ model_field = @model.columns_hash[field]
238
+ current[field.to_sym] = FastAPI::Conversions.convert_type(val, model_field.type, model_field)
239
+ end
304
240
  end
241
+ data << datum
305
242
  end
243
+ { data: dataset, total: total_size, count: dataset.size, offset: offset, error: nil }
244
+ end
306
245
 
307
- if is_array
308
-
309
- type_convert = {
310
- boolean: '::boolean',
311
- integer: '::integer',
312
- float: '::float',
313
- string: '::varchar'
314
- }[type]
315
-
316
- type_convert = '::text' if type.nil?
317
-
318
- if comparator == 'in'
319
- 'ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
320
- else
321
- 'NOT ARRAY[' + (value.map { |val| ActiveRecord::Base.connection.quote(val.to_s) }).join(',') + ']' + type_convert + '[] && ' + field
322
- end
323
- else
324
-
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(',') + ')'
246
+ def parse_many(str, fields, types)
247
+ Oj.load(str).map do |row|
248
+ row.values.each_with_object({}).with_index do |(value, values), index|
249
+ values[fields[index]] = FastAPI::Conversions.convert_type(value, types[index])
329
250
  end
330
251
  end
331
-
332
- elsif comparator == 'contains'
333
-
334
- field_string + ' LIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
335
-
336
- elsif comparator == 'icontains'
337
-
338
- field_string + ' ILIKE \'%\' || ' + ActiveRecord::Base.connection.quote(value.to_s) + ' || \'%\''
339
-
340
- elsif comparator == 'is_null'
341
-
342
- "#{field_string} IS NULL"
343
-
344
- elsif comparator == 'not_null'
345
-
346
- "#{field_string} IS NOT NULL"
347
252
  end
348
- end
349
253
 
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
254
+ def parse_filters(filters, safe = false, model = nil)
255
+ self_obj = model ? model : @model
256
+ self_string_table = model ? "__#{model.to_s.tableize}" : @model.to_s.tableize
357
257
 
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]
367
- else
368
- val
369
- end
370
- end
371
- end
258
+ filters = filters.with_indifferent_access
372
259
 
373
- def parse_filters(filters, safe = false, model = nil)
260
+ # if we're at the top level...
261
+ if model.nil?
374
262
 
375
- self_obj = model ? model : @model
376
- self_string_table = model ? "__#{model.to_s.tableize}" : @model.to_s.tableize
263
+ if safe
264
+ filters.each do |key, value|
377
265
 
378
- filters = filters.clone.symbolize_keys
379
- # if we're at the top level...
380
- if model.nil?
266
+ found_index = key.to_s.rindex('__')
267
+ key_root = (found_index ? key.to_s[0..found_index] : key).to_sym
381
268
 
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
269
+ if [:__order, :__offset, :__count, :__params].exclude?(key) && self_obj.fastapi_filters_whitelist.exclude?(key_root)
270
+ fail %(Filter "#{key}" not supported.)
271
+ end
386
272
 
387
- if [:__order, :__offset, :__count].exclude?(key) && self_obj.fastapi_filters_whitelist.exclude?(key_root)
388
- fail %(Filter "#{key}" not supported.)
389
273
  end
390
274
  end
391
- end
392
275
 
393
- filters = @model.fastapi_filters.clone.merge(filters)
394
- end
276
+ filters = @model.fastapi_filters.clone.merge(filters).with_indifferent_access
395
277
 
396
- params = filters.has_key?(:__params) ? [*filters.delete(:__params)] : []
397
- filters[:__order] ||= [:created_at, :DESC]
278
+ end
398
279
 
399
- filters.each do |key, value|
280
+ params = filters.has_key?(:__params) ? filters.delete(:__params) : []
400
281
 
401
- next if [:__order, :__offset, :__count, :__params].include?(key)
282
+ filters.each do |key, value|
402
283
 
403
- found_index = key.to_s.rindex('__')
404
- key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
284
+ key = key.to_sym
405
285
 
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)
411
- end
412
- end
413
- end
286
+ next if [:__order, :__offset, :__count, :__params].include?(key)
414
287
 
288
+ found_index = key.to_s.rindex('__')
289
+ key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
415
290
 
416
- filter_array = []
417
- filter_has_many = {}
418
- filter_belongs_to = {}
291
+ if !self_obj.column_names.include?(key_root.to_s)
292
+ if !model.nil? || !(@model.reflect_on_all_associations(:has_many).map(&:name).include?(key_root) ||
293
+ @model.reflect_on_all_associations(:belongs_to).map(&:name).include?(key_root) ||
294
+ @model.reflect_on_all_associations(:has_one).map(&:name).include?(key_root))
295
+ fail %(Filter "#{key}" not supported)
296
+ end
297
+ end
419
298
 
420
- order = nil
421
- order_has_many = {}
422
- order_belongs_to = {}
299
+ end
423
300
 
424
- # get the order first
301
+ filter_array = []
302
+ filter_has_many = {}
303
+ filter_belongs_to = {}
425
304
 
426
- if filters.has_key?(:__order)
305
+ order = nil
306
+ order_has_many = {}
307
+ order_belongs_to = {}
427
308
 
428
- value = filters.delete(:__order)
309
+ # get the order first
310
+ if filters.has_key?(:__order)
429
311
 
430
- order = value.clone()
312
+ order = filters.delete(:__order)
431
313
 
432
- if order.is_a?(String)
433
- order = order.split(',')
434
- if order.size < 2
435
- order << 'ASC'
436
- end
437
- elsif order.is_a?(Array)
438
- order = order.map { |v| v.to_s }
439
- while order.size < 2
440
- order << ''
314
+ if order.is_a?(String)
315
+ order = order.split(',')
316
+ if order.size < 2
317
+ order << 'ASC'
318
+ end
319
+ elsif order.is_a?(Array)
320
+ order = order.map(&:to_s)
321
+ while order.size < 2
322
+ order << ''
323
+ end
324
+ else
325
+ order = ['', '']
441
326
  end
442
- else
443
- order = ['', '']
444
- end
445
-
446
- order[1] = 'ASC' if ['ASC', 'DESC'].exclude?(order[1])
447
327
 
448
- if model.nil? && @model.fastapi_custom_order.has_key?(order[0].to_sym)
328
+ order[1] = 'ASC' if ['ASC', 'DESC'].exclude?(order[1])
449
329
 
450
- order[0] = @model.fastapi_custom_order[order[0].to_sym].gsub('self.', self_string_table + '.')
330
+ if model.nil? && @model.fastapi_custom_order.has_key?(order[0].to_sym)
451
331
 
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) }
454
- else
455
- order[0].gsub!(/\$params\[([\w-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1]].to_s) }
456
- end
332
+ order[0] = @model.fastapi_custom_order[order[0].to_sym].gsub('self.', "#{self_string_table}.")
457
333
 
458
- order[0] = "(#{order[0]})"
459
- order = order.join(' ')
460
- else
334
+ if params.is_a?(Array)
335
+ order[0].gsub!(/\$params\[([\w-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1].to_i].to_s) }
336
+ else
337
+ order[0].gsub!(/\$params\[([\w-]+)\]/) { ActiveRecord::Base.connection.quote(params[Regexp.last_match[1]].to_s) }
338
+ end
461
339
 
462
- if self_obj.column_names.exclude?(order[0])
463
- order = nil
464
- else
465
- order[0] = "#{self_string_table}.#{order[0]}"
340
+ order[0] = "(#{order[0]})"
466
341
  order = order.join(' ')
342
+ else
343
+
344
+ if self_obj.column_names.exclude?(order[0])
345
+ order = nil
346
+ else
347
+ order[0] = "#{self_string_table}.#{order[0]}"
348
+ order = order.join(' ')
349
+ end
467
350
  end
468
351
  end
469
- end
470
-
471
- if filters.size > 0
472
352
 
473
353
  filters.each do |key, data|
354
+
355
+ key = key.to_sym
474
356
  field = key.to_s
475
357
 
476
358
  if field.rindex('__').nil?
477
359
  comparator = 'is'
478
360
  else
479
-
480
361
  comparator = field[(field.rindex('__') + 2)..-1]
481
362
  field = field[0...field.rindex('__')]
482
363
 
483
- if @@api_comparator_list.exclude?(comparator)
484
- next # skip dis bro
485
- end
364
+ next if FastAPI::Comparison.invalid_comparator?(comparator)
486
365
  end
487
366
 
488
367
  if model.nil? && self_obj.reflect_on_all_associations(:has_many).map(&:name).include?(key)
@@ -492,7 +371,7 @@ class FastAPI
492
371
  order_has_many[key] = filter_result[:main_order]
493
372
 
494
373
  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))
374
+ self_obj.reflect_on_all_associations(:has_one).map(&:name).include?(key))
496
375
 
497
376
  filter_result = parse_filters(data, safe, field.singularize.classify.constantize)
498
377
  filter_belongs_to[key] = filter_result[:main]
@@ -501,201 +380,20 @@ class FastAPI
501
380
  elsif self_obj.column_names.include?(field)
502
381
 
503
382
  base_field = "#{self_string_table}.#{field}"
504
- field_string = base_field
505
- is_array = false
506
-
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
511
-
512
- if self_obj.columns_hash[field].type == :boolean
513
-
514
- # if data is not a boolean
515
- if !!data != data
516
- data = ['f', 'false'].include?(data) ? false : true
517
- end
518
-
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
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"
530
- end
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)
537
- end
538
- end
539
- end
540
- end
541
-
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
551
-
552
- def api_generate_sql(filters, offset, count, safe = false)
553
-
554
- filters = parse_filters(filters, safe)
383
+ filter_array << Comparison.new(comparator, data, base_field, self_obj.columns_hash[field].type)
555
384
 
556
- belongs = []
557
- has_many = []
558
-
559
- model_lookup = {}
560
-
561
- filter_fields = []
562
- filter_fields.concat(@model.fastapi_fields)
563
- filter_fields.concat(@whitelist_fields)
564
-
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]
568
-
569
- if class_name
570
- model = class_name.constantize
571
- else
572
- model = field.to_s.classify.constantize
573
- end
574
-
575
- model_lookup[field] = model
576
- belongs << { model: model, alias: field, type: :belongs_to }
577
-
578
- elsif @model.reflect_on_all_associations(:has_one).map(&:name).include?(field)
579
-
580
- class_name = @model.reflect_on_association(field).options[:class_name]
581
-
582
- if class_name
583
- model = class_name.constantize
584
- else
585
- model = field.to_s.classify.constantize
586
- end
587
-
588
- model_lookup[field] = model
589
-
590
- belongs << { model: model, alias: field, type: :has_one }
591
-
592
- elsif @model.reflect_on_all_associations(:has_many).map(&:name).include?(field)
593
-
594
- model = field.to_s.singularize.classify.constantize
595
- model_lookup[field] = model
596
- has_many << model
597
-
598
- elsif @model.column_names.include?(field.to_s)
599
-
600
- field_list << field
601
- end
602
- end
603
-
604
- self_string = @model.to_s.tableize.singularize
605
- self_string_table = @model.to_s.tableize
606
-
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
615
-
616
- # Belongs fields (1 to 1)
617
- joins = belongs.each_with_object([]) do |model_data, join_list|
618
-
619
- model_string_table = model_data[:model].to_s.tableize
620
- model_string_table_alias = model_data[:alias].to_s.pluralize
621
-
622
- model_string_field = model_data[:alias].to_s
623
- singular_self_table = self_string_table.singularize
624
-
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}"
630
385
  end
631
386
  end
632
387
 
633
- # fields
634
- if model_data[:type] == :belongs_to
635
- # joins
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"
641
- end
642
- end
643
-
644
- # Many fields (1 to many)
645
- has_many.each do |model|
646
-
647
- model_string_table = model.to_s.tableize
648
- model_symbol = model_string_table.to_sym
649
-
650
- model_fields = model.fastapi_fields_sub.each_with_object([]) do |field, m_fields|
651
- m_fields << "__#{model_string_table}.#{field}"
652
- end
653
-
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
659
- end
660
-
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
665
- end
666
- end
388
+ {
389
+ main: filter_array,
390
+ main_order: order,
391
+ has_many: filter_has_many,
392
+ has_many_order: order_has_many,
393
+ belongs_to: filter_belongs_to,
394
+ belongs_to_order: order_belongs_to
395
+ }
667
396
 
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(' ')
679
397
  end
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
398
  end
701
399
  end