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.
@@ -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
- # Handle other reference types - you may need to customize this
198
- # <%= singular_table_name %>_attributes[:<%= reference.name %>] = # TODO: Assign appropriate value
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
- # Handle other reference types as needed
372
- # minimal_<%= singular_table_name %>[:<%= reference.name %>] = # TODO: Assign appropriate value
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 as needed
448
- # minimal_<%= singular_table_name %>[:<%= reference.name %>] = # TODO: Assign appropriate value
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 &&