propel_api 0.3.2 → 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 +60 -0
- data/README.md +46 -1
- data/lib/generators/propel_api/core/named_base.rb +47 -76
- data/lib/generators/propel_api/install/install_generator.rb +43 -2
- 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,474 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# PropelDynamicScopeGenerator
|
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 = PropelDynamicScopeGenerator.new(Team)
|
13
|
+
# generator.generate_scopes!
|
14
|
+
# generator.register_controller_scopes(MyController)
|
15
|
+
#
|
16
|
+
class PropelDynamicScopeGenerator
|
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
|
+
if value.present?
|
189
|
+
begin
|
190
|
+
# Parse datetime string into proper datetime object
|
191
|
+
parsed_value = case value
|
192
|
+
when String
|
193
|
+
DateTime.parse(value)
|
194
|
+
when Time, Date, DateTime
|
195
|
+
value
|
196
|
+
else
|
197
|
+
DateTime.parse(value.to_s)
|
198
|
+
end
|
199
|
+
where("#{field_name} < ?", parsed_value)
|
200
|
+
rescue ArgumentError, TypeError
|
201
|
+
all
|
202
|
+
end
|
203
|
+
else
|
204
|
+
all
|
205
|
+
end
|
206
|
+
}
|
207
|
+
RUBY
|
208
|
+
end
|
209
|
+
|
210
|
+
def generate_after_scope(scope_name, field_name)
|
211
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
212
|
+
scope :#{scope_name}, ->(value) {
|
213
|
+
if value.present?
|
214
|
+
begin
|
215
|
+
# Parse datetime string into proper datetime object
|
216
|
+
parsed_value = case value
|
217
|
+
when String
|
218
|
+
DateTime.parse(value)
|
219
|
+
when Time, Date, DateTime
|
220
|
+
value
|
221
|
+
else
|
222
|
+
DateTime.parse(value.to_s)
|
223
|
+
end
|
224
|
+
where("#{field_name} > ?", parsed_value)
|
225
|
+
rescue ArgumentError, TypeError
|
226
|
+
all
|
227
|
+
end
|
228
|
+
else
|
229
|
+
all
|
230
|
+
end
|
231
|
+
}
|
232
|
+
RUBY
|
233
|
+
end
|
234
|
+
|
235
|
+
def generate_in_scope(scope_name, field_name)
|
236
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
237
|
+
scope :#{scope_name}, ->(value) {
|
238
|
+
if value.present?
|
239
|
+
values = value.is_a?(Array) ? value : value.to_s.split(",").map(&:strip)
|
240
|
+
values.any? ? where("#{field_name} IN (?)", values) : all
|
241
|
+
else
|
242
|
+
all
|
243
|
+
end
|
244
|
+
}
|
245
|
+
RUBY
|
246
|
+
end
|
247
|
+
|
248
|
+
def generate_null_scope(scope_name, field_name)
|
249
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
250
|
+
scope :#{scope_name}, ->(value) {
|
251
|
+
if value.present?
|
252
|
+
is_null = value.to_s.downcase == "true"
|
253
|
+
is_null ? where("#{field_name} IS NULL") : where("#{field_name} IS NOT NULL")
|
254
|
+
else
|
255
|
+
all
|
256
|
+
end
|
257
|
+
}
|
258
|
+
RUBY
|
259
|
+
end
|
260
|
+
|
261
|
+
def generate_range_scope(scope_name, field_name)
|
262
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
263
|
+
scope :#{scope_name}, ->(value) {
|
264
|
+
if value.present?
|
265
|
+
# Handle both array and string formats
|
266
|
+
if value.is_a?(Array) && value.size == 2
|
267
|
+
where("#{field_name} BETWEEN ? AND ?", value[0], value[1])
|
268
|
+
elsif value.is_a?(String) && value.include?(',')
|
269
|
+
# Handle string format like "150,350"
|
270
|
+
range_values = value.split(',').map(&:strip)
|
271
|
+
if range_values.size == 2
|
272
|
+
where("#{field_name} BETWEEN ? AND ?", range_values[0], range_values[1])
|
273
|
+
else
|
274
|
+
all
|
275
|
+
end
|
276
|
+
else
|
277
|
+
all
|
278
|
+
end
|
279
|
+
else
|
280
|
+
all
|
281
|
+
end
|
282
|
+
}
|
283
|
+
RUBY
|
284
|
+
end
|
285
|
+
|
286
|
+
def generate_date_scope(scope_name, field_name)
|
287
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
288
|
+
scope :#{scope_name}, ->(value) {
|
289
|
+
if value.present?
|
290
|
+
date = value.is_a?(Date) ? value : Date.parse(value.to_s)
|
291
|
+
where("DATE(#{field_name}) = ?", date)
|
292
|
+
else
|
293
|
+
all
|
294
|
+
end
|
295
|
+
}
|
296
|
+
RUBY
|
297
|
+
end
|
298
|
+
|
299
|
+
def generate_year_scope(scope_name, field_name)
|
300
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
301
|
+
scope :#{scope_name}, ->(value) {
|
302
|
+
if value.present?
|
303
|
+
begin
|
304
|
+
# Parse the datetime value to extract year
|
305
|
+
parsed_value = case value
|
306
|
+
when String
|
307
|
+
DateTime.parse(value)
|
308
|
+
when Time, Date, DateTime
|
309
|
+
value
|
310
|
+
else
|
311
|
+
DateTime.parse(value.to_s)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Create date range for the entire year
|
315
|
+
start_of_year = parsed_value.beginning_of_year
|
316
|
+
end_of_year = parsed_value.end_of_year
|
317
|
+
|
318
|
+
where("#{field_name} >= ? AND #{field_name} <= ?", start_of_year, end_of_year)
|
319
|
+
rescue ArgumentError, TypeError
|
320
|
+
all
|
321
|
+
end
|
322
|
+
else
|
323
|
+
all
|
324
|
+
end
|
325
|
+
}
|
326
|
+
RUBY
|
327
|
+
end
|
328
|
+
|
329
|
+
def generate_month_scope(scope_name, field_name)
|
330
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
331
|
+
scope :#{scope_name}, ->(value) {
|
332
|
+
if value.present?
|
333
|
+
begin
|
334
|
+
# Parse the datetime value to extract month
|
335
|
+
parsed_value = case value
|
336
|
+
when String
|
337
|
+
DateTime.parse(value)
|
338
|
+
when Time, Date, DateTime
|
339
|
+
value
|
340
|
+
else
|
341
|
+
DateTime.parse(value.to_s)
|
342
|
+
end
|
343
|
+
|
344
|
+
# Create date range for the entire month
|
345
|
+
start_of_month = parsed_value.beginning_of_month
|
346
|
+
end_of_month = parsed_value.end_of_month
|
347
|
+
|
348
|
+
where("#{field_name} >= ? AND #{field_name} <= ?", start_of_month, end_of_month)
|
349
|
+
rescue ArgumentError, TypeError
|
350
|
+
all
|
351
|
+
end
|
352
|
+
else
|
353
|
+
all
|
354
|
+
end
|
355
|
+
}
|
356
|
+
RUBY
|
357
|
+
end
|
358
|
+
|
359
|
+
def generate_day_scope(scope_name, field_name)
|
360
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
361
|
+
scope :#{scope_name}, ->(value) {
|
362
|
+
if value.present?
|
363
|
+
begin
|
364
|
+
# Parse the datetime value to extract day
|
365
|
+
parsed_value = case value
|
366
|
+
when String
|
367
|
+
DateTime.parse(value)
|
368
|
+
when Time, Date, DateTime
|
369
|
+
value
|
370
|
+
else
|
371
|
+
DateTime.parse(value.to_s)
|
372
|
+
end
|
373
|
+
|
374
|
+
# Create date range for the entire day
|
375
|
+
start_of_day = parsed_value.beginning_of_day
|
376
|
+
end_of_day = parsed_value.end_of_day
|
377
|
+
|
378
|
+
where("#{field_name} >= ? AND #{field_name} <= ?", start_of_day, end_of_day)
|
379
|
+
rescue ArgumentError, TypeError
|
380
|
+
all
|
381
|
+
end
|
382
|
+
else
|
383
|
+
all
|
384
|
+
end
|
385
|
+
}
|
386
|
+
RUBY
|
387
|
+
end
|
388
|
+
|
389
|
+
def generate_sort_scope
|
390
|
+
scope_name = :order_by
|
391
|
+
return if @generated_scopes.include?(scope_name)
|
392
|
+
|
393
|
+
begin
|
394
|
+
# Get all sortable columns (exclude sensitive fields)
|
395
|
+
sortable_columns = @model_class.columns.reject do |column|
|
396
|
+
sensitive_fields.include?(column.name.to_s)
|
397
|
+
end.map(&:name)
|
398
|
+
|
399
|
+
@model_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
400
|
+
scope :#{scope_name}, ->(value) {
|
401
|
+
return all if value.blank?
|
402
|
+
|
403
|
+
# Parse sort parameter (e.g., "name", "-name", "name:asc", "name:desc")
|
404
|
+
field = value.to_s
|
405
|
+
direction = 'asc'
|
406
|
+
|
407
|
+
# Handle "-field" format for descending
|
408
|
+
if field.start_with?('-')
|
409
|
+
field = field[1..-1]
|
410
|
+
direction = 'desc'
|
411
|
+
else
|
412
|
+
# Handle "field:direction" format
|
413
|
+
field, direction = field.split(':')
|
414
|
+
direction ||= 'asc'
|
415
|
+
end
|
416
|
+
|
417
|
+
# Validate field is sortable
|
418
|
+
sortable_fields = #{sortable_columns.inspect}
|
419
|
+
return all unless sortable_fields.include?(field)
|
420
|
+
|
421
|
+
# Validate direction
|
422
|
+
direction = direction.downcase
|
423
|
+
direction = 'asc' unless %w[asc desc].include?(direction)
|
424
|
+
|
425
|
+
order("\#{field} \#{direction}")
|
426
|
+
}
|
427
|
+
RUBY
|
428
|
+
|
429
|
+
@generated_scopes << scope_name
|
430
|
+
rescue => e
|
431
|
+
Rails.logger.error "Error generating sort scope: #{e.message}"
|
432
|
+
Rails.logger.error e.backtrace.first(5)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
def register_has_scope(controller_class, scope_name)
|
437
|
+
return if controller_class.respond_to?(:has_scope_registered?) &&
|
438
|
+
controller_class.has_scope_registered?(scope_name)
|
439
|
+
|
440
|
+
controller_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
441
|
+
has_scope :#{scope_name}
|
442
|
+
RUBY
|
443
|
+
|
444
|
+
@generated_has_scopes << scope_name
|
445
|
+
rescue StandardError => e
|
446
|
+
Rails.logger.warn "Error registering has_scope #{scope_name}: #{e.message}"
|
447
|
+
end
|
448
|
+
|
449
|
+
# Returns the appropriate LIKE operator for the current database adapter
|
450
|
+
def like_operator
|
451
|
+
case @model_class.connection.adapter_name.downcase
|
452
|
+
when 'postgresql', 'postgres'
|
453
|
+
'ILIKE'
|
454
|
+
when 'mysql', 'mysql2'
|
455
|
+
'LIKE'
|
456
|
+
when 'sqlite', 'sqlite3'
|
457
|
+
'LIKE'
|
458
|
+
else
|
459
|
+
'LIKE' # Default to LIKE for unknown adapters
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Returns a list of sensitive field names that should not be sortable
|
464
|
+
def sensitive_fields
|
465
|
+
%w[
|
466
|
+
password password_digest encrypted_password
|
467
|
+
token secret_key api_key
|
468
|
+
encrypted_data encrypted_attributes
|
469
|
+
salt iv
|
470
|
+
created_at updated_at
|
471
|
+
]
|
472
|
+
end
|
473
|
+
|
474
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module PropelFilterOperators
|
2
|
+
def self.operators_for(type, sql_type = nil)
|
3
|
+
case type
|
4
|
+
when :string, :text
|
5
|
+
%w[eq contains starts_with ends_with in]
|
6
|
+
when :integer, :float, :decimal
|
7
|
+
%w[eq gt lt gte lte min max range in]
|
8
|
+
when :boolean
|
9
|
+
%w[eq null]
|
10
|
+
when :datetime, :date
|
11
|
+
%w[before after date year month day]
|
12
|
+
else
|
13
|
+
sql_type == "jsonb" ? %w[eq contains starts_with ends_with gt lt null in range before after] : []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -117,6 +117,20 @@ json_facet :short, fields: [:id<%
|
|
117
117
|
# Always exclude if the field contains security-sensitive words
|
118
118
|
security_patterns = /\A(password|password_digest|password_confirmation|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)\z|.*(_digest|_token|_secret|_key|_salt|_encrypted)$/i
|
119
119
|
|
120
|
+
# Include all boolean fields except specific ones that shouldn't be in short facet
|
121
|
+
if attr.type == :boolean
|
122
|
+
excluded_boolean_fields = %w[internal_flag system_generated admin_only debug_mode test_mode]
|
123
|
+
next false if excluded_boolean_fields.include?(attr.name.to_s)
|
124
|
+
next attr
|
125
|
+
end
|
126
|
+
|
127
|
+
# Include all datetime fields except specific ones that shouldn't be in short facet
|
128
|
+
if [:datetime, :date, :time].include?(attr.type)
|
129
|
+
excluded_datetime_fields = %w[confirmed_at]
|
130
|
+
next false if excluded_datetime_fields.include?(attr.name.to_s)
|
131
|
+
next attr
|
132
|
+
end
|
133
|
+
|
120
134
|
identifying_fields.include?(attr.name.to_s) ||
|
121
135
|
(simple_types.include?(attr.type) &&
|
122
136
|
attr.name.to_s !~ excluded_patterns &&
|
@@ -193,9 +193,60 @@ end
|
|
193
193
|
<% unless (has_organization_reference? && reference.name == 'organization') || (has_agency_reference? && reference.name == 'agency') -%>
|
194
194
|
<% if reference.name.to_s.match?(/user/) -%>
|
195
195
|
<%= singular_table_name %>_attributes[:<%= reference.name %>] = user
|
196
|
+
<% elsif reference.name.to_s.match?(/agent/) -%>
|
197
|
+
<% if has_organization_reference? -%>
|
198
|
+
# Find or create agent for the user within the same organization/agency
|
199
|
+
agent = Agent.find_by(user: user, organization: organization, agency: agency)
|
200
|
+
unless agent
|
201
|
+
agent = Agent.create!(user: user, organization: organization, agency: agency, role: 'member')
|
202
|
+
end
|
203
|
+
<%= singular_table_name %>_attributes[:<%= reference.name %>] = agent
|
204
|
+
<% else -%>
|
205
|
+
# Find or create agent for the user (no tenancy)
|
206
|
+
agent = Agent.find_by(user: user) || Agent.create!(user: user, agency: agencies.sample, organization: organizations.sample, role: 'member')
|
207
|
+
<%= singular_table_name %>_attributes[:<%= reference.name %>] = agent
|
208
|
+
<% end -%>
|
209
|
+
<% elsif reference.name.to_s.include?('parent') -%>
|
210
|
+
# Handle parent relationships (often self-referential)
|
211
|
+
<% parent_class_name = reference.name.to_s.gsub(/parent$/, '').camelize -%>
|
212
|
+
<% if parent_class_name.blank? -%>
|
213
|
+
<% parent_class_name = class_name -%>
|
214
|
+
<% end -%>
|
215
|
+
# Self-referential or parent relationship: skip to avoid circular dependencies in initial seeding
|
216
|
+
# This will be handled in a second pass after main records are created
|
217
|
+
# <%= singular_table_name %>_attributes[:<%= reference.name %>] = nil
|
218
|
+
<% elsif reference.name.to_s.match?(/meeting/) -%>
|
219
|
+
# Handle meeting references - find existing meetings from the same organization/agency
|
220
|
+
<% if has_organization_reference? -%>
|
221
|
+
available_meetings = Meeting.where(organization: organization).limit(10).to_a
|
222
|
+
<% else -%>
|
223
|
+
available_meetings = Meeting.limit(10).to_a
|
224
|
+
<% end -%>
|
225
|
+
<%= singular_table_name %>_attributes[:<%= reference.name %>] = available_meetings.sample if available_meetings.any?
|
226
|
+
<% else -%>
|
227
|
+
# Handle other reference types with generic logic
|
228
|
+
<% reference_class_name = reference.name.to_s.classify -%>
|
229
|
+
begin
|
230
|
+
# Try to find existing records of the reference type, preferably from the same organization
|
231
|
+
<% if has_organization_reference? -%>
|
232
|
+
if defined?(<%= reference_class_name %>) && <%= reference_class_name %>.respond_to?(:where)
|
233
|
+
if <%= reference_class_name %>.column_names.include?('organization_id')
|
234
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.where(organization: organization).limit(10).to_a
|
235
|
+
else
|
236
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.limit(10).to_a
|
237
|
+
end
|
238
|
+
<%= singular_table_name %>_attributes[:<%= reference.name %>] = available_<%= reference.name.pluralize %>.sample if available_<%= reference.name.pluralize %>.any?
|
239
|
+
end
|
196
240
|
<% else -%>
|
197
|
-
|
198
|
-
|
241
|
+
if defined?(<%= reference_class_name %>) && <%= reference_class_name %>.respond_to?(:where)
|
242
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.limit(10).to_a
|
243
|
+
<%= singular_table_name %>_attributes[:<%= reference.name %>] = available_<%= reference.name.pluralize %>.sample if available_<%= reference.name.pluralize %>.any?
|
244
|
+
end
|
245
|
+
<% end -%>
|
246
|
+
rescue => e
|
247
|
+
# Skip unknown reference types to avoid breaking seeds
|
248
|
+
puts "⚠️ Skipped unknown reference '<%= reference.name %>': #{e.message}" if Rails.env.development?
|
249
|
+
end
|
199
250
|
<% end -%>
|
200
251
|
<% end -%>
|
201
252
|
<% end -%>
|
@@ -367,9 +418,39 @@ organizations.each do |org|
|
|
367
418
|
<% unless reference.name == 'organization' || reference.name == 'agency' -%>
|
368
419
|
<% if reference.name.to_s.match?(/user/) -%>
|
369
420
|
minimal_<%= singular_table_name %>[:<%= reference.name %>] = user
|
421
|
+
<% elsif reference.name.to_s.match?(/agent/) -%>
|
422
|
+
<% if has_organization_reference? -%>
|
423
|
+
# Find or create agent for the user within the same organization/agency
|
424
|
+
agent = Agent.find_by(user: user, organization: org, agency: agency)
|
425
|
+
unless agent
|
426
|
+
agent = Agent.create!(user: user, organization: org, agency: agency, role: 'member')
|
427
|
+
end
|
428
|
+
minimal_<%= singular_table_name %>[:<%= reference.name %>] = agent
|
370
429
|
<% else -%>
|
371
|
-
#
|
372
|
-
|
430
|
+
# Find or create agent for the user (no tenancy)
|
431
|
+
agent = Agent.find_by(user: user) || Agent.create!(user: user, agency: agencies.sample, organization: organizations.sample, role: 'member')
|
432
|
+
minimal_<%= singular_table_name %>[:<%= reference.name %>] = agent
|
433
|
+
<% end -%>
|
434
|
+
<% else -%>
|
435
|
+
# Handle other reference types with generic logic (test scenario minimal case)
|
436
|
+
<% reference_class_name = reference.name.to_s.classify -%>
|
437
|
+
begin
|
438
|
+
if defined?(<%= reference_class_name %>) && <%= reference_class_name %>.respond_to?(:where)
|
439
|
+
<% if has_organization_reference? -%>
|
440
|
+
if <%= reference_class_name %>.column_names.include?('organization_id')
|
441
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.where(organization: org).limit(5).to_a
|
442
|
+
else
|
443
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.limit(5).to_a
|
444
|
+
end
|
445
|
+
<% else -%>
|
446
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.limit(5).to_a
|
447
|
+
<% end -%>
|
448
|
+
minimal_<%= singular_table_name %>[:<%= reference.name %>] = available_<%= reference.name.pluralize %>.sample if available_<%= reference.name.pluralize %>.any?
|
449
|
+
end
|
450
|
+
rescue => e
|
451
|
+
# Skip unknown reference types in minimal test scenarios
|
452
|
+
puts "⚠️ Skipped minimal reference '<%= reference.name %>': #{e.message}" if Rails.env.development?
|
453
|
+
end
|
373
454
|
<% end -%>
|
374
455
|
<% end -%>
|
375
456
|
<% end -%>
|
@@ -443,9 +524,22 @@ end
|
|
443
524
|
<% attributes.select { |attr| attr.type == :references && !(attr.respond_to?(:polymorphic?) && attr.polymorphic?) }.each do |reference| -%>
|
444
525
|
<% if reference.name.to_s.match?(/user/) -%>
|
445
526
|
minimal_<%= singular_table_name %>[:<%= reference.name %>] = users.sample if users.any?
|
527
|
+
<% elsif reference.name.to_s.match?(/agent/) -%>
|
528
|
+
# Find or create agent for the user (simple case for minimal scenario)
|
529
|
+
agent = Agent.find_by(user: users.sample) || Agent.create!(user: users.sample, agency: agencies.sample, organization: organizations.sample, role: 'member')
|
530
|
+
minimal_<%= singular_table_name %>[:<%= reference.name %>] = agent
|
446
531
|
<% else -%>
|
447
|
-
# Handle other reference types
|
448
|
-
|
532
|
+
# Handle other reference types with generic logic (minimal scenario)
|
533
|
+
<% reference_class_name = reference.name.to_s.classify -%>
|
534
|
+
begin
|
535
|
+
if defined?(<%= reference_class_name %>) && <%= reference_class_name %>.respond_to?(:where)
|
536
|
+
available_<%= reference.name.pluralize %> = <%= reference_class_name %>.limit(5).to_a
|
537
|
+
minimal_<%= singular_table_name %>[:<%= reference.name %>] = available_<%= reference.name.pluralize %>.sample if available_<%= reference.name.pluralize %>.any?
|
538
|
+
end
|
539
|
+
rescue => e
|
540
|
+
# Skip unknown reference types in minimal scenarios
|
541
|
+
puts "⚠️ Skipped minimal reference '<%= reference.name %>': #{e.message}" if Rails.env.development?
|
542
|
+
end
|
449
543
|
<% end -%>
|
450
544
|
<% end -%>
|
451
545
|
|
@@ -42,6 +42,8 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
|
|
42
42
|
@<%= singular_table_name %> = <%= table_name %>(:john_user)
|
43
43
|
<% elsif singular_table_name == 'agency' -%>
|
44
44
|
@<%= singular_table_name %> = <%= table_name %>(:marketing_agency)
|
45
|
+
<% elsif singular_table_name == 'agent' -%>
|
46
|
+
@<%= singular_table_name %> = <%= table_name %>(:john_marketing_agent)
|
45
47
|
<% else -%>
|
46
48
|
@<%= singular_table_name %> = <%= table_name %>(:one)
|
47
49
|
<% end -%>
|
@@ -681,6 +683,13 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
|
|
681
683
|
excluded_patterns = /\A(description|content|body|notes|comment|bio|about|summary|created_at|updated_at|deleted_at|password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)\z/i
|
682
684
|
security_patterns = /(password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key)/i
|
683
685
|
|
686
|
+
# Include all boolean fields except specific ones that shouldn't be in short facet
|
687
|
+
if attr.type == :boolean
|
688
|
+
excluded_boolean_fields = %w[internal_flag system_generated admin_only debug_mode test_mode]
|
689
|
+
next false if excluded_boolean_fields.include?(attr.name.to_s)
|
690
|
+
next attr
|
691
|
+
end
|
692
|
+
|
684
693
|
identifying_fields.include?(attr.name.to_s) ||
|
685
694
|
(simple_types.include?(attr.type) &&
|
686
695
|
attr.name.to_s !~ excluded_patterns &&
|