propel_api 0.3.1.6 → 0.3.3
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/CHANGELOG.md +83 -0
- data/README.md +46 -1
- data/lib/generators/propel_api/controller/controller_generator.rb +13 -7
- data/lib/generators/propel_api/core/named_base.rb +1191 -103
- data/lib/generators/propel_api/install/install_generator.rb +43 -2
- data/lib/generators/propel_api/resource/resource_generator.rb +173 -61
- data/lib/generators/propel_api/templates/concerns/propel_controller_filters_concern.rb +141 -0
- data/lib/generators/propel_api/templates/concerns/propel_model_filters_concern.rb +128 -0
- data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +1 -0
- data/lib/generators/propel_api/templates/lib/XX_dynamic_scope_generator.rb +360 -0
- data/lib/generators/propel_api/templates/lib/propel_dynamic_scope_generator.rb +474 -0
- data/lib/generators/propel_api/templates/lib/propel_filter_operators.rb +16 -0
- data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +14 -0
- data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +100 -6
- data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +9 -0
- data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +660 -10
- data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +7 -0
- data/lib/propel_api.rb +1 -1
- metadata +11 -5
@@ -0,0 +1,360 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# DynamicScopeGenerator
|
4
|
+
#
|
5
|
+
# Generates dynamic scopes and has_scope registrations for ActiveRecord models
|
6
|
+
# based on database column types and filter operators.
|
7
|
+
#
|
8
|
+
# This class handles the creation of both model scopes and controller has_scope
|
9
|
+
# registrations dynamically, ensuring they work together properly.
|
10
|
+
#
|
11
|
+
# Usage:
|
12
|
+
# generator = DynamicScopeGenerator.new(Team)
|
13
|
+
# generator.generate_scopes!
|
14
|
+
# generator.register_controller_scopes(MyController)
|
15
|
+
#
|
16
|
+
class DynamicScopeGenerator
|
17
|
+
attr_reader :model_class, :generated_scopes, :generated_has_scopes
|
18
|
+
|
19
|
+
def initialize(model_class)
|
20
|
+
@model_class = model_class
|
21
|
+
@generated_scopes = []
|
22
|
+
@generated_has_scopes = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generate all scopes for the model based on its columns
|
26
|
+
def generate_scopes!
|
27
|
+
return if @generated_scopes.any?
|
28
|
+
|
29
|
+
@model_class.columns.each do |column|
|
30
|
+
generate_scopes_for_column(column)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add sorting scope
|
34
|
+
generate_sort_scope
|
35
|
+
end
|
36
|
+
|
37
|
+
# Register has_scope calls for a controller
|
38
|
+
def register_controller_scopes(controller_class)
|
39
|
+
return if @generated_has_scopes.any?
|
40
|
+
|
41
|
+
@generated_scopes.each do |scope_name|
|
42
|
+
register_has_scope(controller_class, scope_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def generate_scopes_for_column(column)
|
49
|
+
name = column.name
|
50
|
+
type = column.type
|
51
|
+
sql_type = column.sql_type_metadata.sql_type
|
52
|
+
|
53
|
+
operators = PropelFilterOperators.operators_for(type, sql_type)
|
54
|
+
|
55
|
+
operators.each do |operator|
|
56
|
+
scope_name = "#{name}_#{operator}".to_sym
|
57
|
+
generate_scope_method(scope_name, name, operator, type)
|
58
|
+
@generated_scopes << scope_name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_scope_method(scope_name, field_name, operator, field_type)
|
63
|
+
return if @model_class.respond_to?(scope_name)
|
64
|
+
|
65
|
+
case operator
|
66
|
+
when "eq"
|
67
|
+
generate_eq_scope(scope_name, field_name, field_type)
|
68
|
+
when "contains"
|
69
|
+
generate_contains_scope(scope_name, field_name)
|
70
|
+
when "starts_with"
|
71
|
+
generate_starts_with_scope(scope_name, field_name)
|
72
|
+
when "ends_with"
|
73
|
+
generate_ends_with_scope(scope_name, field_name)
|
74
|
+
when "gt"
|
75
|
+
generate_gt_scope(scope_name, field_name)
|
76
|
+
when "lt"
|
77
|
+
generate_lt_scope(scope_name, field_name)
|
78
|
+
when "gte", "min"
|
79
|
+
generate_gte_scope(scope_name, field_name)
|
80
|
+
when "lte", "max"
|
81
|
+
generate_lte_scope(scope_name, field_name)
|
82
|
+
when "before"
|
83
|
+
generate_before_scope(scope_name, field_name)
|
84
|
+
when "after"
|
85
|
+
generate_after_scope(scope_name, field_name)
|
86
|
+
when "in"
|
87
|
+
generate_in_scope(scope_name, field_name)
|
88
|
+
when "null"
|
89
|
+
generate_null_scope(scope_name, field_name)
|
90
|
+
when "range"
|
91
|
+
generate_range_scope(scope_name, field_name)
|
92
|
+
when "date"
|
93
|
+
generate_date_scope(scope_name, field_name)
|
94
|
+
when "year"
|
95
|
+
generate_year_scope(scope_name, field_name)
|
96
|
+
when "month"
|
97
|
+
generate_month_scope(scope_name, field_name)
|
98
|
+
when "day"
|
99
|
+
generate_day_scope(scope_name, field_name)
|
100
|
+
end
|
101
|
+
rescue StandardError => e
|
102
|
+
Rails.logger.warn "Error generating scope #{scope_name}: #{e.message}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def generate_eq_scope(scope_name, field_name, field_type)
|
106
|
+
case field_type
|
107
|
+
when :boolean
|
108
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
109
|
+
scope :#{scope_name}, ->(value) {
|
110
|
+
return all if value.blank?
|
111
|
+
boolean_value = case value.to_s.downcase
|
112
|
+
when 'true', '1', 'yes', 'on'
|
113
|
+
true
|
114
|
+
when 'false', '0', 'no', 'off'
|
115
|
+
false
|
116
|
+
else
|
117
|
+
value
|
118
|
+
end
|
119
|
+
where(#{field_name}: boolean_value)
|
120
|
+
}
|
121
|
+
RUBY
|
122
|
+
else
|
123
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
124
|
+
scope :#{scope_name}, ->(value) { value.present? ? where(#{field_name}: value) : all }
|
125
|
+
RUBY
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_contains_scope(scope_name, field_name)
|
130
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
131
|
+
scope :#{scope_name}, ->(value) {
|
132
|
+
value.present? ? where("#{field_name} #{like_operator} ?", "%\#{value}%") : all
|
133
|
+
}
|
134
|
+
RUBY
|
135
|
+
end
|
136
|
+
|
137
|
+
def generate_starts_with_scope(scope_name, field_name)
|
138
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
139
|
+
scope :#{scope_name}, ->(value) {
|
140
|
+
value.present? ? where("#{field_name} #{like_operator} ?", "\#{value}%") : all
|
141
|
+
}
|
142
|
+
RUBY
|
143
|
+
end
|
144
|
+
|
145
|
+
def generate_ends_with_scope(scope_name, field_name)
|
146
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
147
|
+
scope :#{scope_name}, ->(value) {
|
148
|
+
value.present? ? where("#{field_name} #{like_operator} ?", "%\#{value}") : all
|
149
|
+
}
|
150
|
+
RUBY
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_gt_scope(scope_name, field_name)
|
154
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
155
|
+
scope :#{scope_name}, ->(value) {
|
156
|
+
value.present? ? where("#{field_name} > ?", value) : all
|
157
|
+
}
|
158
|
+
RUBY
|
159
|
+
end
|
160
|
+
|
161
|
+
def generate_lt_scope(scope_name, field_name)
|
162
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
163
|
+
scope :#{scope_name}, ->(value) {
|
164
|
+
value.present? ? where("#{field_name} < ?", value) : all
|
165
|
+
}
|
166
|
+
RUBY
|
167
|
+
end
|
168
|
+
|
169
|
+
def generate_gte_scope(scope_name, field_name)
|
170
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
171
|
+
scope :#{scope_name}, ->(value) {
|
172
|
+
value.present? ? where("#{field_name} >= ?", value) : all
|
173
|
+
}
|
174
|
+
RUBY
|
175
|
+
end
|
176
|
+
|
177
|
+
def generate_lte_scope(scope_name, field_name)
|
178
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
179
|
+
scope :#{scope_name}, ->(value) {
|
180
|
+
value.present? ? where("#{field_name} <= ?", value) : all
|
181
|
+
}
|
182
|
+
RUBY
|
183
|
+
end
|
184
|
+
|
185
|
+
def generate_before_scope(scope_name, field_name)
|
186
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
187
|
+
scope :#{scope_name}, ->(value) {
|
188
|
+
value.present? ? where("#{field_name} < ?", value) : all
|
189
|
+
}
|
190
|
+
RUBY
|
191
|
+
end
|
192
|
+
|
193
|
+
def generate_after_scope(scope_name, field_name)
|
194
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
195
|
+
scope :#{scope_name}, ->(value) {
|
196
|
+
value.present? ? where("#{field_name} > ?", value) : all
|
197
|
+
}
|
198
|
+
RUBY
|
199
|
+
end
|
200
|
+
|
201
|
+
def generate_in_scope(scope_name, field_name)
|
202
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
203
|
+
scope :#{scope_name}, ->(value) {
|
204
|
+
if value.present?
|
205
|
+
values = value.is_a?(Array) ? value : value.to_s.split(",").map(&:strip)
|
206
|
+
values.any? ? where(#{field_name}: values) : all
|
207
|
+
else
|
208
|
+
all
|
209
|
+
end
|
210
|
+
}
|
211
|
+
RUBY
|
212
|
+
end
|
213
|
+
|
214
|
+
def generate_null_scope(scope_name, field_name)
|
215
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
216
|
+
scope :#{scope_name}, ->(value) {
|
217
|
+
if value.present?
|
218
|
+
is_null = value.to_s.downcase == "true"
|
219
|
+
is_null ? where("#{field_name} IS NULL") : where("#{field_name} IS NOT NULL")
|
220
|
+
else
|
221
|
+
all
|
222
|
+
end
|
223
|
+
}
|
224
|
+
RUBY
|
225
|
+
end
|
226
|
+
|
227
|
+
def generate_range_scope(scope_name, field_name)
|
228
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
229
|
+
scope :#{scope_name}, ->(value) {
|
230
|
+
if value.present? && value.is_a?(Array) && value.size == 2
|
231
|
+
where("#{field_name} BETWEEN ? AND ?", value[0], value[1])
|
232
|
+
else
|
233
|
+
all
|
234
|
+
end
|
235
|
+
}
|
236
|
+
RUBY
|
237
|
+
end
|
238
|
+
|
239
|
+
def generate_date_scope(scope_name, field_name)
|
240
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
241
|
+
scope :#{scope_name}, ->(value) {
|
242
|
+
if value.present?
|
243
|
+
date = value.is_a?(Date) ? value : Date.parse(value.to_s)
|
244
|
+
where("DATE(#{field_name}) = ?", date)
|
245
|
+
else
|
246
|
+
all
|
247
|
+
end
|
248
|
+
}
|
249
|
+
RUBY
|
250
|
+
end
|
251
|
+
|
252
|
+
def generate_year_scope(scope_name, field_name)
|
253
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
254
|
+
scope :#{scope_name}, ->(value) {
|
255
|
+
value.present? ? where("EXTRACT(YEAR FROM #{field_name}) = ?", value) : all
|
256
|
+
}
|
257
|
+
RUBY
|
258
|
+
end
|
259
|
+
|
260
|
+
def generate_month_scope(scope_name, field_name)
|
261
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
262
|
+
scope :#{scope_name}, ->(value) {
|
263
|
+
value.present? ? where("EXTRACT(MONTH FROM #{field_name}) = ?", value) : all
|
264
|
+
}
|
265
|
+
RUBY
|
266
|
+
end
|
267
|
+
|
268
|
+
def generate_day_scope(scope_name, field_name)
|
269
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
270
|
+
scope :#{scope_name}, ->(value) {
|
271
|
+
value.present? ? where("EXTRACT(DAY FROM #{field_name}) = ?", value) : all
|
272
|
+
}
|
273
|
+
RUBY
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_sort_scope
|
277
|
+
scope_name = :order_by
|
278
|
+
return if @generated_scopes.include?(scope_name)
|
279
|
+
|
280
|
+
begin
|
281
|
+
# Get all sortable columns (exclude sensitive fields)
|
282
|
+
sortable_columns = @model_class.columns.reject do |column|
|
283
|
+
sensitive_fields.include?(column.name.to_s)
|
284
|
+
end.map(&:name)
|
285
|
+
|
286
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
287
|
+
scope :#{scope_name}, ->(value) {
|
288
|
+
return all if value.blank?
|
289
|
+
|
290
|
+
# Parse sort parameter (e.g., "name", "-name", "name:asc", "name:desc")
|
291
|
+
field = value.to_s
|
292
|
+
direction = 'asc'
|
293
|
+
|
294
|
+
# Handle "-field" format for descending
|
295
|
+
if field.start_with?('-')
|
296
|
+
field = field[1..-1]
|
297
|
+
direction = 'desc'
|
298
|
+
else
|
299
|
+
# Handle "field:direction" format
|
300
|
+
field, direction = field.split(':')
|
301
|
+
direction ||= 'asc'
|
302
|
+
end
|
303
|
+
|
304
|
+
# Validate field is sortable
|
305
|
+
sortable_fields = #{sortable_columns.inspect}
|
306
|
+
return all unless sortable_fields.include?(field)
|
307
|
+
|
308
|
+
# Validate direction
|
309
|
+
direction = direction.downcase
|
310
|
+
direction = 'asc' unless %w[asc desc].include?(direction)
|
311
|
+
|
312
|
+
order("\#{field} \#{direction}")
|
313
|
+
}
|
314
|
+
RUBY
|
315
|
+
|
316
|
+
@generated_scopes << scope_name
|
317
|
+
rescue => e
|
318
|
+
Rails.logger.error "Error generating sort scope: #{e.message}"
|
319
|
+
Rails.logger.error e.backtrace.first(5)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def register_has_scope(controller_class, scope_name)
|
324
|
+
return if controller_class.respond_to?(:has_scope_registered?) &&
|
325
|
+
controller_class.has_scope_registered?(scope_name)
|
326
|
+
|
327
|
+
controller_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
328
|
+
has_scope :#{scope_name}
|
329
|
+
RUBY
|
330
|
+
|
331
|
+
@generated_has_scopes << scope_name
|
332
|
+
rescue StandardError => e
|
333
|
+
Rails.logger.warn "Error registering has_scope #{scope_name}: #{e.message}"
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns the appropriate LIKE operator for the current database adapter
|
337
|
+
def like_operator
|
338
|
+
case @model_class.connection.adapter_name.downcase
|
339
|
+
when 'postgresql', 'postgres'
|
340
|
+
'ILIKE'
|
341
|
+
when 'mysql', 'mysql2'
|
342
|
+
'LIKE'
|
343
|
+
when 'sqlite', 'sqlite3'
|
344
|
+
'LIKE'
|
345
|
+
else
|
346
|
+
'LIKE' # Default to LIKE for unknown adapters
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Returns a list of sensitive field names that should not be sortable
|
351
|
+
def sensitive_fields
|
352
|
+
%w[
|
353
|
+
password password_digest encrypted_password
|
354
|
+
token secret_key api_key
|
355
|
+
encrypted_data encrypted_attributes
|
356
|
+
salt iv
|
357
|
+
created_at updated_at
|
358
|
+
]
|
359
|
+
end
|
360
|
+
end
|