propel_api 0.3.2 โ†’ 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a2cc174f214b50c6e5f58829c9136661591fc142a1e14e09aaf7221c0e1ddda
4
- data.tar.gz: ae2f7764a20f7dc8b22b35486c4d1ca68e4910321d98c624306293800326270b
3
+ metadata.gz: ab74f199f5f59a6a62a0f39a8c63960a7b1339b46bd32701219a6249f6cdb5a3
4
+ data.tar.gz: c4aa9676d91f4400cd54f0aa6d5ce0a022f9b6c893ec7b3124da057f679d8770
5
5
  SHA512:
6
- metadata.gz: 4ca81a75e2225c0a2bdff55f17aef1e5f6067c7b19c0e815a074201b39a508a0f46d3afa6bcbc54225e7ebc3314cedf46ecdf18cccfa11c15b2031b0afc8c168
7
- data.tar.gz: 2bab3dadb532118c96fbdc76e8fa389be29fa4d58801fc3dc7ab6fbea90cffa9534795035f306dd470f6d89f841b055909a3e582634fbae49c5296cc3f44e335
6
+ metadata.gz: 27513e98749a5b837f3f7c1e10280e49155ec5a8113c8319c1b8994e17efaf79cba9c549f74a82982a386fa30d2505d62aeadb84d7bd1af9e0debc9576a52d80
7
+ data.tar.gz: 8d1c4bc5f35ac31d8d42570c7d0829e488242512631ab5d883a382fddc77bb12f3c94daa36645b6fefbc92ab50ccad455a4464225749b047ca1dbbcc27f53fb0
data/CHANGELOG.md CHANGED
@@ -10,6 +10,66 @@ 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.3] - 2025-01-XX
14
+
15
+ ### ๐ŸŽ‰ Major Features Added
16
+ - **Comprehensive Dynamic Filtering System**: Complete implementation of automatic URL query string filtering and scoping using the `has_scope` gem
17
+ - **Multi-Type Filtering Support**: Full filtering support across all data types (string, numeric, boolean, datetime, date, time)
18
+ - **Database-Agnostic Implementation**: Works seamlessly across SQLite, PostgreSQL, and MySQL without database-specific code
19
+ - **Automatic Scope Generation**: Dynamic scope generation based on model column types and filter operators
20
+ - **Intelligent Facet Integration**: Automatic inclusion of filterable fields in JSON facets for API responses
21
+
22
+ ### ๐Ÿ” Advanced Filtering Capabilities
23
+ - **String Filtering**: Exact match (`_eq`), contains (`_contains`), starts with (`_starts_with`), ends with (`_ends_with`), in list (`_in`)
24
+ - **Numeric Filtering**: Comparison operators (`_gt`, `_lt`, `_gte`, `_lte`), range filtering (`_range`), in list (`_in`)
25
+ - **Boolean Filtering**: Multiple value format support (`true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`)
26
+ - **DateTime Filtering**: Temporal comparisons (`_before`, `_after`), date extraction (`_year`, `_month`, `_day`), date matching (`_date`)
27
+ - **Range Filtering**: Support for both array and string formats (e.g., `"150,350"` or `[150, 350]`)
28
+
29
+ ### ๐Ÿ› ๏ธ Technical Implementation
30
+ - **PropelDynamicScopeGenerator**: New generator class for automatic scope creation based on model columns
31
+ - **PropelModelFiltersConcern**: Model concern for dynamic scope generation and filter parameter validation
32
+ - **PropelControllerFiltersConcern**: Controller concern for has_scope integration and security validation
33
+ - **PropelFilterOperators**: Configuration class defining available operators for each data type
34
+ - **Database-Agnostic Datetime Parsing**: Ruby-based datetime parsing instead of SQL extraction functions
35
+
36
+ ### ๐Ÿ”’ Security & Performance
37
+ - **Input Validation**: Comprehensive validation of filter parameters against allowed field names and operators
38
+ - **SQL Injection Protection**: All parameters properly escaped and parameterized
39
+ - **Multi-Tenancy Integration**: Automatic organization scoping for all filtered queries
40
+ - **Sensitive Field Filtering**: Automatic exclusion of sensitive fields (passwords, tokens, etc.) from filtering
41
+ - **Performance Optimization**: Efficient range queries and indexed field prioritization
42
+
43
+ ### ๐Ÿ“Š Enhanced JSON Facet System
44
+ - **Smart Field Inclusion**: Automatic inclusion of boolean and datetime fields in `:short` facets
45
+ - **Exclusion Lists**: Configurable exclusion of specific fields (e.g., `created_at`, `updated_at`, `internal_flag`)
46
+ - **Consistent Serialization**: Ensures filterable fields are available in API responses for client-side filtering
47
+
48
+ ### ๐Ÿงช Comprehensive Testing
49
+ - **Test-Driven Development**: Complete test suite with 992 tests covering all filtering scenarios
50
+ - **Fixture-Aware Testing**: Unique test data values to avoid conflicts with fixture data
51
+ - **Multi-Tenancy Testing**: Proper testing of organization-scoped filtering
52
+ - **Edge Case Coverage**: Testing of invalid inputs, empty values, and error conditions
53
+ - **Database Compatibility**: Tests pass across SQLite, PostgreSQL, and MySQL
54
+
55
+ ### ๐Ÿ“– Documentation & Examples
56
+ - **Comprehensive API Documentation**: Complete filtering documentation with examples for all operators
57
+ - **Postman Collection Ready**: Detailed query parameter examples for testing
58
+ - **Error Handling Guide**: Common error scenarios and troubleshooting tips
59
+ - **Performance Guidelines**: Optimization recommendations for large datasets
60
+
61
+ ### ๐Ÿ”ง Configuration & Customization
62
+ - **Flexible Operator Configuration**: Easy addition of new filter operators via `PropelFilterOperators`
63
+ - **Customizable Field Exclusions**: Configurable lists for fields to exclude from facets
64
+ - **Template-Based Generation**: All filtering code generated via ERB templates for easy customization
65
+ - **Backward Compatibility**: Existing applications continue to work without changes
66
+
67
+ ### ๐Ÿ› Bug Fixes
68
+ - **Template Syntax Errors**: Fixed ERB template syntax issues with `next` vs `return` in select blocks
69
+ - **Scope Generation Caching**: Resolved issues with cached scopes not updating after template changes
70
+ - **Datetime Parsing Errors**: Fixed datetime scope generation to handle multiple input formats
71
+ - **SQL Compatibility**: Resolved SQLite `EXTRACT` function compatibility issues
72
+
13
73
  ## [0.3.2] - 2025-09-15
