propel_api 0.3.2 โ 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/README.md +46 -1
- data/lib/generators/propel_api/core/named_base.rb +47 -76
- data/lib/generators/propel_api/install/install_generator.rb +43 -2
- data/lib/generators/propel_api/templates/concerns/propel_controller_filters_concern.rb +141 -0
- data/lib/generators/propel_api/templates/concerns/propel_model_filters_concern.rb +128 -0
- data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +1 -0
- data/lib/generators/propel_api/templates/lib/XX_dynamic_scope_generator.rb +360 -0
- data/lib/generators/propel_api/templates/lib/propel_dynamic_scope_generator.rb +474 -0
- data/lib/generators/propel_api/templates/lib/propel_filter_operators.rb +16 -0
- data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +14 -0
- data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +100 -6
- data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +9 -0
- data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +660 -10
- data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +7 -0
- data/lib/propel_api.rb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab74f199f5f59a6a62a0f39a8c63960a7b1339b46bd32701219a6249f6cdb5a3
|
4
|
+
data.tar.gz: c4aa9676d91f4400cd54f0aa6d5ce0a022f9b6c893ec7b3124da057f679d8770
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
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
|
-
|
1285
|
-
#
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
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
|
-
#
|
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
|
1340
|
-
|
1341
|
-
|
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
|
-
|
1384
|
-
|
1385
|
-
|
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
|
260
|
-
say "
|
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]
|