propel_api 0.2.1 → 0.3.1

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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +239 -4
  4. data/lib/generators/propel_api/controller/controller_generator.rb +26 -0
  5. data/lib/generators/propel_api/core/named_base.rb +97 -1
  6. data/lib/generators/propel_api/core/relationship_inferrer.rb +4 -11
  7. data/lib/generators/propel_api/install/install_generator.rb +39 -2
  8. data/lib/generators/propel_api/resource/resource_generator.rb +205 -63
  9. data/lib/generators/propel_api/templates/config/propel_api.rb.tt +26 -1
  10. data/lib/generators/propel_api/templates/controllers/api_base_controller.rb +75 -0
  11. data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +3 -3
  12. data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +22 -12
  13. data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +2 -2
  14. data/lib/generators/propel_api/templates/errors/propel_api_csrf_error.rb +19 -0
  15. data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +41 -8
  16. data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +43 -10
  17. data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +1 -1
  18. data/lib/generators/propel_api/templates/scaffold/graphiti_resource_template.rb.tt +2 -2
  19. data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +65 -17
  20. data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +40 -6
  21. data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +61 -18
  22. data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +154 -42
  23. data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +20 -0
  24. data/lib/propel_api.rb +1 -1
  25. metadata +24 -2
@@ -10,15 +10,48 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
10
10
  # Define which parameters are allowed for <%= class_name %> creation/updates
11
11
  # This uses the StrongParamsHelper concern from the base controller
12
12
  <% end -%>
13
- permitted_params <%= permitted_param_names.map { |param|
14
- # If param already contains colon (JSON field syntax), use as-is
15
- # Otherwise add colon prefix for regular fields
16
- if param.to_s.include?(':')
17
- param # Already formatted as "field: {}" for JSON fields
13
+ permitted_params <%
14
+ # Generate permitted parameters using shared polymorphic support
15
+ regular_params = []
16
+ json_params = []
17
+
18
+ # Add attribute-based params using polymorphic support
19
+ attributes.each do |attr|
20
+ if attr.type == :json
21
+ json_params << "#{attr.name}: {}"
22
+ elsif attr.type == :references
23
+ # Use Rails' built-in polymorphic detection
24
+ if attr.respond_to?(:polymorphic?) && attr.polymorphic?
25
+ regular_params << ":#{attr.name}_id"
26
+ regular_params << ":#{attr.name}_type"
27
+ else
28
+ regular_params << ":#{attr.name}_id"
29
+ end
30
+ else
31
+ regular_params << ":#{attr.name}"
32
+ end
33
+ end
34
+
35
+ # Add multi-tenancy params unless skipped
36
+ if has_organization_reference?
37
+ regular_params += [":organization_id"]
38
+ end
39
+
40
+ if has_agency_reference?
41
+ regular_params += [":agency_id"]
42
+ end
43
+
44
+ # Remove organization/agency duplicates if they were user-specified
45
+ regular_params.uniq!
46
+
47
+ # Combine regular and JSON params properly
48
+ if json_params.any?
49
+ all_params = regular_params + json_params
18
50
  else
19
- ":#{param}" # Add colon prefix for regular fields
51
+ all_params = regular_params
20
52
  end
21
- }.join(', ') %>
53
+
54
+ -%><%= all_params.join(', ') %>
22
55
 
23
56
  <% if options[:with_comments] -%>
24
57
  # Connect facets to actions - customize as needed for your <%= class_name.downcase %> model
@@ -84,7 +117,7 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
84
117
  # if @resource.save
85
118
  # render json: { data: resource_json(@resource) }, status: :created
86
119
  # else
87
- # render json: { errors: @resource.errors }, status: :unprocessable_entity
120
+ # render json: { errors: @resource.errors }, status: :unprocessable_content
88
121
  # end
89
122
  # end
90
123
  #
@@ -3,7 +3,7 @@
3
3
  class <%= class_name %> < ApplicationRecord
4
4
 
5
5
  # Validations
6
- <% unless options[:skip_tenancy] -%>
6
+ <% if has_organization_reference? -%>
7
7
  # Multi-tenancy: Organization is always required
8
8
  validates :organization, presence: true
9
9
 
@@ -24,7 +24,7 @@ class <%= class_name %> < ApplicationRecord
24
24
  <% if attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
