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
@@ -0,0 +1,91 @@
|
|
1
|
+
module FastAPI
|
2
|
+
class Comparison
|
3
|
+
|
4
|
+
attr_reader :sql
|
5
|
+
alias_method :to_s, :sql
|
6
|
+
|
7
|
+
@@scalar_input = {
|
8
|
+
is: '__FIELD__ = __VALUE__',
|
9
|
+
not: '__FIELD__ <> __VALUE__',
|
10
|
+
gt: '__FIELD__ > __VALUE__',
|
11
|
+
gte: '__FIELD__ >= __VALUE__',
|
12
|
+
lt: '__FIELD__ < __VALUE__',
|
13
|
+
lte: '__FIELD__ <= __VALUE__',
|
14
|
+
like: '__FIELD__ LIKE \'%\' || __VALUE__ || \'%\'',
|
15
|
+
ilike: '__FIELD__ ILIKE \'%\' || __VALUE__ || \'%\'',
|
16
|
+
not_like: 'NOT (__FIELD__ LIKE \'%\' || __VALUE__ || \'%\')',
|
17
|
+
not_ilike: 'NOT (__FIELD__ ILIKE \'%\' || __VALUE__ || \'%\')',
|
18
|
+
null: '__FIELD__ IS NULL',
|
19
|
+
not_null: '__FIELD__ IS NOT NULL'
|
20
|
+
}.with_indifferent_access
|
21
|
+
|
22
|
+
@@multi_input = {
|
23
|
+
in: '__FIELD__ IN (__VALUES__)',
|
24
|
+
not_in: '__FIELD__ NOT IN (__VALUES__)',
|
25
|
+
subset: '__FIELD__ <@ ARRAY[__VALUES__]::__TYPE__[]',
|
26
|
+
not_subset: 'NOT __FIELD__ <@ ARRAY[__VALUES__]::__TYPE__[]',
|
27
|
+
contains: '__FIELD__ @> ARRAY[__VALUES__]::__TYPE__[]',
|
28
|
+
not_contains: 'NOT __FIELD__ @> ARRAY[__VALUES__]::__TYPE__[]',
|
29
|
+
intersects: '__FIELD__ && ARRAY[__VALUES__]::__TYPE__[]',
|
30
|
+
not_intersects: 'NOT __FIELD__ && ARRAY[__VALUES__]::__TYPE__[]'
|
31
|
+
}.with_indifferent_access
|
32
|
+
|
33
|
+
@@booleans = {
|
34
|
+
t: true,
|
35
|
+
true: true,
|
36
|
+
f: false,
|
37
|
+
false: false
|
38
|
+
}.with_indifferent_access
|
39
|
+
|
40
|
+
@@types = Hash.new('text').merge({
|
41
|
+
boolean: 'boolean',
|
42
|
+
integer: 'integer',
|
43
|
+
float: 'float',
|
44
|
+
string: 'varchar'
|
45
|
+
}).with_indifferent_access
|
46
|
+
|
47
|
+
def self.valid_comparator?(comparator)
|
48
|
+
@@scalar_input.key?(comparator) || @@multi_input.key?(comparator)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.invalid_comparator?(comparator)
|
52
|
+
!valid_comparator?(comparator)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(comparator, value, field, type)
|
56
|
+
key = prepare_comparator(comparator, value)
|
57
|
+
val = prepare_value(value, type)
|
58
|
+
if clause = @@scalar_input[key]
|
59
|
+
@sql = scalar_sql(clause, field, val)
|
60
|
+
elsif clause = @@multi_input[key]
|
61
|
+
@sql = multi_sql(clause, val, field, type)
|
62
|
+
else
|
63
|
+
raise ArgumentError.new("Invalid comparator: #{key}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def prepare_comparator(comparator, value)
|
69
|
+
|
70
|
+
if value.nil? and comparator == 'is'
|
71
|
+
comparator = :null
|
72
|
+
end
|
73
|
+
|
74
|
+
return comparator
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_value(value, type)
|
79
|
+
type == :boolean && !!value != value ? @@booleans[value] : value
|
80
|
+
end
|
81
|
+
|
82
|
+
def scalar_sql(clause, field, value)
|
83
|
+
clause.sub('__FIELD__', field).sub('__VALUE__', ActiveRecord::Base.connection.quote(value))
|
84
|
+
end
|
85
|
+
|
86
|
+
def multi_sql(clause, value, field, type)
|
87
|
+
values = [*value].map { |v| ActiveRecord::Base.connection.quote(v) }.join(',')
|
88
|
+
[clause.sub('__FIELD__', field).sub('__VALUES__', values).sub('__TYPE__', @@types[type])].compact.join
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module FastAPI
|
2
|
+
module Conversions
|
3
|
+
|
4
|
+
def self.convert_type(val, type, field = nil)
|
5
|
+
if val && is_array(field)
|
6
|
+
Oj.load(val).map { |inner_value| convert_value(inner_value, type) }
|
7
|
+
else
|
8
|
+
convert_value(val, type)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def self.is_array(field)
|
14
|
+
field && field.respond_to?('array') && field.array
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.convert_value(val, type)
|
18
|
+
if val
|
19
|
+
case type
|
20
|
+
when :integer
|
21
|
+
val.to_i
|
22
|
+
when :float
|
23
|
+
val.to_f
|
24
|
+
when :boolean
|
25
|
+
{ 't' => true, 'f' => false }[val]
|
26
|
+
else
|
27
|
+
val
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module FastAPIExtension
|
1
|
+
module Extension
|
4
2
|
|
5
3
|
extend ActiveSupport::Concern
|
6
4
|
|
@@ -68,9 +66,9 @@ module FastAPIExtension
|
|
68
66
|
end
|
69
67
|
|
70
68
|
def fastapi
|
71
|
-
FastAPI.new(self)
|
69
|
+
FastAPI::Wrapper.new(self)
|
72
70
|
end
|
73
71
|
end
|
74
72
|
end
|
75
73
|
|
76
|
-
ActiveRecord::Base.send(:include,
|
74
|
+
ActiveRecord::Base.send(:include, Extension)
|
data/lib/fastapi/sql.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module FastAPI
|
4
|
+
class SQL
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegator :@sql, :[]
|
8
|
+
|
9
|
+
def initialize(filters, offset, count, klazz, whitelist, safe = false)
|
10
|
+
|
11
|
+
results = filter_fields(klazz, whitelist)
|
12
|
+
models, belongs, has_many, fields = results.values_at(:models, :belongs, :has_many, :fields)
|
13
|
+
|
14
|
+
model_name = klazz.to_s.tableize.singularize
|
15
|
+
table_name = klazz.to_s.tableize
|
16
|
+
|
17
|
+
# Base fields
|
18
|
+
field_list = generate_field_list(klazz, fields, table_name)
|
19
|
+
|
20
|
+
# Belongs fields
|
21
|
+
joins = parse_belongs(belongs, field_list, table_name)
|
22
|
+
|
23
|
+
# Many fields (1 to many)
|
24
|
+
parse_manys(has_many, filters, field_list, model_name, table_name)
|
25
|
+
|
26
|
+
filter_string = filters[:main].size > 0 ? "WHERE #{filters[:main].join(' AND ')}" : nil
|
27
|
+
order_string = filters[:main_order] ? "ORDER BY #{filters[:main_order]}" : nil
|
28
|
+
|
29
|
+
@sql = {
|
30
|
+
query: [
|
31
|
+
"SELECT #{field_list.join(', ')}",
|
32
|
+
"FROM #{table_name}",
|
33
|
+
joins.join(' '),
|
34
|
+
filter_string,
|
35
|
+
order_string,
|
36
|
+
"LIMIT #{count}",
|
37
|
+
"OFFSET #{offset}"
|
38
|
+
].compact.join(' '),
|
39
|
+
count_query: [
|
40
|
+
"SELECT COUNT(id) FROM #{table_name}",
|
41
|
+
filter_string
|
42
|
+
].compact.join(' '),
|
43
|
+
models: models
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def filter_fields(klazz, whitelist)
|
49
|
+
skeleton = { models: {}, belongs: [], has_many: [], fields: [] }
|
50
|
+
(klazz.fastapi_fields + whitelist).each_with_object(skeleton) do |field, results|
|
51
|
+
|
52
|
+
if klazz.reflect_on_all_associations(:belongs_to).map(&:name).include?(field)
|
53
|
+
|
54
|
+
class_name = klazz.reflect_on_association(field).options[:class_name]
|
55
|
+
model = constantize_model(class_name, field)
|
56
|
+
|
57
|
+
results[:models][field] = model
|
58
|
+
results[:belongs] << { model: model, alias: field, type: :belongs_to }
|
59
|
+
|
60
|
+
elsif klazz.reflect_on_all_associations(:has_one).map(&:name).include?(field)
|
61
|
+
|
62
|
+
class_name = klazz.reflect_on_association(field).options[:class_name]
|
63
|
+
model = constantize_model(class_name, field)
|
64
|
+
|
65
|
+
results[:models][field] = model
|
66
|
+
results[:belongs] << { model: model, alias: field, type: :has_one }
|
67
|
+
|
68
|
+
elsif klazz.reflect_on_all_associations(:has_many).map(&:name).include?(field)
|
69
|
+
|
70
|
+
model = field.to_s.singularize.classify.constantize
|
71
|
+
|
72
|
+
results[:models][field] = model
|
73
|
+
results[:has_many] << model
|
74
|
+
|
75
|
+
elsif klazz.column_names.include?(field.to_s)
|
76
|
+
results[:fields] << field
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def generate_field_list(klazz, fields, table)
|
83
|
+
fields.each_with_object([]) do |field, list|
|
84
|
+
if klazz.columns_hash[field.to_s].array
|
85
|
+
list << "ARRAY_TO_JSON(#{table}.#{field}) AS #{field}"
|
86
|
+
else
|
87
|
+
list << "#{table}.#{field} AS #{field}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_belongs(belongs, field_list, model_table_name)
|
93
|
+
belongs.each_with_object([]) do |model_data, join_list|
|
94
|
+
|
95
|
+
table_name = model_data[:model].to_s.tableize
|
96
|
+
table_alias = model_data[:alias].to_s.pluralize
|
97
|
+
|
98
|
+
field_name = model_data[:alias].to_s
|
99
|
+
singular_table_name = model_table_name.singularize
|
100
|
+
|
101
|
+
model_data[:model].fastapi_fields_sub.each do |field|
|
102
|
+
if model_data[:model].columns_hash[field.to_s].array
|
103
|
+
field_list << "ARRAY_TO_JSON(#{table_alias}.#{field}) AS #{field_name}__#{field}"
|
104
|
+
else
|
105
|
+
field_list << "#{table_alias}.#{field} AS #{field_name}__#{field}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# fields
|
110
|
+
if model_data[:type] == :belongs_to
|
111
|
+
# joins
|
112
|
+
join_list << "LEFT JOIN #{table_name} AS #{table_alias} " \
|
113
|
+
"ON #{table_alias}.id = #{model_table_name}.#{field_name}_id"
|
114
|
+
elsif model_data[:type] == :has_one
|
115
|
+
join_list << "LEFT JOIN #{table_name} AS #{table_alias} " \
|
116
|
+
"ON #{table_alias}.#{singular_table_name}_id = #{model_table_name}.id"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_manys(has_many, filters, field_list, model_name, table_name)
|
122
|
+
has_many.each do |model|
|
123
|
+
|
124
|
+
model_string_table = model.to_s.tableize
|
125
|
+
model_symbol = model_string_table.to_sym
|
126
|
+
|
127
|
+
model_fields = model.fastapi_fields_sub.each_with_object([]) do |field, m_fields|
|
128
|
+
m_fields << "__#{model_string_table}.#{field}"
|
129
|
+
end
|
130
|
+
|
131
|
+
if filters[:has_many].has_key?(model_symbol)
|
132
|
+
|
133
|
+
if not filters[:has_many][model_symbol].blank?
|
134
|
+
has_many_filters = "AND #{filters[:has_many][model_symbol].join(' AND ')}"
|
135
|
+
else
|
136
|
+
has_many_filters = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
if not filters[:has_many_order][model_symbol].blank?
|
140
|
+
has_many_order = "ORDER BY #{filters[:has_many_order][model_symbol]}"
|
141
|
+
else
|
142
|
+
has_many_order = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
field_list << [
|
148
|
+
"ARRAY_TO_JSON(ARRAY(SELECT ROW(#{model_fields.join(', ')})",
|
149
|
+
"FROM #{model_string_table}",
|
150
|
+
"AS __#{model_string_table}",
|
151
|
+
"WHERE __#{model_string_table}.#{model_name}_id IS NOT NULL",
|
152
|
+
"AND __#{model_string_table}.#{model_name}_id",
|
153
|
+
"= #{table_name}.id",
|
154
|
+
has_many_filters,
|
155
|
+
has_many_order,
|
156
|
+
")) AS __many__#{model_string_table}"
|
157
|
+
].compact.join(' ')
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def constantize_model(class_name, field)
|
163
|
+
(class_name ? class_name : field.to_s.classify).constantize
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fastapi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keith Horwood
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-06-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -147,7 +147,11 @@ extensions: []
|
|
147
147
|
extra_rdoc_files: []
|
148
148
|
files:
|
149
149
|
- lib/fastapi.rb
|
150
|
-
- lib/fastapi/
|
150
|
+
- lib/fastapi/comparison.rb
|
151
|
+
- lib/fastapi/conversions.rb
|
152
|
+
- lib/fastapi/extension.rb
|
153
|
+
- lib/fastapi/sql.rb
|
154
|
+
- lib/fastapi/utilities.rb
|
151
155
|
homepage: https://github.com/thestorefront/FastAPI
|
152
156
|
licenses:
|
153
157
|
- MIT
|