eluvia-base 3.39.0 → 3.40.0
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 +12 -0
- data/README.md +47 -1
- data/lib/eluvia/active_record/env_filterable.rb +110 -0
- data/lib/eluvia-base.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcf7c769d60f4347beee0d63aff1e1e8944e5fb2412985f531465dd833cfe58e
|
|
4
|
+
data.tar.gz: dc7491fcac983ef84e9f33396139370ce94fb65ce8718742acc4806d4e35e0b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a8eff124c578195160772b542e7ece4419b76ada79214f3b7d7ba3cb5c24768249b8d8c24ea207e37a33e6dc5d4d3b7196f83d369df428ae9724b36702af1e4
|
|
7
|
+
data.tar.gz: 2df71bbb4195fde554b62bc8052d02894be13b14c652325f08af43b94d4b23385499bf0dbe5847ae16786519e59de04be8b6d8a65f4e734f8202bc99f793a55a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.40.0] — 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Eluvia::ActiveRecord::EnvFilterable` — a generic, model-agnostic filter driven by deploy-time
|
|
12
|
+
config (typically a base64-encoded JSON ENV var). `env_filter_fields` declares the column
|
|
13
|
+
whitelist (the only part ever interpolated into SQL; all values stay bound parameters),
|
|
14
|
+
`parse_env_filter_config` decodes the base64 JSON, and `apply_env_filter(scope, config)` applies
|
|
15
|
+
the AND-combined `field`/`operator`/`value` conditions to a scope. Operators: `eq`, `not_eq`,
|
|
16
|
+
`in`, `not_in`, `like`, `not_like` (case-insensitive substring; negative operators are
|
|
17
|
+
NULL-safe). Extracted from the payments service so services need only declare their whitelist
|
|
18
|
+
(VD-2811).
|
|
19
|
+
|
|
8
20
|
## [3.39.0] — 2026-09-18
|
|
9
21
|
|
|
10
22
|
### Added
|
data/README.md
CHANGED
|
@@ -276,7 +276,53 @@ end
|
|
|
276
276
|
If `upload_key` is present on the assigned `Eluvia::File` but no finalizer is registered, a
|
|
277
277
|
`Eluvia::Errors::StandardError` is raised.
|
|
278
278
|
|
|
279
|
-
### 2.7.
|
|
279
|
+
### 2.7. ENV-driven scope restriction
|
|
280
|
+
|
|
281
|
+
`Eluvia::ActiveRecord::EnvFilterable` applies a deploy-time restriction — typically a base64-encoded
|
|
282
|
+
JSON blob in an ENV var — to any scope. It is generic and model-agnostic: a model only declares which
|
|
283
|
+
columns the config may reference, and the config lists `field` / `operator` / `value` conditions that
|
|
284
|
+
are AND-combined. Only declared fields are ever interpolated into SQL (as a column name); every value
|
|
285
|
+
stays a bound parameter, so the config cannot inject SQL. Conditions with an unknown field or operator
|
|
286
|
+
are silently ignored.
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
|
|
290
|
+
class BankTransaction < ApplicationRecord
|
|
291
|
+
include Eluvia::ActiveRecord::EnvFilterable
|
|
292
|
+
|
|
293
|
+
# Only these columns may appear in a filter config.
|
|
294
|
+
env_filter_fields %i[id direction variable_symbol date amount is_hidden]
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Read and apply the config (e.g. from a Pundit `policy_scope`):
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
|
|
302
|
+
config = BankTransaction.parse_env_filter_config(ENV['ADMIN_BANK_TRANSACTIONS_FILTER_B64'])
|
|
303
|
+
scope = BankTransaction.apply_env_filter(BankTransaction.all, config)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`parse_env_filter_config` returns `nil` for a blank/missing ENV var (which disables the filter) and
|
|
307
|
+
raises `ArgumentError` on malformed input, so a broken deploy config fails loudly instead of silently
|
|
308
|
+
letting everything through. The config shape is:
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"conditions": [
|
|
313
|
+
{ "field": "direction", "operator": "eq", "value": "in" },
|
|
314
|
+
{ "field": "is_hidden", "operator": "eq", "value": false },
|
|
315
|
+
{ "field": "variable_symbol", "operator": "like", "value": ["12", "34"] }
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Operators: `eq` / `not_eq` (equality), `in` / `not_in` (membership; value may be a scalar or array),
|
|
321
|
+
`like` / `not_like` (case-insensitive `ILIKE '%…%'` substring match; value may be a scalar or array —
|
|
322
|
+
`like` matches any, `not_like` excludes any). The negative operators (`not_eq`, `not_in`, `not_like`)
|
|
323
|
+
are NULL-safe: a row whose value is `NULL` is kept.
|
|
324
|
+
|
|
325
|
+
### 2.8. Translations
|
|
280
326
|
|
|
281
327
|
All validation error messages raised by this gem (ordering, filtering, fieldset and pagination errors) go through
|
|
282
328
|
`I18n.t` with an English `default:`, so the gem works out of the box without any locale setup. In addition,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
|
|
3
|
+
module Eluvia
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Generic, model-agnostic filter driven by deploy-time configuration (typically a base64-encoded
|
|
6
|
+
# JSON blob in an ENV var). The config is a list of `field` / `operator` / `value` conditions,
|
|
7
|
+
# AND-combined, that narrow a scope — e.g. to restrict what an admin interface may ever see.
|
|
8
|
+
#
|
|
9
|
+
# Only fields declared via `env_filter_fields` are ever interpolated into SQL (as a column name);
|
|
10
|
+
# every value is a bound parameter, so the config cannot be used to inject arbitrary SQL. Unknown
|
|
11
|
+
# fields and operators are silently ignored.
|
|
12
|
+
#
|
|
13
|
+
# @example Declaring the whitelist on a model
|
|
14
|
+
# class BankTransaction < ApplicationRecord
|
|
15
|
+
# include Eluvia::ActiveRecord::EnvFilterable
|
|
16
|
+
# env_filter_fields %i[id direction variable_symbol date amount is_hidden]
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Applying it to a scope (e.g. from a Pundit `policy_scope`)
|
|
20
|
+
# config = BankTransaction.parse_env_filter_config(ENV['ADMIN_BANK_TRANSACTIONS_FILTER_B64'])
|
|
21
|
+
# BankTransaction.apply_env_filter(scope, config)
|
|
22
|
+
module EnvFilterable
|
|
23
|
+
extend ::ActiveSupport::Concern
|
|
24
|
+
|
|
25
|
+
class_methods do
|
|
26
|
+
|
|
27
|
+
# Declares (with arguments) or returns (without arguments) the fields that may appear in an
|
|
28
|
+
# env filter config. Only these column names are ever interpolated into SQL. Names are stored
|
|
29
|
+
# as strings so they compare directly against the config's `field` values.
|
|
30
|
+
#
|
|
31
|
+
# @param field_names [Array<String, Symbol>] whitelisted column names
|
|
32
|
+
# @return [Array<String>] the declared whitelist
|
|
33
|
+
def env_filter_fields(*field_names)
|
|
34
|
+
if field_names.empty?
|
|
35
|
+
@_env_filter_fields ||= []
|
|
36
|
+
else
|
|
37
|
+
@_env_filter_fields = field_names.flatten.map(&:to_s)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Decodes a base64-encoded JSON filter config (typically read straight from an ENV var) into a
|
|
42
|
+
# Hash. Returns `nil` for blank input so a missing ENV var disables the filter. Raises
|
|
43
|
+
# `ArgumentError` on malformed input so a broken deploy-time config fails loudly rather than
|
|
44
|
+
# silently letting everything through.
|
|
45
|
+
#
|
|
46
|
+
# @param encoded [String, nil] base64-encoded JSON, or `nil`/blank
|
|
47
|
+
# @return [Hash, nil]
|
|
48
|
+
def parse_env_filter_config(encoded)
|
|
49
|
+
return nil if encoded.blank?
|
|
50
|
+
|
|
51
|
+
JSON.parse(Base64.strict_decode64(encoded.strip))
|
|
52
|
+
rescue ArgumentError, JSON::ParserError => e
|
|
53
|
+
raise ArgumentError, "invalid env filter config: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Applies an env filter config to +scope+. Config is a Hash with a `"conditions"` array; each
|
|
57
|
+
# condition has `"field"`, `"operator"` and `"value"`. All conditions are AND-combined. A blank
|
|
58
|
+
# config returns the scope unchanged; conditions with an unknown field or operator are ignored.
|
|
59
|
+
#
|
|
60
|
+
# Operators:
|
|
61
|
+
# eq / not_eq equality (not_eq keeps NULLs)
|
|
62
|
+
# in / not_in membership; value may be a scalar or array (not_in keeps NULLs)
|
|
63
|
+
# like / not_like case-insensitive substring match; value may be a scalar or an array
|
|
64
|
+
# (like → matches any; not_like → excludes any, keeps NULLs)
|
|
65
|
+
#
|
|
66
|
+
# @param scope [ActiveRecord::Relation]
|
|
67
|
+
# @param config [Hash, nil]
|
|
68
|
+
# @return [ActiveRecord::Relation]
|
|
69
|
+
def apply_env_filter(scope, config)
|
|
70
|
+
return scope if config.blank?
|
|
71
|
+
|
|
72
|
+
Array(config['conditions']).each do |cond|
|
|
73
|
+
field = cond['field'].to_s
|
|
74
|
+
op = cond['operator'].to_s
|
|
75
|
+
next unless env_filter_fields.include?(field)
|
|
76
|
+
|
|
77
|
+
col = "#{table_name}.#{field}" # field is whitelisted → safe to interpolate
|
|
78
|
+
val = cond['value']
|
|
79
|
+
|
|
80
|
+
scope =
|
|
81
|
+
case op
|
|
82
|
+
when 'eq'
|
|
83
|
+
scope.where(field => val)
|
|
84
|
+
when 'not_eq'
|
|
85
|
+
scope.where("#{col} IS NULL OR #{col} <> ?", val)
|
|
86
|
+
when 'in'
|
|
87
|
+
scope.where(field => Array(val))
|
|
88
|
+
when 'not_in'
|
|
89
|
+
scope.where("#{col} IS NULL OR #{col} NOT IN (?)", Array(val))
|
|
90
|
+
when 'like'
|
|
91
|
+
Array(val).each_with_index.reduce(scope) do |s, (v, i)|
|
|
92
|
+
clause = scope.where("#{col} ILIKE ('%' || ? || '%')", v)
|
|
93
|
+
i.zero? ? clause : s.or(clause)
|
|
94
|
+
end
|
|
95
|
+
when 'not_like'
|
|
96
|
+
Array(val).reduce(scope) do |s, v|
|
|
97
|
+
s.where("#{col} IS NULL OR #{col} NOT ILIKE ('%' || ? || '%')", v)
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
scope
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
scope
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/eluvia-base.rb
CHANGED
|
@@ -20,6 +20,7 @@ require 'eluvia/active_model/object_attribute'
|
|
|
20
20
|
require 'eluvia/active_model/range_attribute' # depends on object_attribute
|
|
21
21
|
|
|
22
22
|
# Active record
|
|
23
|
+
require 'eluvia/active_record/env_filterable'
|
|
23
24
|
require 'eluvia/active_record/file_array_attribute'
|
|
24
25
|
require 'eluvia/active_record/file_attribute'
|
|
25
26
|
require 'eluvia/active_record/filtering'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eluvia-base
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matěj Outlý
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rest-client
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '2.19'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: base64
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.2'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.2'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: ostruct
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,6 +142,7 @@ files:
|
|
|
128
142
|
- lib/eluvia/active_model/enum_attribute.rb
|
|
129
143
|
- lib/eluvia/active_model/object_attribute.rb
|
|
130
144
|
- lib/eluvia/active_model/range_attribute.rb
|
|
145
|
+
- lib/eluvia/active_record/env_filterable.rb
|
|
131
146
|
- lib/eluvia/active_record/file_array_attribute.rb
|
|
132
147
|
- lib/eluvia/active_record/file_attribute.rb
|
|
133
148
|
- lib/eluvia/active_record/filtering.rb
|