propel_api 0.3.0 โ†’ 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 224fb8718c35e1b5d828201c8256a39eeedc0cc6f9ad096ac874f34c9a726c25
4
- data.tar.gz: 6902529de75450cd89415ea357891100f2c2a2d4cc6c57ba6679257e693aaab3
3
+ metadata.gz: 8891f7c2c3ace04fe703d6613d1a2595e19ed7ecaf89ecf960c3000bd1b00e0d
4
+ data.tar.gz: c51158c13407e9b91b01eb10d27b8bb89f5c80517855737a684e6f1fba88eac3
5
5
  SHA512:
6
- metadata.gz: dcd7b7a650de42cf4d0f19e51e777535e399c7f01dd0947e6ecb97854504a2e1a79abfa0341f7bb2dae4dc128cc9261b0c42060c5218aa6129c465920eeb6546
7
- data.tar.gz: 5ee7eace1077821dbb23b93f719d3544447e8a72d5ba0541df8712d55b6294054d2f335aaa4d8bad43e60ea4b69d7737733e4e63bc722bde3ad5fbd2b85a1418
6
+ metadata.gz: c22770ffc6b60119d08f3f8d0e8d6d5f3520f9518b9cedf2c2ae0ad5dbe2e1cc5c98577cdf4639f228b66796a60dbbe901fbbaeb69ee5aaecdc96630a418a116
7
+ data.tar.gz: ea3cbbc43f1f827ceebb10d9e64cc56374ef8d0dbb24e015608bba00bfa1a9608d27edf343c7e2f9677ed049df906a5a1bf45af1161a85bb5d5a85646e67253c
data/CHANGELOG.md CHANGED
@@ -10,6 +10,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
  ### Planned Features
11
11
  - GraphQL adapter support
12
12
 
13
+ ## [0.3.1] - 2025-09-11
14
+
15
+ ### ๐ŸŽ‰ New Features
16
+ - **Full Stack and API-Only Rails App Support**: Enhanced architecture for different Rails application types
17
+ - New dedicated `api_base_controller.rb` template for API-only applications
18
+ - Improved controller template organization with better separation of concerns
19
+ - Enhanced CSRF error handling with dedicated `PropelApiCsrfError` class
20
+ - Better support for both full-stack Rails apps and API-only applications
21
+
22
+ ### ๐Ÿ”ง Enhanced Polymorphic Support
23
+ - **Improved Polymorphic Generators**: Enhanced automatic fixture and test generation with --parents flag
24
+ - Better polymorphic association handling in controller and model generators
25
+ - Automatic fixture template adoption for polymorphic relationships
26
+ - Enhanced agency-organization relationship support in polymorphic contexts
27
+ - **URL/Website/Domain Support**: Enhanced template handling for URL, website, and domain fields
28
+ - Improved model templates for URL validation and formatting
29
+ - Better seed data generation for URL-type fields
30
+ - Enhanced test coverage for URL/domain fields in fixtures and integration tests
31
+
32
+ ### ๐Ÿ› ๏ธ Generator Improvements
33
+ - **Enhanced Core Named Base**: Improved polymorphic association detection and fixture generation
34
+ - Better template processing for polymorphic relationships
35
+ - Enhanced fixture name resolution and parent type selection
36
+ - Improved error handling for polymorphic parent specifications
37
+ - **Template System Enhancements**: Upgraded all ERB templates for better polymorphic handling
38
+ - Consistent fixture template adoption across all generators
39
+ - Better integration with PropelAuthentication fixture patterns
40
+ - Enhanced test template generation with polymorphic parameter support
41
+ - **Controller Architecture Improvements**: Enhanced controller template system
42
+ - Updated Graphiti and PropelFacets controller templates for better inheritance
43
+ - Improved controller base class organization and method visibility
44
+
45
+ ### ๐Ÿ› Bug Fixes
46
+ - **Gem Installation**: Fixed issues with gems not being included when commented out in Gemfile
47
+ - **Integration Test Templates**: Enhanced error handling and parameter validation in generated integration tests
48
+
49
+ ### โš ๏ธ Breaking Changes
50
+ - **REQUIRED --parents for Polymorphic**: Polymorphic associations now require explicit `--parents Parent1,Parent2` declaration. Auto-discovery fallback has been removed. Generator will block with confirmation if --parents is missing.
51
+
13
52
  ## [0.3.0] - 2025-01-15
