Class: PwQuery::Service

Inherits:
Object
  • Object
show all
Defined in:
lib/pw_query/service.rb

Constant Summary collapse

DEFAULT_LIMIT =
50

Instance Method Summary collapse

Constructor Details

#initialize(model, params, options = {}, associations = nil) ⇒ Service

Returns a new instance of Service.



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/pw_query/service.rb', line 7

def initialize(model, params, options = {}, associations = nil)
  @model = model
  @params = params
  @options = options
  @associations = associations
  @total = @model.count
  @limit = @params.fetch(:limit, DEFAULT_LIMIT).to_i
  @meta = {
    total: @total,
    limit: @limit,
    page: @params.fetch(:page, 1).to_i,
    page_size: @total,
    total_pages: (@total.to_f / @limit).ceil
  }
end

Instance Method Details

#apply_aggregation(query, value) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/pw_query/service.rb', line 210

def apply_aggregation(query, value)
  return query if value.nil? || value.empty?

  aggregation_type, aggregation_field = value.split(':')

  case aggregation_type
  when 'count'
    query.count
  when 'sum'
    query.sum(aggregation_field)
  when 'average'
    query.average(aggregation_field)
  when 'max'
    query.maximum(aggregation_field)
  when 'min'
    query.minimum(aggregation_field)
  else
    query
  end
end

#apply_conditions(query, conditions) ⇒ Object



174
175
176
177
178
179
180
# File 'lib/pw_query/service.rb', line 174

def apply_conditions(query, conditions)
  return query unless conditions.any?

  where_clause = conditions.map(&:first).join(' OR ')
  where_values = conditions.map(&:last).flatten
  query.where(where_clause, *where_values)
end

#apply_date_range(query, value) ⇒ Object



87
88
89
90
91
# File 'lib/pw_query/service.rb', line 87

def apply_date_range(query, value)
  start_date = value ? parse_date(value['start']) : 1.month.ago.beginning_of_day
  end_date = value ? parse_date(value['end']) : Time.current
  query.where(created_at: start_date...end_date)
end

#apply_field_selection(query, params) ⇒ Object



78
79
80
81
82
83
84
85
# File 'lib/pw_query/service.rb', line 78

def apply_field_selection(query, params)
  if params[:projection] || params[:fields]
    fields = params[:projection] || params[:fields]
    query.select(fields.split(','))
  else
    query
  end
end

#apply_filters(query, filters) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/pw_query/service.rb', line 45

def apply_filters(query, filters)
  return query if filters.blank?

  filters.each do |key, value|
    next if value.blank?

    values = value.to_s.split(',').map(&:strip)
    condition = values.size > 1 ? values : value
    query = query.where(key => condition)
  end

  query
end

#apply_multi_sort(query, value) ⇒ Object



72
73
74
75
76
# File 'lib/pw_query/service.rb', line 72

def apply_multi_sort(query, value)
  sort_params = value.split(',')
  sorting_criteria = sort_params.map { |param| param.split(':').join(' ') }
  query.order(sorting_criteria.join(', '))
end

#apply_pagination(query, params) ⇒ Object



202
203
204
205
206
207
208
# File 'lib/pw_query/service.rb', line 202

def apply_pagination(query, params)
  page = params.fetch(:page, 1).to_i
  offset_value = page == 1 ? 0 : (page - 1) * @limit
  total_pages = (@total.to_f / @limit).ceil

  page <= total_pages ? query.offset(offset_value).limit(@limit) : query.none
end

#apply_range(query, ranges) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/pw_query/service.rb', line 182

def apply_range(query, ranges)
  return query if ranges.blank?

  ranges.each do |key, value|
    range_values = value.split(',')
    min_value = range_values[0].to_f if range_values[0].present?
    max_value = range_values[1].to_f if range_values[1].present?

    if min_value.present? && max_value.present?
      query = query.where("#{key} BETWEEN ? AND ?", min_value, max_value)
    elsif min_value.present?
      query = query.where("#{key} >= ?", min_value)
    elsif max_value.present?
      query = query.where("#{key} <= ?", max_value)
    end
  end

  query
end

#apply_search(query, filters) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/pw_query/service.rb', line 108

def apply_search(query, filters)
  return query if filters.blank?

  filters = filters.to_unsafe_h
  conditions = build_filter_conditions(query, filters)
  apply_conditions(query, conditions)
end

#apply_sort(query, value) ⇒ Object



67
68
69
70
# File 'lib/pw_query/service.rb', line 67

def apply_sort(query, value)
  sort_field, sort_order = value.split(':')
  query.order("#{sort_field} #{sort_order}")
end

#apply_sorting(query, params) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/pw_query/service.rb', line 59

def apply_sorting(query, params)
  if params[:sort_by]
    apply_multi_sort(query, params[:sort_by])
  else
    apply_sort(query, params[:sort] || 'created_at:DESC')
  end
end

#apply_time_range(query, value) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/pw_query/service.rb', line 93