25
25
  validates :<%= attribute.name %>, format: { with: /\A\+?[\d\s-\(\)]+\z/ }, allow_nil: <%= !attribute.required? %>
26
26
  <% end -%>
27
- <% if attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
27
+ <% if attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
28
28
  validates :<%= attribute.name %>, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }, allow_nil: <%= !attribute.required? %>
29
29
  <% end -%>
30
30
  <% if attribute.name.to_s.match?(/\Alatitude\z/i) -%>
@@ -45,32 +45,40 @@ class <%= class_name %> < ApplicationRecord
45
45
  <% end -%>
46
46
 
47
47
  # Associations
48
- <% unless options[:skip_tenancy] -%>
48
+ <% if has_organization_reference? -%>
49
49
  # Multi-tenancy: Always include organization and agency for data isolation
50
50
  # User/Agent associations are only added when explicitly specified as generator arguments
51
51
  belongs_to :organization
52
+ <% end -%>
53
+ <% if has_agency_reference? -%>
52
54
  belongs_to :agency
55
+ <% end -%>
56
+ <% if has_organization_reference? || has_agency_reference? -%>
53
57
 
54
58
  <% end -%>
55
59
  <% if defined?(relationship_inferrer) && relationship_inferrer.should_generate_associations? -%>
56
60
  <% relationship_inferrer.belongs_to_relationships.each do |relationship| -%>
57
61
  <% # Skip organization and agency if tenancy is included (they're already added above) -%>
58
- <% unless !options[:skip_tenancy] && (relationship.include?('organization') || relationship.include?('agency')) -%>
62
+ <% unless (has_organization_reference? && relationship.include?('organization')) || (has_agency_reference? && relationship.include?('agency')) -%>
59
63
  <%= relationship %>
60
64
  <% end -%>
61
65
  <% end -%>
62
66
  <% elsif !options[:skip_associations] -%>
63
67
  <% attributes.select { |attr| attr.type == :references }.each do |attribute| -%>
64
68
  <% # Skip organization and agency if tenancy is included (they're already added above) -%>
65
- <% unless !options[:skip_tenancy] && (attribute.name == 'organization' || attribute.name == 'agency') -%>
69
+ <% unless (has_organization_reference? && attribute.name == 'organization') || (has_agency_reference? && attribute.name == 'agency') -%>
66
70
  <% if attribute.name == 'organization' -%>
67
71
  belongs_to :organization
72
+ <% else -%>
73
+ <% if attribute.respond_to?(:polymorphic?) && attribute.polymorphic? -%>
74
+ belongs_to :<%= attribute.name %>, polymorphic: true
68
75
  <% else -%>
69
76
  belongs_to :<%= attribute.name %> # Add 'optional: true' if this should be optional
70
77
  <% end -%>
71
78
  <% end -%>
72
79
  <% end -%>
73
80
  <% end -%>
81
+ <% end -%>
74
82
  # Add additional associations here
75
83
  # has_many :comments, dependent: :destroy
76
84
 
@@ -105,7 +113,13 @@ json_facet :short, fields: [:id<%
105
113
  attr.name.to_s !~ security_patterns &&
106
114
  attr.name.to_s.length < 20)
107
115
  end