14
53
 
15
54
  ### ๐ŸŽ‰ Major Features Added
data/README.md CHANGED
@@ -54,11 +54,11 @@ rails generate propel_api:resource User name:string email:string role:string
54
54
  # Resource with associations
55
55
  rails generate propel_api:resource Article title:string content:text user:references category:references
56
56
 
57
- # ๐ŸŽ‰ NEW: Polymorphic associations (v0.3.0)
58
- rails generate propel_api:resource Comment content:text commentable:references{polymorphic} --parents commentable:Post,Video,Product
57
+ # ๐ŸŽ‰ NEW: Polymorphic associations (v0.3.0) - REQUIRES --parents flag
58
+ rails generate propel_api:resource Comment content:text commentable:references{polymorphic} --parents Post,Video,Product
59
59
 
60
60
  # Polymorphic with tenancy
61
- rails generate propel_api:resource Review rating:integer comment:text review_parent:polymorphic --parents review_parent:Product,Agency
61
+ rails generate propel_api:resource Review rating:integer comment:text review_parent:polymorphic --parents Product,Agency
62
62
 
63
63
  # With Graphiti adapter
64
64
  rails generate propel_api Product name:string price:decimal --adapter=graphiti
@@ -81,10 +81,10 @@ PropelApi now provides comprehensive support for polymorphic associations with a
81
81
 
82
82
  ```bash
83
83
  # Generate a Comment that can belong to Posts, Videos, or Products
84
- rails generate propel_api:resource Comment body:text commentable:references{polymorphic} --parents commentable:Post,Video,Product
84
+ rails generate propel_api:resource Comment body:text commentable:references{polymorphic} --parents Post,Video,Product
85
85
 
86
86
  # Generate a Review with full tenancy support
87
- rails generate propel_api:resource Review rating:integer comment:text review_parent:references{polymorphic} --parents reviewable:Product,Agency
87
+ rails generate propel_api:resource Review rating:integer comment:text review_parent:references{polymorphic} --parents Product,Agency
88
88
  ```
89
89
 
90
90
  ### Key Features
@@ -93,10 +93,10 @@ rails generate propel_api:resource Review rating:integer comment:text review_par
93
93
  - Additional support `field_name:polymorphic` instead of Rails' verbose `field_name:references{polymorphic}`
94
94
  - No need for complex quote escaping or nested syntax
95
95
 
96
- #### ๐ŸŽฏ Smart Parent Specification
97
- - `--parents field_name:Parent1,Parent2,Parent3` defines valid parent models
96
+ #### ๐ŸŽฏ Required Parent Specification
97
+ - `--parents Parent1,Parent2,Parent3` defines valid parent models (REQUIRED for polymorphic associations)
98
98
  - Automatically generates varied test data using different parent types
99
- - Validates parent models exist in your application
99
+ - Generator will block with confirmation if --parents is not provided
100
100
 
101
101
  #### ๐Ÿงช Complete Test Coverage
102
102
  - **Model Tests**: Proper polymorphic association validation and parent type checking
@@ -48,6 +48,11 @@ module PropelApi
48
48
  type: :boolean,
49
49
  default: false,
50
50
  desc: "Include usage comments in generated controller"
51
+
52
+ class_option :parents,
53
+ type: :hash,
54
+ default: {},
55
+ desc: "Specify parent types for polymorphic associations (e.g., --parents commentable:Post,Photo,Video)"
51
56
 
52
57
  def validate_model_exists
53
58
  # Ensure attributes are parsed before validation
@@ -64,6 +69,9 @@ module PropelApi
64
69
  if !options[:all_attributes] && (@attributes.nil? || @attributes.empty?) && model_file_exists
65
70
  show_auto_introspection_warning
66
71
  end