def apply_time_range(query, value)
  return query if value.blank?

  start_time = parse_time(value['start'] || '00:00:00')
  end_time = parse_time(value['end'] || '23:59:59')

  table_name = query.model.table_name
  if start_time <= end_time
    query.where("CAST(#{table_name}.created_at AS time) BETWEEN ? AND ?", start_time, end_time)
  else
    query.where("CAST(#{table_name}.created_at AS time) >= ? OR CAST(#{table_name}.created_at AS time) <= ?",
                start_time, end_time)
  end
end

#association_condition(query, key, value) ⇒ Object



124
125
126
127
128
129
# File 'lib/pw_query/service.rb', line 124

def association_condition(query, key, value)
  association_chain = key.split('.')
  attribute = association_chain.pop
  current_table = build_join_chain(query, association_chain)
  create_condition(current_table, attribute, value)
end

#build_filter_conditions(query, filters) ⇒ Object



116
117
118
119
120
121
122
# File 'lib/pw_query/service.rb', line 116

def build_filter_conditions(query, filters)
  filters.map do |key, value|
    next if value.blank?

    key.include?('.') ? association_condition(query, key, value) : direct_condition(query, key, value)
  end.compact
end

#build_join_chain(query, chain) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/pw_query/service.rb', line 131

def build_join_chain(query, chain)
  current_model = query.klass
  current_table = current_model.table_name
  join_chain = []

  chain.each do |assoc|
    reflection = current_model.reflect_on_association(assoc.to_sym) or raise "Unknown association: #{assoc} for model #{current_model}"
    table_alias = "#{reflection.klass.table_name}_as_#{current_table}"
    join_chain << { table: reflection.klass.table_name, alias: table_alias, foreign_key: reflection.foreign_key,
                    parent_table: current_table }
    current_table = table_alias
    current_model = reflection.klass
  end

  query.joins(join_chain.map do |join|
    "LEFT JOIN #{join[:table]} AS #{join[:alias]} ON #{join[:alias]}.id = #{join[:parent_table]}.#{join[:foreign_key]}"
  end.join(' '))
end

#create_condition(table, attribute, value) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/pw_query/service.rb', line 154

def create_condition(table, attribute, value)
  values = value.to_s.split(',').map(&:strip)
  column = get_column_type(table, attribute)

  case column&.type
  when :string, :text
    condition = if values.size > 1
                  values.map do |_v|
                    "LOWER(#{table}.#{attribute}::text) LIKE LOWER(?)"
                  end.join(' OR ')
                else
                  "LOWER(#{table}.#{attribute}::text) LIKE LOWER(?)"
                end
    values = values.size > 1 ? values.map { |v| "%#{escape_like(v)}%" } : ["%#{escape_like(value)}%"]
    [condition, *values]
  else
    values.size > 1 ? ["#{table}.#{attribute} IN (?)", values] : ["#{table}.#{attribute} = ?", value]
  end
end

#direct_condition(query, key, value) ⇒ Object



150
151
152
# File 'lib/pw_query/service.rb', line 150

def direct_condition(query, key, value)
  create_condition(query.klass.table_name, key, value)
end

#escape_like(string) ⇒ Object



238
239
240
# File 'lib/pw_query/service.rb', line 238

def escape_like(string)
  string.gsub(/[\\%_]/) { |m| "\\#{m}" }
end

#get_column_type(table, attribute) ⇒ Object



231
232
233
234
235
236
# File 'lib/pw_query/service.rb', line 231

def get_column_type(table, attribute)
  klass = table.classify.constantize
  klass.columns_hash[attribute.to_s]
rescue StandardError
  nil
end

#include_associations(query, associations) ⇒ Object



41
42
43
# File 'lib/pw_query/service.rb', line 41

def include_associations(query, associations)
  query.includes(associations)
end

#parse_date(date_string) ⇒ Object



248
249
250
251
252
# File 'lib/pw_query/service.rb', line 248

def parse_date(date_string)
  Date.parse(date_string) if date_string.present?
rescue ArgumentError
  nil
end

#parse_time(time_string) ⇒ Object



242
243
244
245
246
# File 'lib/pw_query/service.rb', line 242

def parse_time(time_string)
  Time.parse(time_string).strftime('%H:%M:%S')
rescue ArgumentError
  '00:00:00'
end

#performObject



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/pw_query/service.rb', line 23

def perform
  query = @model.all
  query = include_associations(query, @associations)
  query = apply_filters(query, @params[:filters])
  query = apply_sorting(query, @params)
  query = apply_field_selection(query, @params)
  query = apply_date_range(query, @params[:date_range]) if @options[:date_scope]
  query = apply_time_range(query, @params[:time_range])
  query = apply_search(query, @params[:search])
  query = apply_range(query, @params[:range])
  query = apply_pagination(query, @params)
  query = apply_aggregation(query, @params[:aggregation])

  handle_query(query)

  { meta: @meta, data: query }
end