108
- short_attributes.each do |attribute| -%>, :<%= attribute.name %><% end %>]
116
+
117
+ # Add polymorphic type fields to short facet (important for frontend handling)
118
+ polymorphic_type_fields = attributes.select { |attr| attr.name.end_with?('_type') && attr.type == :string }
119
+ # Also include polymorphic associations (which generate _type fields)
120
+ polymorphic_associations = attributes.select { |attr| attr.type == :references && attr.respond_to?(:polymorphic?) && attr.polymorphic? }
121
+
122
+ short_attributes.each do |attribute| -%>, :<%= attribute.name %><% end %><% polymorphic_type_fields.each do |type_field| -%>, :<%= type_field.name %><% end %><% polymorphic_associations.each do |poly_attr| -%>, :<%= poly_attr.name %>_type<% end %>]
109
123
  json_facet :details, fields: [:id<%
110
124
  # Include most fields except timestamps, security-sensitive, and internal Rails fields for details facet
111
125
  detail_attributes = attributes.reject do |attr|
@@ -124,6 +138,11 @@ json_facet :details, fields: [:id<%
124
138
  # Collect all field names, avoiding duplicates
125
139
  all_fields = Set.new
126
140
 
141
+ # Find polymorphic type fields
142
+ polymorphic_type_fields = attributes.select { |attr| attr.name.end_with?('_type') && attr.type == :string }
143
+ # Also find polymorphic associations (which generate _type fields)
144
+ polymorphic_associations = attributes.select { |attr| attr.type == :references && attr.respond_to?(:polymorphic?) && attr.polymorphic? }
145
+
127
146
  # Add non-reference attributes
128
147
  detail_attributes.reject { |attr| attr.type == :references }.each do |attribute|
129
148
  all_fields << attribute.name.to_sym
@@ -134,19 +153,33 @@ json_facet :details, fields: [:id<%
134
153
  all_fields << "#{reference.name}_id".to_sym
135
154
  end
136
155
 
137
- # Add tenancy references if not skipped
138
- unless options[:skip_tenancy]
156
+ # Add tenancy references if present
157
+ if has_organization_reference?
139
158
  all_fields << :organization_id
159
+ end
160
+ if has_agency_reference?
140
161
  all_fields << :agency_id
141
162
  end
142
163
 
164
+ # Add polymorphic type fields to details facet (important for frontend handling)
165
+ polymorphic_type_fields.each do |type_field|
166
+ all_fields << type_field.name.to_sym
167
+ end
168
+ # Also add polymorphic association type fields
169
+ polymorphic_associations.each do |poly_attr|
170
+ all_fields << "#{poly_attr.name}_type".to_sym
171
+ end
172
+
143
173
  # Output the unique fields
144
174
  all_fields.to_a.sort.each do |field| -%>, :<%= field %><% end -%>]<%
145
175
 
146
176
  # Generate include for nested objects (reference associations)
147
177
  reference_associations = detail_attributes.select { |attr| attr.type == :references }.map(&:name)
148
- unless options[:skip_tenancy]
149
- reference_associations += %w[organization agency]
178
+ if has_organization_reference?
179
+ reference_associations += %w[organization]
180
+ end
181
+ if has_agency_reference?
182
+ reference_associations += %w[agency]
150
183
  end
151
184
  reference_associations.uniq!
152
185
 
@@ -55,7 +55,7 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
55
55
  # if resource_instance.save
56
56
  # respond_with(resource_instance, status: :created)
57
57
  # else
58
- # respond_with(resource_instance.errors, status: :unprocessable_entity)
58
+ # respond_with(resource_instance.errors, status: :unprocessable_content)
59
59
  # end
60
60
  # end
61
61
  #
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- <% unless options[:skip_tenancy] -%>
3
+ <% if has_organization_reference? || has_agency_reference? -%>
4
4
  require_relative '../concerns/tenancy'
5
5
  <% end -%>
6
6
 
@@ -20,7 +20,7 @@ require_relative '../concerns/tenancy'
20
20
  # @see https://www.graphiti.dev/guides/concepts/resources
21
21
  #
22
22
  class <%= class_name %>Resource < ApplicationResource
23
- <% unless options[:skip_tenancy] -%>
23
+ <% if has_organization_reference? || has_agency_reference? -%>
24
24
  include Tenancy
25
25
  <% end -%>
26
26
 
@@ -13,7 +13,7 @@ puts "📋 Loading fixture data for consistent test/dev experience..."
13
13
 
14
14
  # Only load fixtures if not already in development environment with existing data
15
15
  fixture_count = 0
16
- <% unless options[:skip_tenancy] -%>
16
+ <% if has_organization_reference? || has_agency_reference? -%>
17
17
  if Rails.env.development? && (Organization.count > 0 || User.count > 0)
18
18
  <% else -%>
19
19
  <% if attributes.any? { |attr| attr.type == :references && attr.name.to_s.match?(/user/) } -%>
@@ -31,8 +31,10 @@ else
31
31
  begin
32
32
  # Load all necessary fixtures together to handle dependencies
33
33
  fixture_files = ['<%= table_name %>']
34
- <% unless options[:skip_tenancy] -%>
34
+ <% if has_organization_reference? -%>
35
35
  fixture_files << 'organizations'
36
+ <% end -%>
37
+ <% if has_agency_reference? -%>
36
38
  fixture_files << 'agencies'
37
39
  <% end -%>
38
40
  <% if attributes.any? { |attr| attr.type == :references && attr.name.to_s.match?(/user/) } -%>
@@ -67,8 +69,10 @@ end
67
69
  puts "🎨 Generating additional realistic <%= table_name %> with Faker..."
68
70
 
69
71
  # Use existing organizations, agencies, and users (from fixtures + any additional ones)
70
- <% unless options[:skip_tenancy] -%>
72
+ <% if has_organization_reference? -%>
71
73
  organizations = Organization.all
74
+ <% end -%>
75
+ <% if has_agency_reference? -%>
72
76
  agencies = Agency.all
73
77
 
74
78
  # If we still don't have enough orgs, create some with Faker
@@ -102,7 +106,7 @@ users = User.all
102
106
 
103
107
  if users.count < 6
104
108
  puts "➕ Creating additional users..."
105
- <% unless options[:skip_tenancy] -%>
109
+ <% if has_organization_reference? -%>
106
110
  organizations.each do |org|
107
111
  next if users.where(organization: org).count >= 2
108
112
 
@@ -122,7 +126,7 @@ if users.count < 6
122
126
  if org_agencies.any?
123
127
  # Assign to random agency within organization
124
128
  agency = org_agencies.sample
125
- Agent.create!(user: new_user, agency: agency, role: 'member')
129
+ Agent.create!(user: new_user, agency: agency, organization: org, role: 'member')
126
130
  end
127
131
  end
128
132
  end
@@ -145,12 +149,14 @@ end
145
149
 
146
150
  # Generate varied, realistic data based on the model attributes
147
151
  30.times do |i|
148
- <% unless options[:skip_tenancy] -%>
152
+ <% if has_organization_reference? -%>
149
153
  organization = organizations.sample
154
+ <% end -%>
155
+ <% if has_agency_reference? -%>
150
156
  agency = agencies.where(organization: organization).sample || agencies.sample
151
157
  <% end -%>
152
158
  <% if attributes.any? { |attr| attr.type == :references && attr.name.to_s.match?(/user/) } -%>
153
- <% unless options[:skip_tenancy] -%>
159
+ <% if has_organization_reference? -%>
154
160
  user = users.where(organization: organization).sample || users.sample
155
161
  <% else -%>
156
162
  user = users.sample
@@ -159,16 +165,32 @@ end
159
165
 
160
166
  <%= singular_table_name %>_attributes = {}
161
167
 
162
- <% unless options[:skip_tenancy] -%>
163
- # Multi-tenancy: Always assign organization (required) and agency (optional)
168
+ <% if has_organization_reference? -%>
169
+ # Multi-tenancy: Assign organization
164
170
  <%= singular_table_name %>_attributes[:organization] = organization
165
- <%= singular_table_name %>_attributes[:agency] = [agency, nil].sample # 50% chance of having agency
171
+ <% end -%>
172
+ <% if has_agency_reference? -%>
173
+ # Multi-tenancy: Assign agency
174
+ <%= singular_table_name %>_attributes[:agency] = agency # Always assign agency for valid records
175
+ <% end -%>
176
+ <% if has_organization_reference? || has_agency_reference? -%>
166
177
 
167
178
  <% end -%>
179
+ # Handle polymorphic associations
180
+ <% polymorphic_associations.each do |poly_assoc| -%>
181
+ # Assign polymorphic association: <%= poly_assoc[:field_name] %>
182
+ available_parents = <%= poly_assoc[:parent_types].map { |type| "#{type}.limit(5).to_a" }.join(' + ') %>
183
+ if available_parents.any?
184
+ selected_parent = available_parents.sample
185
+ <%= singular_table_name %>_attributes[:<%= poly_assoc[:id_field] %>] = selected_parent.id
186
+ <%= singular_table_name %>_attributes[:<%= poly_assoc[:type_field] %>] = selected_parent.class.name
187
+ end
188
+ <% end -%>
189
+
168
190
  # Assign additional reference attributes dynamically
169
- <% attributes.select { |attr| attr.type == :references }.each do |reference| -%>
191
+ <% attributes.select { |attr| attr.type == :references && !(attr.respond_to?(:polymorphic?) && attr.polymorphic?) }.each do |reference| -%>
170
192
  <% # Skip organization and agency if tenancy is included (they're already assigned above) -%>
171
- <% unless !options[:skip_tenancy] && (reference.name == 'organization' || reference.name == 'agency') -%>
193
+ <% unless (has_organization_reference? && reference.name == 'organization') || (has_agency_reference? && reference.name == 'agency') -%>
172
194
  <% if reference.name.to_s.match?(/user/) -%>
173
195
  <%= singular_table_name %>_attributes[:<%= reference.name %>] = user
174
196
  <% else -%>
@@ -189,7 +211,7 @@ end
189
211
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.unique.username
190
212
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
191
213
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::PhoneNumber.phone_number
192
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
214
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
193
215
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.url
194
216
  <% elsif attribute.name.to_s.match?(/\A(slug)\z/i) -%>
195
217
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.slug
@@ -312,9 +334,11 @@ puts "\n✅ Created #{<%= class_name %>.count} <%= table_name %>"
312
334
  puts "🎯 Creating specific test scenarios..."
313
335
 
314
336
  # Create some <%= table_name %> with edge case data
315
- <% unless options[:skip_tenancy] -%>
337
+ <% if has_organization_reference? -%>
316
338
  organizations.each do |org|
339
+ <% if has_agency_reference? -%>
317
340
  agency = agencies.where(organization: org).sample
341
+ <% end -%>
318
342
  <% if attributes.any? { |attr| attr.type == :references && attr.name.to_s.match?(/user/) } -%>
319
343
  user = users.where(organization: org).sample
320
344
  <% end -%>
@@ -326,8 +350,19 @@ organizations.each do |org|
326
350
  minimal_<%= singular_table_name %>[:organization] = org
327
351
  minimal_<%= singular_table_name %>[:agency] = agency
328
352
 
353
+ # Handle polymorphic associations
354
+ <% polymorphic_associations.each do |poly_assoc| -%>
355
+ # Assign polymorphic association: <%= poly_assoc[:field_name] %>
356
+ available_parents = <%= poly_assoc[:parent_types].map { |type| "#{type}.limit(5).to_a" }.join(' + ') %>
357
+ if available_parents.any?
358
+ selected_parent = available_parents.sample
359
+ minimal_<%= singular_table_name %>[:<%= poly_assoc[:id_field] %>] = selected_parent.id
360
+ minimal_<%= singular_table_name %>[:<%= poly_assoc[:type_field] %>] = selected_parent.class.name
361
+ end
362
+ <% end -%>
363
+
329
364
  # Assign additional reference attributes dynamically
330
- <% attributes.select { |attr| attr.type == :references }.each do |reference| -%>
365
+ <% attributes.select { |attr| attr.type == :references && !(attr.respond_to?(:polymorphic?) && attr.polymorphic?) }.each do |reference| -%>
331
366
  <% # Skip organization and agency if tenancy is included (they're already assigned above) -%>
332
367
  <% unless reference.name == 'organization' || reference.name == 'agency' -%>
333
368
  <% if reference.name.to_s.match?(/user/) -%>
@@ -393,8 +428,19 @@ end
393
428
  # Create a minimal <%= singular_table_name %> (only required fields)
394
429
  minimal_<%= singular_table_name %> = {}
395
430
 
431
+ # Handle polymorphic associations
432
+ <% polymorphic_associations.each do |poly_assoc| -%>
433
+ # Assign polymorphic association: <%= poly_assoc[:field_name] %>
434
+ available_parents = <%= poly_assoc[:parent_types].map { |type| "#{type}.limit(5).to_a" }.join(' + ') %>
435
+ if available_parents.any?
436
+ selected_parent = available_parents.sample
437
+ minimal_<%= singular_table_name %>[:<%= poly_assoc[:id_field] %>] = selected_parent.id
438
+ minimal_<%= singular_table_name %>[:<%= poly_assoc[:type_field] %>] = selected_parent.class.name
439
+ end
440
+ <% end -%>
441
+
396
442
  # Assign reference attributes dynamically
397
- <% attributes.select { |attr| attr.type == :references }.each do |reference| -%>
443
+ <% attributes.select { |attr| attr.type == :references && !(attr.respond_to?(:polymorphic?) && attr.polymorphic?) }.each do |reference| -%>
398
444
  <% if reference.name.to_s.match?(/user/) -%>
399
445
  minimal_<%= singular_table_name %>[:<%= reference.name %>] = users.sample if users.any?
400
446
  <% else -%>
@@ -466,8 +512,10 @@ puts "📊 Summary:"
466
512
  puts " • Total <%= table_name %>: #{total_<%= table_name %>}"
467
513
  puts " • └── Fixture <%= table_name %>: #{fixture_<%= table_name %>} (same as test data)"
468
514
  puts " • └── Faker <%= table_name %>: #{faker_<%= table_name %>} (realistic dev data)"
469
- <% unless options[:skip_tenancy] -%>
515
+ <% if has_organization_reference? -%>
470
516
  puts " • Organizations: #{Organization.count}"
517
+ <% end -%>
518
+ <% if has_agency_reference? -%>
471
519
  puts " • Agencies: #{Agency.count}"
472
520
  puts " • <%= table_name %> per organization: #{(total_<%= table_name %>.to_f / Organization.count).round(1)} avg"
473
521
  <% end -%>
@@ -8,6 +8,18 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
8
8
  @organization = organizations(:acme_org)
9
9
  @user = users(:john_user)
10
10
  @agency = agencies(:marketing_agency)
11
+ <% # Set up polymorphic associations using --parents specification -%>
12
+ <% polymorphic_associations.each do |assoc| -%>
13
+ <% if assoc[:parent_types] && assoc[:parent_types].any? -%>
14
+ <% first_parent = assoc[:parent_types].first -%>
15
+ # Set up polymorphic association for <%= assoc[:field_name] %> using specified parents
16
+ @<%= first_parent.underscore %> = <%= first_parent.underscore.pluralize %>(<%= case first_parent.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
17
+ @<%= assoc[:field_name] %> = @<%= first_parent.underscore %> # Use first specified parent type
18
+ <% assoc[:parent_types][1..-1].each do |parent_type| -%>
19
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
20
+ <% end -%>
21
+ <% end -%>
22
+ <% end -%>
11
23
  <% if singular_table_name == 'organization' -%>
12
24
  @<%= singular_table_name %> = <%= table_name %>(:acme_org)
13
25
  <% elsif singular_table_name == 'user' -%>
@@ -21,7 +33,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
21
33
  @auth_headers = { 'Authorization' => "Bearer #{@token}" }
22
34
 
23
35
  # Ensure test <%= singular_table_name %> belongs to test user's organization
24
- <% unless singular_table_name == 'organization' -%>
36
+ <% if has_organization_reference? -%>
25
37
  @<%= singular_table_name %>.update!(organization: @organization)
26
38
  <% end -%>
27
39
  end
@@ -98,6 +110,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
98
110
  assert_includes pagination.keys, 'prev_page'
99
111
  end
100
112
 
113
+ <% if has_organization_reference? -%>
101
114
  test "should only show records for user's organization" do
102
115
  # Create record for different organization
103
116
  other_org = Organization.create!(name: "Other Organization")
@@ -148,6 +161,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
148
161
  # Should include own organization's record
149
162
  assert_includes <%= table_name %>_ids, @<%= singular_table_name %>.id
150
163
  end
164
+ <% end -%>
151
165
 
152
166
  # === SHOW TESTS ===
153
167
 
@@ -167,6 +181,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
167
181
  assert_response :not_found
168
182
  end
169
183
 
184
+ <% if has_organization_reference? -%>
170
185
  test "should not show <%= singular_table_name %> from different organization" do
171
186
  other_org = Organization.create!(name: "Other Organization")
172
187
  other_<%= singular_table_name %> = <%= class_name %>.create!(
@@ -207,6 +222,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
207
222
  get <%= api_singular_route_helper %>_url(other_<%= singular_table_name %>), headers: @auth_headers
208
223
  assert_response :not_found
209
224
  end
225
+ <% end -%>
210
226
 
211
227
  # === CREATE TESTS ===
212
228
 
@@ -242,7 +258,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
242
258
  created_<%= singular_table_name %> = response_body['data']
243
259
  assert_not_nil created_<%= singular_table_name %>, "Response should contain <%= singular_table_name %> data"
244
260
 
245
- <% if class_name != 'Organization' -%>
261
+ <% if has_organization_reference? -%>
246
262
  # Verify organization reference (handle both nested and flat formats)
247
263
  if created_<%= singular_table_name %>['organization']
248
264
  assert_equal @organization.id, created_<%= singular_table_name %>['organization']['id']
@@ -284,7 +300,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
284
300
  headers: @auth_headers
285
301
  end
286
302
 
287
- assert_response :unprocessable_entity
303
+ assert_response :unprocessable_content
288
304
 
289
305
  response_body = JSON.parse(response.body)
290
306
  assert_includes response_body.keys, 'errors'
@@ -317,7 +333,9 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
317
333
  -%>
318
334
 
319
335
  params = valid_<%= singular_table_name %>_params
336
+ <% if has_organization_reference? -%>
320
337
  params.delete(:organization_id) # Remove organization_id to test behavior
338
+ <% end -%>
321
339
  <% if has_user_id -%>
322
340
  params.delete(:user_id) # Remove user_id to test behavior
323
341
  <% end -%>
@@ -333,15 +351,17 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
333
351
  headers: @auth_headers
334
352
  end
335
353
 
336
- assert_response :unprocessable_entity
354
+ assert_response :unprocessable_content
337
355
 
338
356
  # Should return validation errors for missing tenancy
339
357
  error_response = JSON.parse(response.body)
340
358
 
359
+ <% if has_organization_reference? -%>
341
360
  if require_org_id
342
361
  assert_includes error_response['errors'].keys, 'organization_id',
343
362
  "Should require organization_id when require_organization_id = true"
344
363
  end
364
+ <% end -%>
345
365
  <% if has_user_id -%>
346
366
  if require_user_id
347
367
  assert_includes error_response['errors'].keys, 'user_id',
@@ -367,7 +387,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
367
387
  created_response = JSON.parse(response.body)
368
388
  created_<%= singular_table_name %> = created_response['data']
369
389
 
370
- <% unless class_name == 'Organization' -%>
390
+ <% if has_organization_reference? -%>
371
391
  # Verify organization_id was auto-assigned
372
392
  if created_<%= singular_table_name %>['organization']
373
393
  assert_equal @user.organization.id, created_<%= singular_table_name %>['organization']['id'],
@@ -423,12 +443,13 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
423
443
  params: { data: invalid_<%= singular_table_name %>_params },
424
444
  headers: @auth_headers
425
445
 
426
- assert_response :unprocessable_entity
446
+ assert_response :unprocessable_content
427
447
 
428
448
  response_body = JSON.parse(response.body)
429
449
  assert_includes response_body.keys, 'errors'
430
450
  end
431
451
 
452
+ <% if has_organization_reference? -%>
432
453
  test "should not update <%= singular_table_name %> from different organization" do
433
454
  other_org = Organization.create!(name: "Other Organization")
434
455
  other_<%= singular_table_name %> = <%= class_name %>.create!(
@@ -483,6 +504,9 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
483
504
  assert_response :no_content
484
505
  end
485
506
 
507
+ <% end -%>
508
+
509
+ <% if has_organization_reference? -%>
486
510
  test "should not destroy <%= singular_table_name %> from different organization" do
487
511
  other_org = Organization.create!(name: "Other Organization")
488
512
  other_<%= singular_table_name %> = <%= class_name %>.create!(
@@ -526,6 +550,7 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
526
550
 
527
551
  assert_response :not_found
528
552
  end
553
+ <% end -%>
529
554
 
530
555
  # === PARAMETER HANDLING TESTS ===
531
556
 
@@ -664,12 +689,21 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
664
689
 
665
690
  <% attributes.each_with_index do |attribute, index| -%>
666
691
  <% if attribute.type == :references -%>
692
+ <% if attribute.respond_to?(:polymorphic?) && attribute.polymorphic? -%>
693
+ <%= attribute.name %>_id: @<%= attribute.name %>.id,
694
+ <%= attribute.name %>_type: @<%= attribute.name %>.class.name<%= ',' if index < attributes.length - 1 %>
695
+ <% else -%>
667
696
  <%= attribute.name %>_id: @<%= attribute.name %>.id<%= ',' if index < attributes.length - 1 %>
697
+ <% end -%>
668
698
  <% elsif attribute.type == :string && !reference_names.include?(attribute.name) -%>
669
699
  <% if attribute.name.to_s.match?(/email/) -%>
670
700
  <%= attribute.name %>: "test@example.com"<%= ',' if index < attributes.length - 1 %>
671
701
  <% elsif attribute.name.to_s.match?(/password/) -%>
672
702
  <%= attribute.name %>: "password123"<%= ',' if index < attributes.length - 1 %>
703
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
704
+ <%= attribute.name %>: "https://example.com"<%= ',' if index < attributes.length - 1 %>
705
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
706
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
673
707
  <% else -%>
674
708
  <%= attribute.name %>: "Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
675
709
  <% end -%>