14
74
 
15
75
  ### ๐Ÿ› Critical Bug Fixes
data/README.md CHANGED
@@ -1,6 +1,51 @@
1
1
  # PropelApi
2
2
 
3
- A comprehensive Rails generator that creates complete API resources with models, controllers, tests, and realistic seed data. Supports both PropelFacets (JSON Facet) and Graphiti serialization engines with **fixed route insertion** and proper indentation.
3
+ A comprehensive Rails generator that creates complete API resources with models, controllers, tests, and realistic seed data. Supports both PropelFacets (JSON Facet) and Graphiti serialization engines with **fixed route insertion**, proper indentation, and **comprehensive dynamic filtering system**.
4
+
5
+ ## ๐ŸŽ‰ NEW in v0.3.3: Dynamic Filtering System
6
+
7
+ PropelApi now includes a complete automatic URL query string filtering and scoping system using the `has_scope` gem. This provides powerful, database-agnostic filtering across all data types with zero configuration required.
8
+
9
+ ### Quick Filtering Examples
10
+
11
+ ```bash
12
+ # String filtering
13
+ GET /api/v1/meetings?title_contains=Project&status_in=active,pending
14
+
15
+ # Numeric filtering
16
+ GET /api/v1/meetings?max_participants_gte=50&max_participants_lte=200
17
+
18
+ # Boolean filtering
19
+ GET /api/v1/meetings?recording_enabled_eq=true&ai_research_enabled_eq=false
20
+
21
+ # DateTime filtering
22
+ GET /api/v1/meetings?start_time_after=2024-01-01T00:00:00Z&start_time_before=2024-12-31T23:59:59Z
23
+
24
+ # Range filtering
25
+ GET /api/v1/meetings?max_participants_range=150,350
26
+
27
+ # Combined filtering with sorting and pagination
28
+ GET /api/v1/meetings?title_contains=Project&max_participants_gte=50&order_by=start_time&page=1&limit=20
29
+ ```
30
+
31
+ ### Supported Filter Operators
32
+
33
+ | Data Type | Operators | Examples |
34
+ |-----------|-----------|----------|
35
+ | **String** | `_eq`, `_contains`, `_starts_with`, `_ends_with`, `_in` | `name_eq=Acme`, `title_contains=Project` |
36
+ | **Numeric** | `_eq`, `_gt`, `_lt`, `_gte`, `_lte`, `_range`, `_in` | `max_participants_gte=50`, `price_range=100,500` |
37
+ | **Boolean** | `_eq` (supports `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`) | `recording_enabled_eq=true` |
38
+ | **DateTime** | `_before`, `_after`, `_year`, `_month`, `_day`, `_date` | `start_time_after=2024-01-01`, `created_at_year=2024` |
39
+ | **Date** | Same as DateTime | `event_date_after=2024-01-01` |
40
+ | **Time** | Same as DateTime | `start_time_hour=14` |
41
+
42
+ ### Automatic Features
43
+
44
+ - **Zero Configuration**: Filtering works automatically for all generated resources
45
+ - **Database Agnostic**: Works with SQLite, PostgreSQL, and MySQL
46
+ - **Security Built-in**: SQL injection protection and input validation
47
+ - **Multi-tenancy Ready**: Automatic organization scoping
48
+ - **Performance Optimized**: Efficient queries with proper indexing support
4
49
 
