fastapi 0.1.27 → 0.2.0

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