72
+ # Process polymorphic parent information if provided
73
+ process_polymorphic_parents if options[:parents].present?
74
+
67
75
  # Validate attributes if not using introspection
68
76
  validate_attributes_exist unless should_auto_introspect?
69
77
  end
@@ -110,6 +118,24 @@ module PropelApi
110
118
 
111
119
  private
112
120
 
121
+ def process_polymorphic_parents
122
+ # Convert _type attributes to polymorphic associations for template processing
123
+ options[:parents].each do |field_name, parent_list|
124
+ # Check if we have the corresponding _id and _type attributes
125
+ type_attr = @attributes.find { |attr| attr.name == "#{field_name}_type" }
126
+ id_attr = @attributes.find { |attr| attr.name == "#{field_name}_id" }
127
+
128
+ if type_attr && id_attr
129
+ # Add a mock polymorphic attribute to @attributes for template processing
130
+ @attributes << OpenStruct.new(
131
+ name: field_name,
132
+ type: :references,
133
+ polymorphic?: true
134
+ )
135
+ end
136
+ end
137
+ end
138
+
113
139
  def route_name
114
140
  file_name.pluralize
115
141
  end
@@ -527,6 +527,11 @@ module PropelApi
527
527
  @attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
528
528
  end
529
529
 
530
+ def has_user_reference?
531
+ return false unless defined?(@attributes) && @attributes
532
+ @attributes.any? { |attr| (attr.name == 'user' && attr.type == :references) || attr.name == 'user_id' }
533
+ end
534
+
530
535
  # Polymorphic associations helper methods
531
536
  def polymorphic_parent_types
532
537
  @polymorphic_parent_types ||= parse_polymorphic_parent_types
@@ -31,6 +31,12 @@ module PropelApi
31
31
  else
32
32
  say "Installing API controller with #{@adapter} adapter", :green
33
33
  say "Using namespace: #{namespace_display} version: #{version_display}", :blue
34
+
35
+ # Create Api::BaseController first
36
+ create_api_base_controller
37
+
38
+ # Optionally enhance CSRF error messages (non-breaking)
39
+ add_csrf_error_handling
34
40
  end
35
41
 
36
42
  case @adapter
@@ -107,6 +113,37 @@ module PropelApi
107
113
 
108
114
  private
109
115
 
116
+ def create_api_base_controller
117
+ template "controllers/api_base_controller.rb", "app/controllers/api/base_controller.rb"
118
+ say "Created Api::BaseController for CSRF-free API endpoints", :green
119
+ end
120
+
121
+ def add_csrf_error_handling
122
+ # Copy the error handler
123
+ template "errors/propel_api_csrf_error.rb", "app/errors/propel_api_csrf_error.rb"
124
+
125
+ # Inject rescue_from into ApplicationController
126
+ inject_into_class "app/controllers/application_controller.rb", ApplicationController, <<-RUBY
127
+ # Enhanced CSRF error handling for JSON requests - added by PropelAPI
128
+ rescue_from ActionController::InvalidAuthenticityToken, with: :handle_propel_csrf_error
129
+
130
+ private
131
+
132
+ def handle_propel_csrf_error
133
+ if request.format.json? || request.content_type&.include?('application/json')
134
+ PropelApiCsrfError.handle_csrf_error_for_json(self)
135
+ else
136
+ raise ActionController::InvalidAuthenticityToken
137
+ end
138
+ end
139
+ RUBY
140
+
141
+ say "Enhanced ApplicationController with helpful API guidance", :green
142
+ rescue => e
143
+ say "Could not inject CSRF error handling into ApplicationController: #{e.message}", :yellow
144
+ say "You can manually add the rescue_from handler if desired", :blue
145
+ end
146
+
110
147
  def copy_propel_facets_controller
111
148
  template "controllers/api_controller_propel_facets.rb", api_controller_path
