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 +4 -4
- data/lib/fastapi.rb +281 -583
- data/lib/fastapi/comparison.rb +91 -0
- data/lib/fastapi/conversions.rb +32 -0
- data/lib/fastapi/{active_record_extension.rb → extension.rb} +3 -5
- data/lib/fastapi/sql.rb +166 -0
- data/lib/fastapi/utilities.rb +7 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a39886af522fab889ef06e2c54bdee7f323df410
|
4
|
+
data.tar.gz: abf090555863c01150f1562e6c596390a346fb2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a27ad46c34689bffa30355e4432019d858473c8ba581c3dc0fecf149075eb4098cbbb681ba52dba670d4080893d235cce491913eee8228745caf1aad471b465
|
7
|
+
data.tar.gz: be109289004c0c3315f9e05d4fddab3ec643fb8ddeb56ee25047b91090bba50d025a4352abcf35d7f2d4e03d0db8396716c2aa4a1b5aedfee37be6148bd4c35d
|
data/lib/fastapi.rb
CHANGED
@@ -1,488 +1,367 @@
|
|
1
1
|
require 'oj'
|
2
|
-
require '
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
46
|
+
@metadata = meta.merge(result.slice(:total, :offset, :count, :error))
|
47
|
+
@data = result[:data]
|
177
48
|
|
178
|
-
|
179
|
-
|
180
|
-
end
|
49
|
+
self
|
50
|
+
end
|
181
51
|
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
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
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
232
|
-
|
233
|
-
model = model_lookup[field_sym]
|
128
|
+
Oj.dump({ meta: meta, data: data })
|
129
|
+
end
|
234
130
|
|
235
|
-
|
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
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
168
|
+
private
|
169
|
+
def error(offset, message)
|
170
|
+
{ data: [], total: 0, count: 0, offset: offset, error: { message: message } }
|
171
|
+
end
|
242
172
|
|
243
|
-
|
173
|
+
def fastapi_query(filters = {}, safe = false)
|
244
174
|
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
207
|
+
total_size = count_result.values.size > 0 ? count_result.values[0][0].to_i : 0
|
281
208
|
|
282
|
-
|
209
|
+
fields = result.fields
|
210
|
+
rows = result.values
|
283
211
|
|
284
|
-
|
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
|
-
|
217
|
+
if field[0..7] == '__many__'
|
287
218
|
|
288
|
-
|
219
|
+
field = field[8..-1]
|
220
|
+
field_sym = field.to_sym
|
221
|
+
model = model_lookup[field_sym]
|
289
222
|
|
290
|
-
|
223
|
+
current[field_sym] = parse_many(val, model[:fields], model[:types])
|
291
224
|
|
292
|
-
|
225
|
+
elsif split_index
|
293
226
|
|
294
|
-
|
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
|
-
|
231
|
+
current[obj_name] ||= {}
|
297
232
|
|
298
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
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
|
-
|
260
|
+
# if we're at the top level...
|
261
|
+
if model.nil?
|
374
262
|
|
375
|
-
|
376
|
-
|
263
|
+
if safe
|
264
|
+
filters.each do |key, value|
|
377
265
|
|
378
|
-
|
379
|
-
|
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
|
-
|
383
|
-
|
384
|
-
|
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
|
-
|
394
|
-
end
|
276
|
+
filters = @model.fastapi_filters.clone.merge(filters).with_indifferent_access
|
395
277
|
|
396
|
-
|
397
|
-
filters[:__order] ||= [:created_at, :DESC]
|
278
|
+
end
|
398
279
|
|
399
|
-
|
280
|
+
params = filters.has_key?(:__params) ? filters.delete(:__params) : []
|
400
281
|
|
401
|
-
|
282
|
+
filters.each do |key, value|
|
402
283
|
|
403
|
-
|
404
|
-
key_root = found_index.nil? ? key : key.to_s[0...found_index].to_sym
|
284
|
+
key = key.to_sym
|
405
285
|
|
406
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
421
|
-
order_has_many = {}
|
422
|
-
order_belongs_to = {}
|
299
|
+
end
|
423
300
|
|
424
|
-
|
301
|
+
filter_array = []
|
302
|
+
filter_has_many = {}
|
303
|
+
filter_belongs_to = {}
|
425
304
|
|
426
|
-
|
305
|
+
order = nil
|
306
|
+
order_has_many = {}
|
307
|
+
order_belongs_to = {}
|
427
308
|
|
428
|
-
|
309
|
+
# get the order first
|
310
|
+
if filters.has_key?(:__order)
|
429
311
|
|
430
|
-
|
312
|
+
order = filters.delete(:__order)
|
431
313
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
-
|
328
|
+
order[1] = 'ASC' if ['ASC', 'DESC'].exclude?(order[1])
|
449
329
|
|
450
|
-
|
330
|
+
if model.nil? && @model.fastapi_custom_order.has_key?(order[0].to_sym)
|
451
331
|
|
452
|
-
|
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
|
-
|
459
|
-
|
460
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
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
|