5
50
  ## Installation
6
51
 
@@ -1263,38 +1263,54 @@ module PropelApi
1263
1263
  say "="*70, :red
1264
1264
  end
1265
1265
 
1266
- # Route management helper methods
1266
+ # Route management helper methods - using Rails' gsub_file for proper insertion
1267
1267
  def insert_routes
1268
+ return if route_already_exists?
1269
+
1270
+ if @api_namespace.present? && @api_version.present?
1271
+ insert_nested_namespace_route
1272
+ elsif @api_namespace.present?
1273
+ insert_single_namespace_route
1274
+ else
1275
+ route "resources :#{route_name}"
1276
+ end
1277
+ end
1278
+
1279
+ private
1280
+
1281
+ def insert_nested_namespace_route
1268
1282
  routes_file = File.join(destination_root, "config/routes.rb")
1269
- return unless File.exist?(routes_file)
1270
-
1271
1283
  routes_content = File.read(routes_file)
1272
1284
 
1273
- if @api_namespace.present? && @api_version.present?
1274
- # Both namespace and version: insert into existing or create new structure
1275
- if has_nested_namespace?(routes_content, @api_namespace, @api_version)
1276
- resource_line = " resources :#{route_name}"
1277
- insert_into_nested_namespace(routes_content, @api_namespace, @api_version, resource_line)
1278
- else
1279
- # Create new nested namespace structure
1280
- # Rails route method adds 2 spaces to each line, so we need to adjust for that
1281
- route_content = "namespace :#{@api_namespace} do\n namespace :#{@api_version} do\n resources :#{route_name}\n end\nend"
1282
- route route_content
1285
+ # Look for the nested namespace pattern and insert after it (flexible whitespace matching)
1286
+ nested_pattern = /namespace\s+:#{@api_namespace}\s+do\s*\n\s*namespace\s+:#{@api_version}\s+do\s*\n/
1287
+
1288
+ if routes_content.match?(nested_pattern)
1289
+ # Use gsub_file to replace the namespace opening with namespace + our resource
1290
+ gsub_file "config/routes.rb", nested_pattern do |match|
1291
+ match + " resources :#{route_name}\n"
1283
1292
  end