112
149
  copy_example_controller
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Base controller for API endpoints ONLY
5
+ #
6
+ # โš ๏ธ SECURITY WARNING: This controller disables CSRF protection!
7
+ # โš ๏ธ ONLY use this for stateless JSON API endpoints with JWT authentication
8
+ # โš ๏ธ DO NOT use this for web controllers that serve HTML or handle sessions
9
+ #
10
+ # For web controllers, inherit from ApplicationController instead.
11
+ #
12
+ class Api::BaseController < ActionController::Base
13
+ # Set up and then skip CSRF protection for API endpoints (they use JWT tokens instead)
14
+ protect_from_forgery with: :null_session
15
+ skip_before_action :verify_authenticity_token
16
+
17
+ # Disable session handling for API requests (stateless)
18
+ before_action :disable_session
19
+ before_action :ensure_json_request, if: -> { Rails.env.production? }
20
+
21
+ # Handle common API errors
22
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
23
+ rescue_from ActionController::ParameterMissing, with: :parameter_missing
24
+ rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
25
+
26
+ private
27
+
28
+ def disable_session
29
+ request.session_options[:skip] = true
30
+ end
31
+
32
+ def ensure_json_request
33
+ unless request.format.json? || request.content_type&.include?('application/json')
34
+ Rails.logger.warn "Non-JSON request to API controller: #{controller_name}##{action_name}"
35
+ render json: {
36
+ error: 'Invalid request format',
37
+ message: 'This endpoint only accepts JSON requests'
38
+ }, status: :not_acceptable
39
+ end
40
+ end
41
+
42
+ def record_not_found(exception)
43
+ Rails.logger.error "API Record not found: #{exception.message}"
44
+ render json: {
45
+ error: 'Record not found',
46
+ message: 'The requested resource could not be found'
47
+ }, status: :not_found
48
+ end
49
+
50
+ def parameter_missing(exception)
51
+ Rails.logger.error "API Parameter missing: #{exception.message}"
52
+ render json: {
53
+ error: 'Missing required parameters',
54
+ message: 'Required parameters are missing from the request'
55
+ }, status: :bad_request
56
+ end
57
+
58
+ def record_invalid(exception)
59
+ Rails.logger.error "API Record invalid: #{exception.record.errors.full_messages}"
60
+
61
+ # In development, show full validation errors for debugging
62
+ # In production, show generic message to prevent information disclosure
63
+ errors = if Rails.env.development?
64
+ exception.record.errors.full_messages
65
+ else
66
+ ['Validation failed - please check your input']
67
+ end
68
+
69
+ render json: {
70
+ error: 'Validation failed',
71
+ errors: errors
72
+ }, status: :unprocessable_content
73
+ end
74
+ end
75
+
@@ -1,4 +1,4 @@
1
- class <%= api_controller_class_name %> < ApplicationController
1
+ class <%= api_controller_class_name %> < Api::BaseController
2
2
  include Graphiti::Rails
3
3
  include Graphiti::Responders
4
4
  include PropelAuthenticationConcern
@@ -1,4 +1,4 @@
1
- class <%= api_controller_class_name %> < ApplicationController
1
+ class <%= api_controller_class_name %> < Api::BaseController
2
2
  include HasScope
3
3
  include Pagy::Backend
4
4
  include FacetRenderer
@@ -216,7 +216,7 @@ class <%= api_controller_class_name %> < ApplicationController
216
216
  def agency_tenancy_enabled?
217
217
  # Check PropelAuthentication configuration (owns tenancy models)
218
218
  if defined?(PropelAuthentication) && PropelAuthentication.respond_to?(:configuration)
219
- PropelAuthentication.configuration.agency_tenancy
219
+ PropelAuthentication.configuration.agency_required?
220
220
  else
221
221
  true # Safe default - enables agency tenancy when configuration unavailable
222
222
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # PropelAPI CSRF Error Handler
5
+ # Provides helpful guidance when developers send JSON requests to web controllers
6
+ #
7
+ class PropelApiCsrfError < StandardError
8
+ def self.handle_csrf_error_for_json(controller)
9
+ # Log security event for monitoring
10
+ Rails.logger.warn "CSRF bypass attempted on #{controller.controller_name}##{controller.action_name} from #{controller.request.remote_ip}"
11
+
12
+ controller.render json: {
13
+ error: "CSRF token verification failed",
14
+ message: "This endpoint requires CSRF protection and is intended for web browser requests.",
15
+ hint: "For API requests, use the Propel-generated API controllers instead.",
16
+ note: "API controllers are CSRF-free and use JWT authentication."
17
+ }, status: :unprocessable_content
18
+ end
19
+ end
@@ -12,38 +12,46 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
12
12
  <% end -%>
