typed_eav 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +122 -0
- data/lib/typed_eav/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66997bbaca41b0302fe7a86baf9b22233f2ade442c2255c3323a900245bd78fb
|
|
4
|
+
data.tar.gz: 90498aa57c720504bc09a34e62fab81d0377baaeab3ea2d0598317d6d5b38a93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a67ca56147f16ca0b6ffa02121f5783dc10f588ae36bc4d6b9df73c5de19bfac22be3b76613c693719156d15b8442723de4fd05479f72dc8074ad5b272c2599e
|
|
7
|
+
data.tar.gz: f4802412a11f7d5010487c2bede22037a6302653269eaf2dd4a933f98612296d38b2a6ce4bdd6244db1286c6572ac706615e9723ed73a2bc0ac582289af7ebd8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be 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
|
+
## [0.3.1] - 2026-05-25
|
|
9
|
+
|
|
10
|
+
Documentation-only release. No code or behavior changes.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- README §"Architecture" — full overview of the post-0.3.0 internal
|
|
15
|
+
module layout: macro entry (`HasTypedEav`), the two-altitude query
|
|
16
|
+
pattern (`EntityQuery` → `FilterQuery` → `QueryBuilder`), `BulkRead`
|
|
17
|
+
and `BulkWrite` siblings, `InstanceMethods`, `Field::TypedStorage`
|
|
18
|
+
concern, family intermediate bases (`ValidatedString`, `RangeBounded`,
|
|
19
|
+
`Optionable`), `ScopeTuple`, `Partition`, `EventDispatcher`, and the
|
|
20
|
+
Phase-6 modules (`SchemaPortability`, `CSVMapper`). Anchored to ADRs
|
|
21
|
+
0001–0005 throughout.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- `TEST_PLAN.md` — pre-0.3.0 test-sweep planning artifact (2026-04-08).
|
|
26
|
+
Described specs for modules deleted in #9. Git history preserves it.
|
|
27
|
+
- `typed_eav-enhancement-plan.md` — pre-0.3.0 phased roadmap. References
|
|
28
|
+
v0.1.0 line numbers and Phase-1 work that has since shipped. Git
|
|
29
|
+
history preserves it.
|
|
30
|
+
|
|
8
31
|
## [0.3.0] - 2026-05-25
|
|
9
32
|
|
|
10
33
|
Pre-1.0 architecture cleanup arc (issues #9–#13). No public-API breakage
|
|
@@ -205,6 +228,7 @@ worked examples.
|
|
|
205
228
|
|
|
206
229
|
Initial release.
|
|
207
230
|
|
|
231
|
+
[0.3.1]: https://github.com/dchuk/typed_eav/releases/tag/v0.3.1
|
|
208
232
|
[0.3.0]: https://github.com/dchuk/typed_eav/releases/tag/v0.3.0
|
|
209
233
|
[0.2.1]: https://github.com/dchuk/typed_eav/releases/tag/v0.2.1
|
|
210
234
|
[0.2.0]: https://github.com/dchuk/typed_eav/releases/tag/v0.2.0
|
data/README.md
CHANGED
|
@@ -1239,6 +1239,128 @@ The gem creates four tables:
|
|
|
1239
1239
|
- `typed_eav_options` - allowed values for select/multi-select fields
|
|
1240
1240
|
- `typed_eav_sections` - optional UI grouping
|
|
1241
1241
|
|
|
1242
|
+
## Architecture
|
|
1243
|
+
|
|
1244
|
+
Internal module layout as of 0.3.0. Most consumers never reach for these directly — the public surface is the `has_typed_eav` macro and the instance/class methods it installs — but the split matters if you're extending the gem, debugging an integration, or evaluating it for production. Decisions are anchored by [ADR-0001](docs/adr/0001-collapse-column-mapping-stack.md) through [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md).
|
|
1245
|
+
|
|
1246
|
+
### Macro entry: `HasTypedEav`
|
|
1247
|
+
|
|
1248
|
+
`lib/typed_eav/has_typed_eav.rb` (~120 LOC) is the macro shell. When you call `has_typed_eav` on an AR model, it:
|
|
1249
|
+
|
|
1250
|
+
1. `extend`s `TypedEAV::EntityQuery` onto the class (class-level query methods).
|
|
1251
|
+
2. `include`s `TypedEAV::HasTypedEav::InstanceMethods` (per-record accessors).
|
|
1252
|
+
3. Wires scope/parent-scope kwargs into the model's class-level configuration.
|
|
1253
|
+
4. Registers the model with `TypedEAV::Registry`.
|
|
1254
|
+
|
|
1255
|
+
The macro is intentionally thin. All real behavior lives in the modules it pulls in.
|
|
1256
|
+
|
|
1257
|
+
### Class-level reads: two-altitude query pattern
|
|
1258
|
+
|
|
1259
|
+
```
|
|
1260
|
+
Contact.where_typed_eav(...) ← public class method
|
|
1261
|
+
│
|
|
1262
|
+
▼
|
|
1263
|
+
TypedEAV::EntityQuery ← high altitude: orchestrator
|
|
1264
|
+
• resolves scope/parent_scope from ambient context or explicit kwargs
|
|
1265
|
+
• owns the UNSET_SCOPE / ALL_SCOPES sentinels
|
|
1266
|
+
• delegates to FilterQuery
|
|
1267
|
+
│
|
|
1268
|
+
▼
|
|
1269
|
+
TypedEAV::FilterQuery ← multi-filter composition
|
|
1270
|
+
• normalizes filter input shapes (positional, hash, hash-of-hashes)
|
|
1271
|
+
• looks up field definitions via TypedEAV::Partition
|
|
1272
|
+
• per filter, asks QueryBuilder for the SQL fragment
|
|
1273
|
+
• unions/intersects per-field entity-id sets
|
|
1274
|
+
• returns an ActiveRecord::Relation scoped to the host model
|
|
1275
|
+
│
|
|
1276
|
+
▼
|
|
1277
|
+
TypedEAV::QueryBuilder ← low altitude: per-field SQL primitive
|
|
1278
|
+
• turns a single (field, op, value) into a WHERE clause against typed_eav_values
|
|
1279
|
+
• knows about typed-column projections (integer_value, string_value, etc.)
|
|
1280
|
+
• knows about operator-specific column choice (currency-cents vs currency-code)
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
`QueryBuilder` is the single place that decides "given this field and this operator, which column and which SQL fragment?" `FilterQuery` never builds SQL fragments directly; `EntityQuery` never touches columns. Splitting the two altitudes keeps custom field types extending only the column-mapping surface (`value_column`, `operators`, `operator_column`) without ever subclassing `FilterQuery`.
|
|
1284
|
+
|
|
1285
|
+
### Bulk reads: `BulkRead`
|
|
1286
|
+
|
|
1287
|
+
`typed_eav_hash_for(records)` (the plural read) routes through `TypedEAV::BulkRead`. Given a record collection and an effective `(scope, parent_scope)`, it:
|
|
1288
|
+
|
|
1289
|
+
1. Groups records by partition tuple via `TypedEAV::Partition.definitions_by_name`.
|
|
1290
|
+
2. Issues one batched `WHERE entity_id IN (...) AND field_id IN (...)` query per partition.
|
|
1291
|
+
3. Returns a `{record_id => {field_name => value}}` map.
|
|
1292
|
+
|
|
1293
|
+
Single-record reads (`typed_eav_value`, `typed_eav_hash`) live on `InstanceMethods` and use the same partition helpers but without batching.
|
|
1294
|
+
|
|
1295
|
+
### Bulk writes: `BulkWrite`
|
|
1296
|
+
|
|
1297
|
+
`bulk_set_typed_eav_values(records, attrs)` routes through `TypedEAV::BulkWrite`, an executor that:
|
|
1298
|
+
|
|
1299
|
+
1. Memoizes field definitions for the call via `Thread.current[:typed_eav_bulk_defs_memo]`.
|
|
1300
|
+
2. Validates each attribute against its field type's cast contract.
|
|
1301
|
+
3. Upserts in a single SQL round trip per typed column.
|
|
1302
|
+
|
|
1303
|
+
`BulkWrite` and `BulkRead` are siblings — one read path, one write path — but they don't share a base class. Per [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md), keeping them independent preserves the option to evolve each on its own schedule.
|
|
1304
|
+
|
|
1305
|
+
### Per-record reads/writes: `InstanceMethods`
|
|
1306
|
+
|
|
1307
|
+
`lib/typed_eav/has_typed_eav/instance_methods.rb` (~250 LOC) holds the per-record API:
|
|
1308
|
+
|
|
1309
|
+
- `typed_eav_value(name)` / `typed_eav_hash` — reads
|
|
1310
|
+
- `set_typed_eav_value(name, value)` / `typed_eav_attributes=` — writes
|
|
1311
|
+
- `typed_eav_changes` — dirty tracking
|
|
1312
|
+
- `typed_eav_scope` / `typed_eav_parent_scope` — scope resolution per record
|
|
1313
|
+
|
|
1314
|
+
Every method uses `TypedEAV::Partition.definitions_by_name` so the collision-precedence rules for ambient/explicit/parent scopes are computed in one place.
|
|
1315
|
+
|
|
1316
|
+
### Field types and storage: `Field::TypedStorage`
|
|
1317
|
+
|
|
1318
|
+
`TypedEAV::Field::Base` is the STI parent of every field type. The shared storage surface lives in the `TypedEAV::Field::TypedStorage` concern (`lib/typed_eav/field/typed_storage.rb`, ~200 LOC), auto-included on `Field::Base`. Per [ADR-0001](docs/adr/0001-collapse-column-mapping-stack.md), it provides:
|
|
1319
|
+
|
|
1320
|
+
- **Class DSL**: `value_column`, `value_columns`, `operators`, `operator_column`, `supported_operators` — describe where typed values live and which operators they support.
|
|
1321
|
+
- **Instance override points**: `read_value(record)`, `write_value(record, casted)`, `apply_default(record)` — the three methods a multi-cell field type overrides.
|
|
1322
|
+
- **Concrete snapshot helpers**: `value_changed?`, `before_snapshot`, `after_snapshot` — derived automatically from `value_columns`; not overridable.
|
|
1323
|
+
|
|
1324
|
+
Custom multi-cell field types subclass `Field::Base` directly and override only the three instance methods. See §[Multi-cell field types](#multi-cell-field-types) for `Currency` as the canonical worked example.
|
|
1325
|
+
|
|
1326
|
+
### Field families: intermediate STI bases
|
|
1327
|
+
|
|
1328
|
+
Per [ADR-0004](docs/adr/0004-field-family-intermediate-bases.md), three intermediate STI parents factor shared validation behavior out of `Field::Base`:
|
|
1329
|
+
|
|
1330
|
+
- **`TypedEAV::Field::ValidatedString`** — parent of `Text`, `Email`, `Url`. Owns string-length and pattern-validation helpers including `max_gte_min_length` (which now covers Email/Url, not just Text).
|
|
1331
|
+
- **`TypedEAV::Field::RangeBounded`** — parent of `Integer`, `Decimal`, `Date`, `DateTime` (and `Percentage < Decimal`). Owns range-validation helpers including `validates :max, comparison: { greater_than_or_equal_to: :min }` (which now covers Date/DateTime, not just Integer/Decimal).
|
|
1332
|
+
- **`TypedEAV::Field::Optionable`** — a Rails concern included by `Select` and `MultiSelect`. Owns the public-facing sorted `allowed_values` reader and the option-inclusion validators.
|
|
1333
|
+
|
|
1334
|
+
`Color`, `Boolean`, `Json`, and the array field types (`TextArray`, `IntegerArray`, `DecimalArray`, `DateArray`) remain direct children of `Field::Base`. See §[Family intermediate bases](#family-intermediate-bases-extension-points) for extension examples.
|
|
1335
|
+
|
|
1336
|
+
### Scope tuple normalization: `ScopeTuple`
|
|
1337
|
+
|
|
1338
|
+
`TypedEAV::ScopeTuple` (`lib/typed_eav/scope_tuple.rb`, ~120 LOC) is the canonical source of truth for the `(scope, parent_scope)` partition tuple. It provides:
|
|
1339
|
+
|
|
1340
|
+
- `normalize_permissive(scope)` — coerces input to a tuple; tolerates bare scalars (used by `with_scope`, `normalize_scope`, `Field#validate_parent_scope_invariant`).
|
|
1341
|
+
- `normalize_strict(scope)` — same shape, but raises on bare-scalar input (used by `current_scope`; preserves Phase-1's asymmetric contract that `Config.scope_resolver` must return a tuple).
|
|
1342
|
+
- `invariant_satisfied?(scope, parent_scope)` — Boolean check for the orphan-parent invariant (`parent_scope` set without `scope` = invalid).
|
|
1343
|
+
|
|
1344
|
+
Each calling site keeps its own response policy (raise / AR error / silent narrow) using the Boolean return — `ScopeTuple` is a predicate, not an enforcer.
|
|
1345
|
+
|
|
1346
|
+
### Partition tuple helpers: `Partition`
|
|
1347
|
+
|
|
1348
|
+
`TypedEAV::Partition` (`lib/typed_eav/partition.rb`, ~100 LOC) owns the `(entity_type, scope, parent_scope)` precedence rules:
|
|
1349
|
+
|
|
1350
|
+
- `definitions_by_name(model, scope, parent_scope)` — returns the field-definitions map for a single resolved partition.
|
|
1351
|
+
- `definitions_multimap_by_name(model)` — returns the cross-partition multimap used by `unscoped { }` blocks.
|
|
1352
|
+
- `visible_fields(model, scope, parent_scope)` / `visible_sections(...)` — scope-respecting field/section iteration with the orphan-parent invariant inlined via `ScopeTuple.invariant_satisfied?`.
|
|
1353
|
+
|
|
1354
|
+
The definitions helpers used to live as class methods on `HasTypedEav` before 0.3.0. They moved to `Partition` per [ADR-0002](docs/adr/0002-entity-query-orchestration.md) because they describe the partition domain, not the macro.
|
|
1355
|
+
|
|
1356
|
+
### Events: `EventDispatcher`
|
|
1357
|
+
|
|
1358
|
+
`TypedEAV::EventDispatcher` (`lib/typed_eav/event_dispatcher.rb`, ~150 LOC) is the broker for `on_value_change` and `on_field_change` callbacks. Per [ADR-0003](docs/adr/0003-keep-event-dispatcher-broker.md), it intentionally stays a broker rather than getting absorbed into either `Value` or `Field` — its multi-publisher / multi-subscriber shape doesn't belong on either model. See §[Event hooks](#event-hooks) for the public callback contract.
|
|
1359
|
+
|
|
1360
|
+
### Schema portability and CSV: independent modules
|
|
1361
|
+
|
|
1362
|
+
`TypedEAV::SchemaPortability` and `TypedEAV::CSVMapper` (Phase-6 modules) are deliberately decoupled from the core read/write path per [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md). They depend on the public `has_typed_eav` macro surface, never on internal modules.
|
|
1363
|
+
|
|
1242
1364
|
## License
|
|
1243
1365
|
|
|
1244
1366
|
MIT
|
data/lib/typed_eav/version.rb
CHANGED