familia 2.0.0.pre16 → 2.0.0.pre18
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/.github/workflows/ci.yml +2 -2
- data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.rst +82 -0
- data/CLAUDE.md +47 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/overview.md +2 -2
- data/docs/qodo-merge-compliance.md +96 -0
- data/docs/reference/api-technical.md +1 -1
- data/examples/encrypted_fields.rb +1 -1
- data/examples/safe_dump.rb +1 -1
- data/lib/familia/base.rb +6 -6
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/hashkey.rb +2 -1
- data/lib/familia/data_type/types/sorted_set.rb +113 -10
- data/lib/familia/data_type/types/stringkey.rb +0 -4
- data/lib/familia/data_type.rb +8 -195
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/encrypted_fields.rb +5 -2
- data/lib/familia/features/external_identifier.rb +49 -8
- data/lib/familia/features/object_identifier.rb +84 -12
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
- data/lib/familia/features/relationships/indexing.rb +7 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features/transient_fields.rb +7 -2
- data/lib/familia/features.rb +6 -1
- data/lib/familia/field_type.rb +0 -18
- data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
- data/lib/familia/horreum/serialization.rb +190 -0
- data/lib/familia/horreum.rb +39 -14
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +16 -1
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +1 -1
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +3 -3
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- data/try/{core → integration}/database_consistency_try.rb +12 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
- data/try/{models → integration/models}/customer_try.rb +6 -6
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +2 -2
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +15 -7
- data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +5 -5
- data/try/{core → unit/core}/errors_try.rb +4 -4
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +2 -2
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +3 -3
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/{data_types → unit/data_types}/string_try.rb +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- data/try/{horreum → unit/horreum}/commands_try.rb +3 -1
- data/try/unit/horreum/defensive_initialization_try.rb +86 -0
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +3 -1
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- data/try/valkey.conf +26 -0
- metadata +149 -132
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
- /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
data/lib/familia/data_type.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# lib/familia/data_type.rb
|
2
2
|
|
3
|
-
require_relative 'data_type/
|
3
|
+
require_relative 'data_type/class_methods'
|
4
|
+
require_relative 'data_type/settings'
|
5
|
+
require_relative 'data_type/connection'
|
6
|
+
require_relative 'data_type/database_commands'
|
4
7
|
require_relative 'data_type/serialization'
|
5
8
|
|
6
9
|
# Familia
|
@@ -14,6 +17,7 @@ module Familia
|
|
14
17
|
# @abstract Subclass and implement Database data type specific methods
|
15
18
|
class DataType
|
16
19
|
include Familia::Base
|
20
|
+
extend ClassMethods
|
17
21
|
extend Familia::Features
|
18
22
|
|
19
23
|
using Familia::Refinements::TimeLiterals
|
@@ -29,60 +33,6 @@ module Familia
|
|
29
33
|
attr_reader :registered_types, :valid_options, :has_related_fields
|
30
34
|
end
|
31
35
|
|
32
|
-
# DataType::ClassMethods
|
33
|
-
#
|
34
|
-
module ClassMethods
|
35
|
-
attr_accessor :parent, :suffix, :prefix, :uri
|
36
|
-
attr_writer :logical_database
|
37
|
-
|
38
|
-
# To be called inside every class that inherits DataType
|
39
|
-
# +methname+ is the term used for the class and instance methods
|
40
|
-
# that are created for the given +klass+ (e.g. set, list, etc)
|
41
|
-
def register(klass, methname)
|
42
|
-
Familia.trace :REGISTER, nil, "[#{self}] Registering #{klass} as #{methname.inspect}" if Familia.debug?
|
43
|
-
|
44
|
-
@registered_types[methname] = klass
|
45
|
-
end
|
46
|
-
|
47
|
-
# Get the registered type class from a given method name
|
48
|
-
# +methname+ is the method name used to register the class (e.g. :set, :list, etc)
|
49
|
-
# Returns the registered class or nil if not found
|
50
|
-
def registered_type(methname)
|
51
|
-
@registered_types[methname]
|
52
|
-
end
|
53
|
-
|
54
|
-
def logical_database(val = nil)
|
55
|
-
@logical_database = val unless val.nil?
|
56
|
-
@logical_database || parent&.logical_database
|
57
|
-
end
|
58
|
-
|
59
|
-
def uri(val = nil)
|
60
|
-
@uri = val unless val.nil?
|
61
|
-
@uri || (parent ? parent.uri : Familia.uri)
|
62
|
-
end
|
63
|
-
|
64
|
-
def inherited(obj)
|
65
|
-
Familia.trace :DATATYPE, nil, "#{obj} is my kinda type" if Familia.debug?
|
66
|
-
obj.logical_database = logical_database
|
67
|
-
obj.default_expiration = default_expiration # method added via Features::Expiration
|
68
|
-
obj.uri = uri
|
69
|
-
super
|
70
|
-
end
|
71
|
-
|
72
|
-
def valid_keys_only(opts)
|
73
|
-
opts.slice(*DataType.valid_options)
|
74
|
-
end
|
75
|
-
|
76
|
-
def relations?
|
77
|
-
@has_related_fields ||= false
|
78
|
-
end
|
79
|
-
end
|
80
|
-
extend ClassMethods
|
81
|
-
|
82
|
-
attr_reader :keystring, :opts, :uri, :logical_database
|
83
|
-
|
84
|
-
alias url uri
|
85
|
-
|
86
36
|
# +keystring+: If parent is set, this will be used as the suffix
|
87
37
|
# for dbkey. Otherwise this becomes the value of the key.
|
88
38
|
# If this is an Array, the elements will be joined.
|
@@ -125,146 +75,9 @@ module Familia
|
|
125
75
|
init if respond_to? :init
|
126
76
|
end
|
127
77
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
return @dbclient if @dbclient
|
132
|
-
|
133
|
-
# Delegate to parent if present, otherwise fall back to Familia
|
134
|
-
parent ? parent.dbclient : Familia.dbclient(opts[:logical_database])
|
135
|
-
end
|
136
|
-
|
137
|
-
# Produces the full dbkey for this object.
|
138
|
-
#
|
139
|
-
# @return [String] The full dbkey.
|
140
|
-
#
|
141
|
-
# This method determines the appropriate dbkey based on the context of the DataType object:
|
142
|
-
#
|
143
|
-
# 1. If a hardcoded key is set in the options, it returns that key.
|
144
|
-
# 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
|
145
|
-
# 3. For class-level DataType objects, it uses the parent class's dbkey method.
|
146
|
-
# 4. For standalone DataType objects, it uses the keystring as the full dbkey.
|
147
|
-
#
|
148
|
-
# For class-level DataType objects (parent_class? == true):
|
149
|
-
# - The suffix is optional and used to differentiate between different types of objects.
|
150
|
-
# - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
|
151
|
-
# - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
|
152
|
-
# - Passing nil as the suffix is how class-level DataType objects are created without
|
153
|
-
# the global default 'object' suffix.
|
154
|
-
#
|
155
|
-
# @example Instance-level DataType
|
156
|
-
# user_instance.some_datatype.dbkey # => "user:123:some_datatype"
|
157
|
-
#
|
158
|
-
# @example Class-level DataType
|
159
|
-
# User.some_datatype.dbkey # => "user:some_datatype"
|
160
|
-
#
|
161
|
-
# @example Standalone DataType
|
162
|
-
# DataType.new("mykey").dbkey # => "mykey"
|
163
|
-
#
|
164
|
-
# @example Class-level DataType with explicit nil suffix
|
165
|
-
# User.dbkey("123", nil) # => "user:123"
|
166
|
-
#
|
167
|
-
def dbkey
|
168
|
-
# Return the hardcoded key if it's set. This is useful for
|
169
|
-
# support legacy keys that aren't derived in the same way.
|
170
|
-
return opts[:dbkey] if opts[:dbkey]
|
171
|
-
|
172
|
-
if parent_instance?
|
173
|
-
# This is an instance-level datatype object so the parent instance's
|
174
|
-
# dbkey method is defined in Familia::Horreum::InstanceMethods.
|
175
|
-
parent.dbkey(keystring)
|
176
|
-
elsif parent_class?
|
177
|
-
# This is a class-level datatype object so the parent class' dbkey
|
178
|
-
# method is defined in Familia::Horreum::DefinitionMethods.
|
179
|
-
parent.dbkey(keystring, nil)
|
180
|
-
else
|
181
|
-
# This is a standalone DataType object where it's keystring
|
182
|
-
# is the full database key (dbkey).
|
183
|
-
keystring
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def class?
|
188
|
-
!@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
|
189
|
-
end
|
190
|
-
|
191
|
-
# Provides a structured way to "gear down" to run db commands that are
|
192
|
-
# not implemented in our DataType classes since we intentionally don't
|
193
|
-
# have a method_missing method.
|
194
|
-
def direct_access
|
195
|
-
yield(dbclient, dbkey)
|
196
|
-
end
|
197
|
-
|
198
|
-
def parent_instance?
|
199
|
-
parent&.is_a?(Horreum::ParentDefinition)
|
200
|
-
end
|
201
|
-
|
202
|
-
def parent_class?
|
203
|
-
parent.is_a?(Class) && parent.ancestors.include?(Familia::Horreum)
|
204
|
-
end
|
205
|
-
|
206
|
-
def parent?
|
207
|
-
parent_class? || parent_instance?
|
208
|
-
end
|
209
|
-
|
210
|
-
def parent
|
211
|
-
# Return cached ParentDefinition if available
|
212
|
-
return @parent if @parent
|
213
|
-
|
214
|
-
# Return class-level parent if no instance parent
|
215
|
-
return self.class.parent unless @parent_ref
|
216
|
-
|
217
|
-
# Create ParentDefinition dynamically from stored reference.
|
218
|
-
# This ensures we get the current identifier value (available after initialization)
|
219
|
-
# rather than a stale nil value from initialization time. Cannot cache due to frozen object.
|
220
|
-
Horreum::ParentDefinition.from_parent(@parent_ref)
|
221
|
-
end
|
222
|
-
|
223
|
-
def parent=(value)
|
224
|
-
case value
|
225
|
-
when Horreum::ParentDefinition
|
226
|
-
@parent = value
|
227
|
-
when nil
|
228
|
-
@parent = nil
|
229
|
-
@parent_ref = nil
|
230
|
-
else
|
231
|
-
# Store parent instance reference for lazy ParentDefinition creation.
|
232
|
-
# During initialization, the parent's identifier may not be available yet,
|
233
|
-
# so we defer ParentDefinition creation until first access for memory efficiency.
|
234
|
-
# Note: @parent_ref is not cleared after use because DataType objects are frozen.
|
235
|
-
@parent_ref = value
|
236
|
-
@parent = nil # Will be created dynamically in parent method
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def uri
|
241
|
-
# Return explicit instance URI if set
|
242
|
-
return @uri if @uri
|
243
|
-
|
244
|
-
# If we have a parent with logical_database, build URI with that database
|
245
|
-
if parent && parent.respond_to?(:logical_database) && parent.logical_database
|
246
|
-
new_uri = (self.class.uri || Familia.uri).dup
|
247
|
-
new_uri.db = parent.logical_database
|
248
|
-
new_uri
|
249
|
-
else
|
250
|
-
# Fall back to class-level URI or global Familia.uri
|
251
|
-
self.class.uri || Familia.uri
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def uri=(value)
|
256
|
-
@uri = value
|
257
|
-
end
|
258
|
-
|
259
|
-
def dump_method
|
260
|
-
self.class.dump_method
|
261
|
-
end
|
262
|
-
|
263
|
-
def load_method
|
264
|
-
self.class.load_method
|
265
|
-
end
|
266
|
-
|
267
|
-
include Commands
|
78
|
+
include Settings
|
79
|
+
include Connection
|
80
|
+
include DatabaseCommands
|
268
81
|
include Serialization
|
269
82
|
end
|
270
83
|
|
@@ -44,8 +44,18 @@ module Familia
|
|
44
44
|
new(**parsed)
|
45
45
|
end
|
46
46
|
|
47
|
-
def self.from_json(
|
48
|
-
|
47
|
+
def self.from_json(json_string_or_hash)
|
48
|
+
# Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
|
49
|
+
if json_string_or_hash.is_a?(Hash)
|
50
|
+
# Already parsed - use directly
|
51
|
+
parsed = json_string_or_hash
|
52
|
+
# Symbolize keys if they're strings
|
53
|
+
parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
|
54
|
+
new(**parsed)
|
55
|
+
else
|
56
|
+
# JSON string - validate and parse
|
57
|
+
validate!(json_string_or_hash)
|
58
|
+
end
|
49
59
|
end
|
50
60
|
|
51
61
|
# Instance methods for decryptability validation
|
@@ -32,15 +32,22 @@ module Familia
|
|
32
32
|
Familia::Encryption.secure_wipe(key) if key
|
33
33
|
end
|
34
34
|
|
35
|
-
def decrypt(
|
36
|
-
return nil if
|
35
|
+
def decrypt(encrypted_json_or_hash, context:, additional_data: nil)
|
36
|
+
return nil if encrypted_json_or_hash.nil? || (encrypted_json_or_hash.respond_to?(:empty?) && encrypted_json_or_hash.empty?)
|
37
37
|
|
38
38
|
# Increment counter immediately to track all decryption attempts, even failed ones
|
39
39
|
Familia::Encryption.derivation_count.increment
|
40
40
|
|
41
41
|
begin
|
42
|
-
|
43
|
-
|
42
|
+
# Delegate parsing and instantiation to EncryptedData.from_json
|
43
|
+
# Wrap validation errors for security (don't expose internal structure details)
|
44
|
+
begin
|
45
|
+
data = Familia::Encryption::EncryptedData.from_json(encrypted_json_or_hash)
|
46
|
+
raise EncryptionError, 'Failed to parse encrypted data' unless data
|
47
|
+
rescue EncryptionError => e
|
48
|
+
# Re-wrap validation errors with generic message for security
|
49
|
+
raise EncryptionError, "Decryption failed: #{e.message}"
|
50
|
+
end
|
44
51
|
|
45
52
|
# Validate algorithm support
|
46
53
|
provider = Registry.get(data.algorithm)
|
@@ -28,7 +28,9 @@ module Familia::Features
|
|
28
28
|
]
|
29
29
|
|
30
30
|
# Ensure the Features module exists within the base module
|
31
|
-
|
31
|
+
unless base.const_defined?(:Features) || config_name.eql?('features')
|
32
|
+
base.const_set(:Features, Module.new)
|
33
|
+
end
|
32
34
|
|
33
35
|
# Use the shared autoload_files method
|
34
36
|
autoload_files(dir_patterns, log_prefix: "Autoloader[#{config_name}]")
|
@@ -29,8 +29,10 @@ module Familia
|
|
29
29
|
# Already concealed, store as-is
|
30
30
|
instance_variable_set(:"@#{field_name}", value)
|
31
31
|
elsif field_type.encrypted_json?(value)
|
32
|
-
# Already encrypted JSON from database - wrap in ConcealedString without re-encrypting
|
33
|
-
|
32
|
+
# Already encrypted (JSON string or Hash from database) - wrap in ConcealedString without re-encrypting
|
33
|
+
# Convert Hash back to JSON string if needed (v2.0 deserialization returns Hash)
|
34
|
+
encrypted_string = value.is_a?(Hash) ? Familia::JsonSerializer.dump(value) : value
|
35
|
+
concealed = ConcealedString.new(encrypted_string, self, field_type)
|
34
36
|
instance_variable_set(:"@#{field_name}", concealed)
|
35
37
|
else
|
36
38
|
# Encrypt plaintext and wrap in ConcealedString
|
@@ -138,7 +140,13 @@ module Familia
|
|
138
140
|
|
139
141
|
# Check if a string looks like encrypted JSON data
|
140
142
|
def encrypted_json?(data)
|
141
|
-
|
143
|
+
# Support both JSON strings (legacy) and Hashes (v2.0 deserialization)
|
144
|
+
if data.is_a?(Hash)
|
145
|
+
required_keys = %w[algorithm nonce ciphertext auth_tag key_version]
|
146
|
+
required_keys.all? { |key| data.key?(key) || data.key?(key.to_sym) }
|
147
|
+
else
|
148
|
+
Familia::Encryption::EncryptedData.valid?(data)
|
149
|
+
end
|
142
150
|
end
|
143
151
|
|
144
152
|
private
|
@@ -259,7 +259,7 @@ module Familia
|
|
259
259
|
# - Insider threats with application access
|
260
260
|
#
|
261
261
|
module EncryptedFields
|
262
|
-
Familia::Base.add_feature self, :encrypted_fields
|
262
|
+
Familia::Base.add_feature self, :encrypted_fields, depends_on: nil, field_group: :encrypted_fields
|
263
263
|
|
264
264
|
def self.included(base)
|
265
265
|
Familia.trace :LOADED, self, base if Familia.debug?
|
@@ -297,7 +297,10 @@ module Familia
|
|
297
297
|
@encrypted_fields ||= []
|
298
298
|
@encrypted_fields << name unless @encrypted_fields.include?(name)
|
299
299
|
|
300
|
-
|
300
|
+
# Add to field_groups if the group exists
|
301
|
+
if field_groups&.key?(:encrypted_fields)
|
302
|
+
field_groups[:encrypted_fields] << name
|
303
|
+
end
|
301
304
|
|
302
305
|
field_type = EncryptedFieldType.new(name, aad_fields: aad_fields, **)
|
303
306
|
register_field_type(field_type)
|
@@ -10,6 +10,7 @@ module Familia
|
|
10
10
|
def self.included(base)
|
11
11
|
Familia.trace :LOADED, self, base if Familia.debug?
|
12
12
|
base.extend ModelClassMethods
|
13
|
+
base.include ModelInstanceMethods
|
13
14
|
|
14
15
|
# Ensure default prefix is set in feature options
|
15
16
|
base.add_feature_options(:external_identifier, prefix: 'ext')
|
@@ -75,9 +76,6 @@ module Familia
|
|
75
76
|
|
76
77
|
instance_variable_set(:"@#{field_name}", derived_extid)
|
77
78
|
|
78
|
-
# Update mapping if we have an identifier (objid)
|
79
|
-
self.class.extid_lookup[derived_extid] = identifier if respond_to?(:identifier) && identifier
|
80
|
-
|
81
79
|
derived_extid
|
82
80
|
end
|
83
81
|
end
|
@@ -103,11 +101,6 @@ module Familia
|
|
103
101
|
|
104
102
|
# Set the new value
|
105
103
|
instance_variable_set(:"@#{field_name}", value)
|
106
|
-
|
107
|
-
# Update mapping if we have both extid and identifier
|
108
|
-
return unless value && respond_to?(:identifier) && identifier
|
109
|
-
|
110
|
-
self.class.extid_lookup[value] = identifier
|
111
104
|
end
|
112
105
|
end
|
113
106
|
end
|
@@ -159,6 +152,54 @@ module Familia
|
|
159
152
|
end
|
160
153
|
end
|
161
154
|
|
155
|
+
# Instance methods for external identifier management
|
156
|
+
module ModelInstanceMethods
|
157
|
+
# Override save to update extid_lookup mapping
|
158
|
+
#
|
159
|
+
# This ensures the extid_lookup index is populated during save operations
|
160
|
+
# rather than during object initialization, preventing unwanted database
|
161
|
+
# writes when calling .new()
|
162
|
+
#
|
163
|
+
# @param update_expiration [Boolean] Whether to update key expiration
|
164
|
+
# @return [Boolean] True if save was successful
|
165
|
+
#
|
166
|
+
def save(update_expiration: true)
|
167
|
+
result = super
|
168
|
+
|
169
|
+
# Update extid_lookup mapping after successful save
|
170
|
+
if result && respond_to?(:extid) && respond_to?(:identifier)
|
171
|
+
current_extid = extid # Triggers lazy generation if needed
|
172
|
+
if current_extid && identifier
|
173
|
+
self.class.extid_lookup[current_extid] = identifier
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
result
|
178
|
+
end
|
179
|
+
|
180
|
+
# Override save_if_not_exists to update extid_lookup mapping
|
181
|
+
#
|
182
|
+
# This ensures the extid_lookup index is populated during create operations
|
183
|
+
# which use save_if_not_exists instead of save.
|
184
|
+
#
|
185
|
+
# @param update_expiration [Boolean] Whether to update key expiration
|
186
|
+
# @return [Boolean] True if save was successful
|
187
|
+
#
|
188
|
+
def save_if_not_exists(update_expiration: true)
|
189
|
+
result = super
|
190
|
+
|
191
|
+
# Update extid_lookup mapping after successful save
|
192
|
+
if result && respond_to?(:extid) && respond_to?(:identifier)
|
193
|
+
current_extid = extid # Triggers lazy generation if needed
|
194
|
+
if current_extid && identifier
|
195
|
+
self.class.extid_lookup[current_extid] = identifier
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
result
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
162
203
|
# Derives a deterministic, public-facing external identifier from the object's
|
163
204
|
# internal `objid`.
|
164
205
|
#
|
@@ -88,6 +88,7 @@ module Familia
|
|
88
88
|
def self.included(base)
|
89
89
|
Familia.trace :LOADED, self, base if Familia.debug?
|
90
90
|
base.extend ModelClassMethods
|
91
|
+
base.include ModelInstanceMethods
|
91
92
|
|
92
93
|
# Ensure default generator is set in feature options
|
93
94
|
base.add_feature_options(:object_identifier, generator: DEFAULT_GENERATOR)
|
@@ -160,9 +161,6 @@ module Familia
|
|
160
161
|
generator = options[:generator] || DEFAULT_GENERATOR
|
161
162
|
instance_variable_set(:"@#{field_name}_generator_used", generator)
|
162
163
|
|
163
|
-
# Update mapping from objid to model primary key
|
164
|
-
self.class.objid_lookup[generated_id] = identifier if respond_to?(:identifier) && identifier
|
165
|
-
|
166
164
|
generated_id
|
167
165
|
end
|
168
166
|
end
|
@@ -198,14 +196,11 @@ module Familia
|
|
198
196
|
|
199
197
|
instance_variable_set(:"@#{field_name}", value)
|
200
198
|
|
201
|
-
# Update mapping from objid to this new identifier
|
202
|
-
self.class.objid_lookup[value] = identifier unless value.nil? || identifier.nil?
|
203
|
-
|
204
199
|
# When setting objid from external source (e.g., loading from Valkey/Redis),
|
205
|
-
#
|
206
|
-
#
|
207
|
-
|
208
|
-
instance_variable_set(:"@#{field_name}_generator_used",
|
200
|
+
# infer the generator type from the format to restore provenance tracking.
|
201
|
+
# This allows features like ExternalIdentifier to work correctly on loaded objects.
|
202
|
+
inferred_generator = infer_objid_generator(value)
|
203
|
+
instance_variable_set(:"@#{field_name}_generator_used", inferred_generator)
|
209
204
|
end
|
210
205
|
end
|
211
206
|
end
|
@@ -284,6 +279,54 @@ module Familia
|
|
284
279
|
end
|
285
280
|
end
|
286
281
|
|
282
|
+
# Instance methods for object identifier management
|
283
|
+
module ModelInstanceMethods
|
284
|
+
# Override save to update objid_lookup mapping
|
285
|
+
#
|
286
|
+
# This ensures the objid_lookup index is populated during save operations
|
287
|
+
# rather than during object initialization, preventing unwanted database
|
288
|
+
# writes when calling .new()
|
289
|
+
#
|
290
|
+
# @param update_expiration [Boolean] Whether to update key expiration
|
291
|
+
# @return [Boolean] True if save was successful
|
292
|
+
#
|
293
|
+
def save(update_expiration: true)
|
294
|
+
result = super
|
295
|
+
|
296
|
+
# Update objid_lookup mapping after successful save
|
297
|
+
if result && respond_to?(:objid) && respond_to?(:identifier)
|
298
|
+
current_objid = objid # Triggers lazy generation if needed
|
299
|
+
if current_objid && identifier
|
300
|
+
self.class.objid_lookup[current_objid] = identifier
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
result
|
305
|
+
end
|
306
|
+
|
307
|
+
# Override save_if_not_exists to update objid_lookup mapping
|
308
|
+
#
|
309
|
+
# This ensures the objid_lookup index is populated during create operations
|
310
|
+
# which use save_if_not_exists instead of save.
|
311
|
+
#
|
312
|
+
# @param update_expiration [Boolean] Whether to update key expiration
|
313
|
+
# @return [Boolean] True if save was successful
|
314
|
+
#
|
315
|
+
def save_if_not_exists(update_expiration: true)
|
316
|
+
result = super
|
317
|
+
|
318
|
+
# Update objid_lookup mapping after successful save
|
319
|
+
if result && respond_to?(:objid) && respond_to?(:identifier)
|
320
|
+
current_objid = objid # Triggers lazy generation if needed
|
321
|
+
if current_objid && identifier
|
322
|
+
self.class.objid_lookup[current_objid] = identifier
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
result
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
287
330
|
# Instance method for generating object identifier using configured strategy
|
288
331
|
#
|
289
332
|
# This method is called by the ObjectIdentifierFieldType when lazy generation
|
@@ -304,10 +347,39 @@ module Familia
|
|
304
347
|
objid
|
305
348
|
end
|
306
349
|
|
307
|
-
#
|
350
|
+
# Infers the generator type (:uuid_v7, :uuid_v4, :hex) from the format of an objid string.
|
308
351
|
#
|
309
|
-
#
|
352
|
+
# This method analyzes the objid format to restore provenance tracking when loading
|
353
|
+
# objects from Redis, allowing dependent features like ExternalIdentifier to work correctly.
|
310
354
|
#
|
355
|
+
# @param objid_value [String] The objid string to analyze
|
356
|
+
# @return [Symbol, nil] The inferred generator type or nil if unknown
|
357
|
+
def infer_objid_generator(objid_value)
|
358
|
+
return nil if objid_value.nil? || objid_value.to_s.empty?
|
359
|
+
|
360
|
+
objid_str = objid_value.to_s
|
361
|
+
|
362
|
+
# UUID format: xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx (36 chars with hyphens)
|
363
|
+
# where V is the version nibble at position 14
|
364
|
+
if objid_str.length == 36 && objid_str[8] == '-' && objid_str[13] == '-' && objid_str[18] == '-' && objid_str[23] == '-'
|
365
|
+
version_char = objid_str[14]
|
366
|
+
case version_char
|
367
|
+
when '7'
|
368
|
+
:uuid_v7
|
369
|
+
when '4'
|
370
|
+
:uuid_v4
|
371
|
+
else
|
372
|
+
nil # Unknown UUID version
|
373
|
+
end
|
374
|
+
# Hex format: pure hexadecimal without hyphens (32 or 64 chars typically)
|
375
|
+
elsif objid_str.match?(/\A[0-9a-fA-F]+\z/)
|
376
|
+
:hex
|
377
|
+
else
|
378
|
+
nil # Unknown format
|
379
|
+
end
|
380
|
+
end
|
381
|
+
private :infer_objid_generator
|
382
|
+
|
311
383
|
def object_identifier=(value)
|
312
384
|
self.objid = value
|
313
385
|
end
|
@@ -75,7 +75,7 @@ module Familia
|
|
75
75
|
actual_target_class.class_eval do
|
76
76
|
# Helper method to get index set for a specific field value
|
77
77
|
# This acts as a factory for field-value-specific DataTypes
|
78
|
-
define_method("#{index_name}_for") do |field_value|
|
78
|
+
define_method(:"#{index_name}_for") do |field_value|
|
79
79
|
# Return properly managed DataType instance with parameterized key
|
80
80
|
index_key = "#{index_name}:#{field_value}"
|
81
81
|
Familia::UnsortedSet.new(index_key, parent: self)
|
@@ -99,26 +99,26 @@ module Familia
|
|
99
99
|
# Generate instance sampling method (e.g., company.sample_from_department)
|
100
100
|
actual_target_class.class_eval do
|
101
101
|
|
102
|
-
define_method("sample_from_#{field}") do |field_value, count = 1|
|
102
|
+
define_method(:"sample_from_#{field}") do |field_value, count = 1|
|
103
103
|
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
104
104
|
|
105
105
|
# Get random members efficiently (O(1) via SRANDMEMBER with count)
|
106
106
|
# Returns array even for count=1 for consistent API
|
107
107
|
index_set.sample(count).map do |id|
|
108
|
-
indexed_class.
|
108
|
+
indexed_class.find_by_identifier(id)
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
112
|
# Generate bulk query method (e.g., company.find_all_by_department)
|
113
|
-
define_method("find_all_by_#{field}") do |field_value|
|
113
|
+
define_method(:"find_all_by_#{field}") do |field_value|
|
114
114
|
index_set = send("#{index_name}_for", field_value) # i.e. UnsortedSet
|
115
115
|
|
116
116
|
# Get all members from set
|
117
|
-
index_set.members.map { |id| indexed_class.
|
117
|
+
index_set.members.map { |id| indexed_class.find_by_identifier(id) }
|
118
118
|
end
|
119
119
|
|
120
120
|
# Generate method to rebuild the index for this parent instance
|
121
|
-
define_method("rebuild_#{index_name}") do
|
121
|
+
define_method(:"rebuild_#{index_name}") do
|
122
122
|
# This would need to be implemented based on how you track which
|
123
123
|
# objects belong to this parent instance
|
124
124
|
# For now, just a placeholder
|
@@ -138,7 +138,7 @@ module Familia
|
|
138
138
|
def generate_mutation_methods_self(indexed_class, field, target_class, index_name)
|
139
139
|
target_class_config = target_class.config_name
|
140
140
|
indexed_class.class_eval do
|
141
|
-
method_name = "add_to_#{target_class_config}_#{index_name}"
|
141
|
+
method_name = :"add_to_#{target_class_config}_#{index_name}"
|
142
142
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
143
143
|
|
144
144
|
define_method(method_name) do |target_instance|
|
@@ -154,7 +154,7 @@ module Familia
|
|
154
154
|
index_set.add(identifier)
|
155
155
|
end
|
156
156
|
|
157
|
-
method_name = "remove_from_#{target_class_config}_#{index_name}"
|
157
|
+
method_name = :"remove_from_#{target_class_config}_#{index_name}"
|
158
158
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
159
159
|
|
160
160
|
define_method(method_name) do |target_instance|
|
@@ -170,7 +170,7 @@ module Familia
|
|
170
170
|
index_set.remove(identifier)
|
171
171
|
end
|
172
172
|
|
173
|
-
method_name = "update_in_#{target_class_config}_#{index_name}"
|
173
|
+
method_name = :"update_in_#{target_class_config}_#{index_name}"
|
174
174
|
Familia.ld("[MultiIndexGenerators] #{name} method #{method_name}")
|
175
175
|
|
176
176
|
define_method(method_name) do |target_instance, old_field_value = nil|
|