13
13
  permitted_params <%
14
14
  # Generate permitted parameters using shared polymorphic support
15
- param_list = []
15
+ regular_params = []
16
+ json_params = []
16
17
 
17
18
  # Add attribute-based params using polymorphic support
18
19
  attributes.each do |attr|
19
20
  if attr.type == :json
20
- param_list << "#{attr.name}: {}"
21
+ json_params << "#{attr.name}: {}"
21
22
  elsif attr.type == :references
22
23
  # Use Rails' built-in polymorphic detection
23
24
  if attr.respond_to?(:polymorphic?) && attr.polymorphic?
24
- param_list << ":#{attr.name}_id"
25
- param_list << ":#{attr.name}_type"
25
+ regular_params << ":#{attr.name}_id"
26
+ regular_params << ":#{attr.name}_type"
26
27
  else
27
- param_list << ":#{attr.name}_id"
28
+ regular_params << ":#{attr.name}_id"
28
29
  end
29
30
  else
30
- param_list << ":#{attr.name}"
31
+ regular_params << ":#{attr.name}"
31
32
  end
32
33
  end
33
34
 
34
35
  # Add multi-tenancy params unless skipped
35
36
  if has_organization_reference?
36
- param_list += [":organization_id"]
37
+ regular_params += [":organization_id"]
37
38
  end
38
39
 
39
40
  if has_agency_reference?
40
- param_list += [":agency_id"]
41
+ regular_params += [":agency_id"]
41
42
  end
42
43
 
43
44
  # Remove organization/agency duplicates if they were user-specified
44
- param_list.uniq!
45
+ regular_params.uniq!
45
46
 
46
- -%><%= param_list.join(', ') %>
47
+ # Combine regular and JSON params properly
48
+ if json_params.any?
49
+ all_params = regular_params + json_params
50
+ else
51
+ all_params = regular_params
52
+ end
53
+
54
+ -%><%= all_params.join(', ') %>
47
55
 
48
56
  <% if options[:with_comments] -%>
49
57
  # Connect facets to actions - customize as needed for your <%= class_name.downcase %> model
@@ -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) -%>
@@ -69,12 +69,16 @@ class <%= class_name %> < ApplicationRecord
69
69
  <% unless (has_organization_reference? && attribute.name == 'organization') || (has_agency_reference? && attribute.name == 'agency') -%>
70
70
  <% if attribute.name == 'organization' -%>
71
71
  belongs_to :organization
72
+ <% else -%>
73
+ <% if attribute.respond_to?(:polymorphic?) && attribute.polymorphic? -%>
74
+ belongs_to :<%= attribute.name %>, polymorphic: true
72
75
  <% else -%>
73
76
  belongs_to :<%= attribute.name %> # Add 'optional: true' if this should be optional
74
77
  <% end -%>
75
78
  <% end -%>
76
79
  <% end -%>
77
80
  <% end -%>
81
+ <% end -%>
78
82
  # Add additional associations here
79
83
  # has_many :comments, dependent: :destroy
80
84
 
@@ -109,7 +113,13 @@ json_facet :short, fields: [:id<%
109
113
  attr.name.to_s !~ security_patterns &&
110
114
  attr.name.to_s.length < 20)
111
115
  end