1284
- elsif @api_namespace.present?
1285
- # Only namespace: insert into existing or create new
1286
- if has_single_namespace?(routes_content, @api_namespace)
1287
- resource_line = " resources :#{route_name}"
1288
- insert_into_single_namespace(routes_content, @api_namespace, resource_line)
1289
- else
1290
- # Create new namespace
1291
- # Rails route method adds 2 spaces to each line, so we need to adjust for that
1292
- route_content = "namespace :#{@api_namespace} do\n resources :#{route_name}\nend"
1293
- route route_content
1293
+ else
1294
+ # Create new namespace block using Rails' route method
1295
+ route "namespace :#{@api_namespace} do\n namespace :#{@api_version} do\n resources :#{route_name}\n end\nend"
1296
+ end
1297
+ end
1298
+
1299
+ def insert_single_namespace_route
1300
+ routes_file = File.join(destination_root, "config/routes.rb")
1301
+ routes_content = File.read(routes_file)
1302
+
1303
+ # Look for the single namespace pattern and insert after it
1304
+ single_pattern = /namespace\s+:#{@api_namespace}\s+do\s*\n/
1305
+
1306
+ if routes_content.match?(single_pattern)
1307
+ # Use gsub_file to replace the namespace opening with namespace + our resource
1308
+ gsub_file "config/routes.rb", single_pattern do |match|
1309
+ match + " resources :#{route_name}\n"
1294
1310
  end
1295
1311
  else
1296
- # No namespace: simple resources
1297
- route "resources :#{route_name}"
1312
+ # Create new namespace block using Rails' route method
1313
+ route "namespace :#{@api_namespace} do\n resources :#{route_name}\nend"
1298
1314
  end
1299
1315
  end
1300
1316
 
@@ -1336,58 +1352,13 @@ module PropelApi
1336
1352
  end
1337
1353
  end
1338
1354
 