112
- 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 %>]
113
123
  json_facet :details, fields: [:id<%
114
124
  # Include most fields except timestamps, security-sensitive, and internal Rails fields for details facet
115
125
  detail_attributes = attributes.reject do |attr|
@@ -128,6 +138,11 @@ json_facet :details, fields: [:id<%
128
138
  # Collect all field names, avoiding duplicates
129
139
  all_fields = Set.new
130
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
+
131
146
  # Add non-reference attributes
132
147
  detail_attributes.reject { |attr| attr.type == :references }.each do |attribute|
133
148
  all_fields << attribute.name.to_sym
@@ -146,6 +161,15 @@ json_facet :details, fields: [:id<%
146
161
  all_fields << :agency_id
147
162
  end
148
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
+
149
173
  # Output the unique fields
150
174
  all_fields.to_a.sort.each do |field| -%>, :<%= field %><% end -%>]<%
151
175
 
@@ -126,7 +126,7 @@ if users.count < 6
126
126
  if org_agencies.any?
127
127
  # Assign to random agency within organization
128
128
  agency = org_agencies.sample
129
- Agent.create!(user: new_user, agency: agency, role: 'member')
129
+ Agent.create!(user: new_user, agency: agency, organization: org, role: 'member')
130
130
  end
131
131
  end
132
132
  end
@@ -171,7 +171,7 @@ end
171
171
  <% end -%>
172
172
  <% if has_agency_reference? -%>
173
173
  # Multi-tenancy: Assign agency
174
- <%= singular_table_name %>_attributes[:agency] = [agency, nil].sample # 50% chance of having agency
174
+ <%= singular_table_name %>_attributes[:agency] = agency # Always assign agency for valid records
175
175
  <% end -%>
176
176
  <% if has_organization_reference? || has_agency_reference? -%>
177
177
 
@@ -211,7 +211,7 @@ end
211
211
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.unique.username
212
212
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
213
213
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::PhoneNumber.phone_number
214
- <% 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) -%>
215
215
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.url
216
216
  <% elsif attribute.name.to_s.match?(/\A(slug)\z/i) -%>
217
217
  <%= singular_table_name %>_attributes[:<%= attribute.name %>] = Faker::Internet.slug
@@ -8,13 +8,15 @@ 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 -%>
11
12
  <% polymorphic_associations.each do |assoc| -%>
12
- <% assoc[:parent_types].each_with_index do |parent_type, index| -%>
13
- <% if index == 0 -%>
14
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= parent_type.underscore == 'agency' ? ':marketing_agency' : ':one' %>)
15
- @<%= assoc[:field_name] %> = @<%= parent_type.underscore %> # Default polymorphic parent for tests
16
- <% else -%>
17
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= index == 1 ? ':marketing_agency' : ':one' %>)
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 %>)
18
20
  <% end -%>
19
21
  <% end -%>
20
22
  <% end -%>
@@ -698,6 +700,10 @@ class <%= controller_class_name_with_namespace %>ControllerTest < ActionDispatch
698
700
  <%= attribute.name %>: "test@example.com"<%= ',' if index < attributes.length - 1 %>
699
701
  <% elsif attribute.name.to_s.match?(/password/) -%>
700
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 %>
701
707
  <% else -%>
702
708
  <%= attribute.name %>: "Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
703
709
  <% end -%>
@@ -40,7 +40,7 @@ one:
40
40
  <%= attribute.name %>: test_user_1
41
41
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
42
42
  <%= attribute.name %>: "+1-555-0001"
43
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
43
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
44
44
  <%= attribute.name %>: "https://example1.com"
45
45
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
46
46
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> One"
@@ -144,7 +144,7 @@ two:
144
144
  <%= attribute.name %>: test_user_2
145
145
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
146
146
  <%= attribute.name %>: "+1-555-0002"
147
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
147
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
148
148
  <%= attribute.name %>: "https://example2.com"
149
149
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
150
150
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> Two"
@@ -248,7 +248,7 @@ three:
248
248
  <%= attribute.name %>: test_user_3
249
249
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
250
250
  <%= attribute.name %>: "+1-555-0003"
251
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
251
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
252
252
  <%= attribute.name %>: "https://example3.com"
253
253
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
254
254
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> Three"
@@ -2,6 +2,12 @@
2
2
 
3
3
  require "test_helper"
4
4
 
5
+ <%
6
+ # Check attributes directly instead of database columns (more reliable during generation)
7
+ has_agency_id = attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
8
+ has_user_id = attributes.any? { |attr| attr.name == 'user' && attr.type == :references }
9
+ -%>
10
+
5
11
  class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
6
12
 
7
13
  def setup
@@ -11,10 +17,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
11
17
  <% polymorphic_associations.each do |assoc| -%>
12
18
  <% assoc[:parent_types].each_with_index do |parent_type, index| -%>
13
19
  <% if index == 0 -%>
14
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= parent_type.underscore == 'agency' ? ':marketing_agency' : ':one' %>)
20
+ @<%= 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 %>)
15
21
  @<%= assoc[:field_name] %> = @<%= parent_type.underscore %> # Default polymorphic parent for tests
16
22
  <% else -%>
17
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= index == 1 ? ':marketing_agency' : ':one' %>)
23
+ @<%= 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 %>)
18
24
  <% end -%>
19
25
  <% end -%>
20
26
  <% end -%>
@@ -26,6 +32,21 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
26
32
  @<%= singular_table_name %> = <%= table_name %>(:marketing_agency)
27
33
  <% else -%>
28
34
  @<%= singular_table_name %> = <%= table_name %>(:one)
35
+ <% end -%>
36
+ <% if has_user_reference? -%>
37
+ @user = users(:john_user)
38
+ <% end -%>
39
+ <% # Set up polymorphic associations using --parents specification -%>
40
+ <% polymorphic_associations.each do |assoc| -%>
41
+ <% if assoc[:parent_types] && assoc[:parent_types].any? -%>
42
+ <% first_parent = assoc[:parent_types].first -%>
43
+ # Set up polymorphic association for <%= assoc[:field_name] %> using specified parents
44
+ @<%= 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 %>)
45
+ @<%= assoc[:field_name] %> = @<%= first_parent.underscore %> # Use first specified parent type
46
+ <% assoc[:parent_types][1..-1].each do |parent_type| -%>
47
+ @<%= 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 %>)
48
+ <% end -%>
49
+ <% end -%>
29
50
  <% end -%>
30
51
  @token = @user.generate_jwt_token
31
52
  @auth_headers = { 'Authorization' => "Bearer #{@token}" }
@@ -66,6 +87,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
66
87
  <%= attribute.name %>: "workflow@example.com"<%= ',' if index < attributes.length - 1 %>
67
88
  <% elsif attribute.name.to_s.match?(/password/) -%>
68
89
  <%= attribute.name %>: "password123"<%= ',' if index < attributes.length - 1 %>
90
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
91
+ <%= attribute.name %>: "https://workflow.example.com"<%= ',' if index < attributes.length - 1 %>
92
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
93
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
69
94
  <% else -%>
70
95
  <%= attribute.name %>: "Workflow Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
71
96
  <% end -%>
@@ -125,7 +150,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
125
150
 
126
151
  # Verify created <%= singular_table_name %> data
127
152
  assert_equal @organization.id, created_<%= singular_table_name %>['organization']['id']
128
- <% unless singular_table_name == 'user' -%>
153
+ <% if has_user_id && singular_table_name != 'user' -%>
129
154
  assert_equal @user.id, created_<%= singular_table_name %>['user']['id']
130
155
  <% end -%>
131
156
  <% attributes.each do |attribute| -%>
@@ -304,6 +329,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
304
329
  <%= attribute.name %>: "pagination#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
305
330
  <% elsif attribute.name.include?('email') -%>
306
331
  <%= attribute.name %>: "pagination#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
332
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
333
+ <%= attribute.name %>: "https://pagination#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
334
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
335
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
307
336
  <% else -%>
308
337
  <%= attribute.name %>: "Pagination Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
309
338
  <% end -%>
@@ -389,11 +418,6 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
389
418
  # Test 4: Tenancy context behavior based on configuration
390
419
  require_org_id = PropelAuthentication.configuration.require_organization_id
391
420
  require_user_id = PropelAuthentication.configuration.require_user_id
392
- <%
393
- # Check attributes directly instead of database columns (more reliable during generation)
394
- has_agency_id = attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
395
- has_user_id = attributes.any? { |attr| attr.name == 'user' && attr.type == :references }
396
- -%>
397
421
 