1339
- def has_nested_namespace?(content, namespace, version)
1340
- content.match?(/namespace\s+:#{namespace}\s+do.*?namespace\s+:#{version}\s+do/m)
1341
- end
1342
-
1343
- def has_single_namespace?(content, namespace)
1344
- content.match?(/namespace\s+:#{namespace}\s+do/)
1345
- end
1346
-
1347
- def insert_into_nested_namespace(content, namespace, version, resource_line)
1348
- # Check if resource already exists
1349
- existing_pattern = /namespace\s+:#{namespace}\s+do.*?namespace\s+:#{version}\s+do.*?resources\s+:#{route_name}/m
1350
-
1351
- if content.match?(existing_pattern)
1352
- say "Route for #{route_name} already exists in #{namespace}/#{version} namespace", :yellow
1353
- return
1354
- end
1355
-
1356
- # Find the position to insert the resource line
1357
- # Look for the end of the nested namespace's opening line
1358
- after_pattern = /namespace\s+:#{namespace}\s+do\s*\n\s*namespace\s+:#{version}\s+do\s*\n/
1359
-
1360
- if content.match?(after_pattern)
1361
- # Insert the resource line with proper indentation (4 spaces for nested namespace)
1362
- insert_into_file "config/routes.rb", "#{resource_line}\n", :after => after_pattern
1363
- else
1364
- # This shouldn't happen if has_nested_namespace? returned true, but handle it
1365
- route_content = "namespace :#{namespace} do\n namespace :#{version} do\n#{resource_line}\n end\nend"
1366
- route route_content
1367
- end
1368
- end
1369
-
1370
- def insert_into_single_namespace(content, namespace, resource_line)
1371
- # Check if resource already exists
1372
- existing_pattern = /namespace\s+:#{namespace}\s+do.*?resources\s+:#{route_name}/m
1373
-
1374
- if content.match?(existing_pattern)
1375
- say "Route for #{route_name} already exists in #{namespace} namespace", :yellow
1376
- return
1377
- end
1378
-
1379
- # Find the position to insert the resource line
1380
- # Look for the end of the namespace's opening line
1381
- after_pattern = /namespace\s+:#{namespace}\s+do\s*\n/
1355
+ def route_already_exists?
1356
+ routes_file = File.join(destination_root, "config/routes.rb")
1357
+ return false unless File.exist?(routes_file)
1382
1358
 
1383
- if content.match?(after_pattern)
1384
- # Insert the resource line with proper indentation (resource_line already has correct 2 spaces)
1385
- insert_into_file "config/routes.rb", "#{resource_line}\n", :after => after_pattern
1386
- else
1387
- # This shouldn't happen if has_single_namespace? returned true, but handle it
1388
- route_content = "namespace :#{namespace} do\n resources :#{route_name}\n end"
1389
- route route_content
1390
- end
1359
+ routes_content = File.read(routes_file)
1360
+ # Check for actual route declarations (not comments) - must be at start of line or after whitespace
1361
+ routes_content.match?(/^\s*resources\s+:#{Regexp.escape(route_name)}(\s|$)/)
1391
1362
  end
1392
1363
 
1393
1364
  def cleanup_empty_namespaces(content)
@@ -37,6 +37,9 @@ module PropelApi
37
37
 
38
38
  # Optionally enhance CSRF error messages (non-breaking)
39
39
  add_csrf_error_handling
40
+
41
+ # Add dynamic filtering capabilities to ApplicationRecord
42
+ add_model_filters_concern
40
43
  end
41
44
 
42
45
  case @adapter
@@ -113,6 +116,28 @@ module PropelApi
113
116
 
114
117
  private
115
118
 
119
+ def copy_propel_facets_concerns
120
+ if behavior == :revoke
121
+ # Remove concerns
122
+ remove_file "app/controllers/concerns/propel_controller_filters_concern.rb"
123
+ remove_file "app/models/concerns/propel_model_filters_concern.rb"
124
+ remove_file "lib/propel_filter_operators.rb"
125
+ remove_file "lib/propel_dynamic_scope_generator.rb"
126
+ say "๐Ÿ—‘๏ธ Removed PropelApi filtering concerns", :red
127
+ else
128
+ # Copy concerns to host app using XX-prefixed source files
129
+ copy_file "concerns/propel_controller_filters_concern.rb", "app/controllers/concerns/propel_controller_filters_concern.rb"
130
+ copy_file "concerns/propel_model_filters_concern.rb", "app/models/concerns/propel_model_filters_concern.rb"
131
+ copy_file "lib/propel_filter_operators.rb", "lib/propel_filter_operators.rb"
132
+ copy_file "lib/propel_dynamic_scope_generator.rb", "lib/propel_dynamic_scope_generator.rb"
133
+ say "๐Ÿ“ฆ Copied PropelApi filtering concerns to app", :green
134
+ say " - Controller filtering: app/controllers/concerns/propel_controller_filters_concern.rb", :blue
135
+ say " - Model filtering: app/models/concerns/propel_model_filters_concern.rb", :blue
136
+ say " - Filter operators: lib/propel_filter_operators.rb", :blue
137
+ say " - Dynamic scope generator: lib/propel_dynamic_scope_generator.rb", :blue
138
+ end
139
+ end
140
+
116
141
  def create_api_base_controller
117
142
  template "controllers/api_base_controller.rb", "app/controllers/api/base_controller.rb"
118
143
  say "Created Api::BaseController for CSRF-free API endpoints", :green
@@ -144,6 +169,18 @@ module PropelApi
144
169
  say "You can manually add the rescue_from handler if desired", :blue
145
170
  end
146
171
 
172
+ def add_model_filters_concern
173
+ # Inject PropelModelFiltersConcern into ApplicationRecord
174
+ inject_into_class "app/models/application_record.rb", ApplicationRecord, <<-RUBY
175
+ include PropelModelFiltersConcern
176
+ RUBY
177
+
178
+ say "Enhanced ApplicationRecord with dynamic filtering capabilities", :green
179
+ rescue => e
180
+ say "Could not inject PropelModelFiltersConcern into ApplicationRecord: #{e.message}", :yellow
181
+ say "You can manually add 'include PropelModelFiltersConcern' to ApplicationRecord if desired", :blue
182
+ end
183
+
147
184
  def copy_propel_facets_controller
148
185
  template "controllers/api_controller_propel_facets.rb", api_controller_path
149
186
  copy_example_controller
@@ -256,8 +293,12 @@ module PropelApi
256
293
  say " end", :yellow
257
294
  say "\n5. See doc/api_controller_example.rb for complete usage examples"
258
295
  say "6. Your API endpoints will be available at: #{api_route_prefix}/..."
259
- say "7. Pagination and filtering are built-in via Pagy and HasScope"
260
- say "\n8. Generate resources using:"
296
+ say "7. Pagination and secure filtering are built-in via Pagy and PropelApi filtering"
297
+ say "8. Filtering examples:"
298
+ say " GET /api/v1/users?name_contains=john&active_eq=true&sort=-created_at", :yellow
299
+ say " - Secure field validation prevents SQL injection"
300
+ say " - Sensitive fields (passwords, tokens) automatically excluded", :cyan
301
+ say "\n9. Generate resources using:"
261
302
  say " rails generate propel_api Resource field1:type field2:type", :yellow
262
303
  end
263
304
 
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DynamicControllerFiltersConcern
4
+ #
5
+ # Provides dynamic filtering capabilities for API controllers using has_scope.
6
+ # This concern automatically generates and registers scopes based on the model's
7
+ # database columns and their types.
8
+ #
9
+ # Features:
10
+ # - Automatic scope generation based on column types
11
+ # - Security validation of filter parameters
12
+ # - Error handling for malformed filters
13
+ # - Support for all standard filter operators
14
+ #
15
+ # Usage:
16
+ # class Api::V1::TeamsController < Api::V1::ApiController
17
+ # include PropelControllerFiltersConcern
18
+ # end
19
+ #
20
+ module PropelControllerFiltersConcern
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ include HasScope
25
+
26
+ # Track which scopes have been registered to avoid duplicates
27
+ class_attribute :_registered_scopes, default: Set.new
28
+
29
+ # Register dynamic scopes when the controller is first used
30
+ before_action :ensure_dynamic_scopes_registered, prepend: true
31
+ end
32
+
33
+ private
34
+
35
+ def ensure_dynamic_scopes_registered
36
+ return if self.class._registered_scopes.include?(resource_class.name)
37
+
38
+ model_class = resource_class
39
+ return unless model_class&.respond_to?(:columns)
40
+
41
+ # Ensure scopes are generated for the model (lazy loading)
42
+ model_class.ensure_scopes_generated!
43
+
44
+ # Generate scopes for the model
45
+ generator = PropelDynamicScopeGenerator.new(model_class)
46
+ generator.generate_scopes!
47
+
48
+ # Register has_scope calls for this controller
49
+ generator.register_controller_scopes(self.class)
50
+
51
+ # Mark as registered to avoid duplicate work
52
+ self.class._registered_scopes << model_class.name
53
+ rescue StandardError => e
54
+ Rails.logger.error "Error registering dynamic scopes: #{e.message}"
55
+ Rails.logger.error e.backtrace.join("\n")
56
+ end
57
+
58
+ # Override apply_scopes to add security validation and error handling
59
+ def apply_scopes(base_scope)
60
+ # Filter out any potentially dangerous or unsupported parameters
61
+ safe_params = filter_safe_parameters(params)
62
+
63
+ # Apply scopes with error handling
64
+ result = super(base_scope, safe_params)
65
+ result || base_scope
66
+ rescue StandardError => e
67
+ Rails.logger.warn "Error in apply_scopes: #{e.message}"
68
+ base_scope
69
+ end
70
+
71
+ # Security: Filter out dangerous or unsupported parameters
72
+ def filter_safe_parameters(params)
73
+ return {} unless params.is_a?(ActionController::Parameters)
74
+
75
+ # Get allowed filter parameters based on model columns
76
+ allowed_filters = allowed_filter_parameters
77
+
78
+ # Filter to only include known, safe parameters
79
+ safe_params = params.slice(*allowed_filters)
80
+
81
+ # Log any filtered out parameters for security monitoring
82
+ filtered_params = params.except(*allowed_filters).except(:controller, :action, :format)
83
+ if filtered_params.present?
84
+ Rails.logger.warn "Filtered out potentially unsafe parameters: #{filtered_params.keys.join(', ')}"
85
+ end
86
+
87
+ safe_params
88
+ end
89
+
90
+ # Get list of allowed filter parameters based on model columns
91
+ def allowed_filter_parameters
92
+ return [] unless resource_class&.respond_to?(:columns)
93
+
94
+ allowed = []
95
+
96
+ resource_class.columns.each do |column|
97
+ name = column.name
98
+ type = column.type
99
+ sql_type = column.sql_type_metadata.sql_type
100
+
101
+ # Skip sensitive fields
102
+ next if sensitive_field?(name)
103
+
104
+ # Get operators for this column type
105
+ operators = PropelFilterOperators.operators_for(type, sql_type)
106
+
107
+ # Add parameter names for each operator
108
+ operators.each do |operator|
109
+ allowed << "#{name}_#{operator}".to_sym
110
+ end
111
+ end
112
+
113
+ # Add special parameters
114
+ allowed += [:order_by, :page, :limit, :per_page]
115
+
116
+ allowed
117
+ end
118
+
119
+ # Check if a field name is considered sensitive and should not be filterable
120
+ def sensitive_field?(field_name)
121
+ sensitive_fields = %w[
122
+ password password_hash password_digest
123
+ token secret_key api_key
124
+ encrypted_data encrypted_password
125
+ salt pepper
126
+ private_key public_key
127
+ ssn social_security_number
128
+ credit_card_number
129
+ bank_account_number
130
+ ]
131
+
132
+ sensitive_fields.any? { |sensitive| field_name.downcase.include?(sensitive) }
133
+ end
134
+
135
+ # Override to provide better error messages for invalid filters
136
+ def handle_invalid_filter(filter_name, error)
137
+ Rails.logger.warn "Invalid filter '#{filter_name}': #{error.message}"
138
+ # Don't raise the error, just log it and continue
139
+ # This prevents one bad filter from breaking the entire request
140
+ end
141
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DynamicModelFiltersConcern
4
+ #
5
+ # Provides dynamic filtering capabilities for ActiveRecord models.
6
+ # This concern automatically generates scopes based on the model's
7
+ # database columns and their types.
8
+ #
9
+ # Features:
10
+ # - Automatic scope generation based on column types
11
+ # - Support for all standard filter operators
12
+ # - Type-aware filtering (boolean, string, numeric, datetime)
13
+ # - Security validation of filter parameters
14
+ #
15
+ # Usage:
16
+ # class Team < ApplicationRecord
17
+ # include PropelModelFiltersConcern
18
+ # end
19
+ #
20
+ module PropelModelFiltersConcern
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ # Track which scopes have been generated to avoid duplicates
25
+ class_attribute :_generated_scopes, default: Set.new
26
+
27
+ # Generate scopes lazily when first needed
28
+ # This prevents issues with abstract classes like ApplicationRecord
29
+ end
30
+
31
+ class_methods do
32
+ # Generate all dynamic scopes for this model
33
+ def generate_dynamic_scopes!
34
+ return if _generated_scopes.include?(name)
35
+
36
+ # Skip abstract classes (like ApplicationRecord)
37
+ return if abstract_class?
38
+
39
+ # Skip if the model doesn't have a table (e.g., abstract classes)
40
+ return unless table_exists?
41
+
42
+ generator = PropelDynamicScopeGenerator.new(self)
43
+ generator.generate_scopes!
44
+
45
+ # Mark as generated to avoid duplicate work
46
+ _generated_scopes << name
47
+ rescue StandardError => e
48
+ Rails.logger.error "Error generating dynamic scopes for #{name}: #{e.message}"
49
+ Rails.logger.error e.backtrace.join("\n")
50
+ end
51
+
52
+ # Ensure scopes are generated (lazy loading)
53
+ def ensure_scopes_generated!
54
+ generate_dynamic_scopes! unless _generated_scopes.include?(name)
55
+ end
56
+
57
+ # Get all available filter parameters for this model
58
+ def available_filter_parameters
59
+ return [] unless respond_to?(:columns)
60
+
61
+ allowed = []
62
+
63
+ columns.each do |column|
64
+ name = column.name
65
+ type = column.type
66
+ sql_type = column.sql_type_metadata.sql_type
67
+
68
+ # Skip sensitive fields
69
+ next if sensitive_field?(name)
70
+
71
+ # Get operators for this column type
72
+ operators = PropelFilterOperators.operators_for(type, sql_type)
73
+
74
+ # Add parameter names for each operator
75
+ operators.each do |operator|
76
+ allowed << "#{name}_#{operator}".to_sym
77
+ end
78
+ end
79
+
80
+ allowed
81
+ end
82
+
83
+ # Check if a field name is considered sensitive
84
+ def sensitive_field?(field_name)
85
+ sensitive_fields = %w[
86
+ password password_hash password_digest
87
+ token secret_key api_key
88
+ encrypted_data encrypted_password
89
+ salt pepper
90
+ private_key public_key
91
+ ssn social_security_number
92
+ credit_card_number
93
+ bank_account_number
94
+ ]
95
+
96
+ sensitive_fields.any? { |sensitive| field_name.downcase.include?(sensitive) }
97
+ end
98
+
99
+ # Get filterable columns for this model
100
+ def filterable_columns
101
+ return [] unless respond_to?(:columns)
102
+
103
+ columns.reject { |column| sensitive_field?(column.name) }
104
+ end
105
+
106
+ # Get operators available for a specific column
107
+ def operators_for_column(column_name)
108
+ column = columns.find { |c| c.name == column_name }
109
+ return [] unless column
110
+
111
+ PropelFilterOperators.operators_for(column.type, column.sql_type_metadata.sql_type)
112
+ end
113
+
114
+ # Validate filter parameters
115
+ def validate_filter_parameters(params)
116
+ return {} unless params.is_a?(ActionController::Parameters)
117
+
118
+ allowed_filters = available_filter_parameters
119
+ params.slice(*allowed_filters)
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def ensure_dynamic_scopes_generated
126
+ self.class.generate_dynamic_scopes!
127
+ end
128
+ end
@@ -4,6 +4,7 @@ class <%= api_controller_class_name %> < Api::BaseController
4
4
  include FacetRenderer
5
5
  include StrongParamsHelper
6
6
  include PropelAuthenticationConcern
7
+ include PropelControllerFiltersConcern
7
8
 
8
9
  before_action :authenticate_user
9
10
  before_action :set_resource, only: [:show, :update, :destroy]