398
422
  <% if has_agency_id -%>
399
423
  # Model with agency - provide agency_id but test missing organization_id
@@ -573,6 +597,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
573
597
  <%= attribute.name %>: "org1_<%= attribute.name %>@example.com"<%= ',' if index < attributes.length - 1 %>
574
598
  <% elsif attribute.name.include?('password') -%>
575
599
  <%= attribute.name %>: "org1_secure_password"<%= ',' if index < attributes.length - 1 %>
600
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
601
+ <%= attribute.name %>: "https://org1.example.com"<%= ',' if index < attributes.length - 1 %>
602
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
603
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
576
604
  <% else -%>
577
605
  <%= attribute.name %>: "Org 1 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
578
606
  <% end -%>
@@ -619,6 +647,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
619
647
  <%= attribute.name %>: "org2_<%= attribute.name %>@example.com"<%= ',' if index < attributes.length - 1 %>
620
648
  <% elsif attribute.name.include?('password') -%>
621
649
  <%= attribute.name %>: "org2_secure_password"<%= ',' if index < attributes.length - 1 %>
650
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
651
+ <%= attribute.name %>: "https://org2.example.com"<%= ',' if index < attributes.length - 1 %>
652
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
653
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
622
654
  <% else -%>
623
655
  <%= attribute.name %>: "Org 2 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
624
656
  <% end -%>
@@ -775,6 +807,10 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
775
807
  <%= attribute.name %>: "concurrent@example.com"<%= ',' if index < attributes.length - 1 %>
776
808
  <% elsif attribute.name.include?('email') -%>
777
809
  <%= attribute.name %>: "concurrent@example.com"<%= ',' if index < attributes.length - 1 %>
810
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
811
+ <%= attribute.name %>: "https://concurrent.example.com"<%= ',' if index < attributes.length - 1 %>
812
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
813
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
778
814
  <% else -%>
779
815
  <%= attribute.name %>: "Concurrent Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
780
816
  <% end -%>
@@ -890,6 +926,10 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
890
926
  <%= attribute.name %>: "bulk#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
891
927
  <% elsif attribute.name.include?('email') -%>
892
928
  <%= attribute.name %>: "bulk#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
929
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
930
+ <%= attribute.name %>: "https://bulk#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
931
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
932
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
893
933
  <% else -%>
894
934
  <%= attribute.name %>: "Bulk Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
895
935
  <% end -%>
@@ -11,10 +11,10 @@ class <%= class_name %>Test < ActiveSupport::TestCase
11
11
  <% polymorphic_associations.each do |assoc| -%>
12
12
  <% assoc[:parent_types].each_with_index do |parent_type, index| -%>
13
13
  <% if index == 0 -%>
14
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= parent_type.underscore == 'agency' ? ':marketing_agency' : ':one' %>)
14
+ @<%= 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 %>)
15
15
  @<%= assoc[:field_name] %> = @<%= parent_type.underscore %> # Default polymorphic parent for tests
16
16
  <% else -%>
17
- @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= index == 1 ? ':marketing_agency' : ':one' %>)
17
+ @<%= 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 %>)
18
18
  <% end -%>
19
19
  <% end -%>
20
20
  <% end -%>
data/lib/propel_api.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PropelApi
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propel_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Martin, Rafael Pivato, Chi Putera
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-10 00:00:00.000000000 Z
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -73,9 +73,11 @@ files:
73
73
  - lib/generators/propel_api/install/install_generator.rb
74
74
  - lib/generators/propel_api/resource/resource_generator.rb
75
75
  - lib/generators/propel_api/templates/config/propel_api.rb.tt
76
+ - lib/generators/propel_api/templates/controllers/api_base_controller.rb
76
77
  - lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb
77
78
  - lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb
78
79
  - lib/generators/propel_api/templates/controllers/example_controller.rb.tt
80
+ - lib/generators/propel_api/templates/errors/propel_api_csrf_error.rb
79
81
  - lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt
80
82
  - lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt
81
83
  - lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt