typed_eav 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 392d8c372c1b70b4b710cef0b9f67d79531fb237bbca37419906ed468ced365f
4
- data.tar.gz: e8d9fd82f5d0d3a2cd894f89090a31522882c0606f28e691be373aacccf73cce
3
+ metadata.gz: 2584a7a3e9eab294e874931c8202f2dae2082be6a036bd419fce7df3fdd94f56
4
+ data.tar.gz: 7d24090e4969e89bb268f89510b46ce034d8f9f045054c0d6d80828c4cd3006c
5
5
  SHA512:
6
- metadata.gz: 0fa26b021b7d5223f1af10e016dad89c546102f8f807d00fa6d3ea8d07a95a3e91948fa67e09137b2d93bb4cba38bf0ed2a91887812a7777fef4821bd87db190
7
- data.tar.gz: 195ee963d09d43cb358ff2f39ee4f62bd131cb75fbb110bbe6a00e569c5b693fbd21313e27f899e492edc9ebb921a1021bd432aa754059df4f609d6fa50cd30a
6
+ metadata.gz: 4aeaa932e2dff4d5ab22ae95c66d98953b93163fc9580445f05a365ce1489a56cfdbaf998e47548d5fe468227341eec170409eafad5191449cb96201039900d0
7
+ data.tar.gz: 1b626f58647506b2d0317e382d0fd2d8ebf7daae1c11e84dc9d58b1929107af9c62854c19c39a6244e9957d1e20ccb7b3c924c7aee9a77aa19414453c356e534
data/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ 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.5.0] - 2026-06-01
9
+
10
+ Adds a human display label for fields, distinct from the immutable machine
11
+ slug `name` (issue #21). Fully additive — every change defaults to the
12
+ pre-0.5.0 behavior. Existing rows (with `label` NULL) render exactly as
13
+ before, and consumers that never set a label observe no difference.
14
+
15
+ ### Added
16
+
17
+ - `label` — a nullable, free-text column on `typed_eav_fields`
18
+ (`20260507000000_add_label_to_typed_eav_fields.rb`). No index, no
19
+ default, no backfill: `label` never participates in uniqueness, lookup,
20
+ partitioning, or ordering, so an index would be dead weight. Run
21
+ `rails typed_eav:install:migrations` (or copy the migration) and migrate
22
+ to pick it up. `name` stays the immutable machine key.
23
+
24
+ - `TypedEAV::Field::Base#display_name` — the canonical human-facing
25
+ display string. Returns the free-text `label` when present, otherwise
26
+ falls back to `name.humanize`. A blank (`""`) label falls back too (via
27
+ `presence`). This is the ONE accessor all rendering should use; existing
28
+ rows render unchanged.
29
+
30
+ - `SchemaPortability` round-trips the label. `export_schema` emits the
31
+ **raw** `"label"` so `import_schema` reproduces it verbatim and
32
+ divergence detection treats a differing label as a difference; legacy
33
+ payloads with no `"label"` key import as NULL with no version gate.
34
+ `export_snapshot_schema` emits the **resolved** `"display_name"` instead
35
+ (render-oriented snapshots hand the consumer a ready-to-render string —
36
+ intentionally asymmetric to the raw-label regular export).
37
+
38
+ ### Changed
39
+
40
+ - A label-only edit on a Field dispatches the `:update` event, not
41
+ `:rename`. `:rename` remains reserved for changes to the machine `name`.
42
+ Regression-pinned in `spec/regressions/issue_21_label_no_rename_spec.rb`.
43
+
44
+ ### References
45
+
46
+ - Issue #21 — Field display label / `display_name` contract.
47
+
8
48
  ## [0.4.0] - 2026-05-26
9
49
 
10
50
  Closes four follow-up gaps (PRD #15) surfaced when a downstream Rails app
data/README.md CHANGED
@@ -810,6 +810,7 @@ A few non-obvious contracts worth knowing about up front:
810
810
  - **Orphan-parent rows rejected**: a `Field` or `Section` row with `parent_scope` set but `scope` blank is invalid. The `Value`-side guard rejects cross-`(scope, parent_scope)` writes too.
811
811
  - **Event hooks fire from `after_commit`**: the `on_value_change` and `on_field_change` callbacks fire after the database write is durable; their exceptions never break a save. See §"Event hooks" for the full contract.
812
812
  - **Versioning is opt-in**: When enabled (`TypedEAV.config.versioning = true` on the gem; `versioned: true` per host), every `:create` / `:update` / `:destroy` event on a Value writes an append-only audit row in `typed_eav_value_versions`. See §"Versioning" for the full contract.
813
+ - **`label` is cosmetic, `name` is the machine key**: A field's optional `label` is free-text human display, independent of the slug `name`. Render via `display_name`, which returns `label` when present else `name.humanize`. `label` has no uniqueness or format constraints (only a 255-char max) and never affects ordering, lookup, partitioning, or rename detection — editing only `label` fires `on_field_change` with `:update`, never `:rename`. Existing rows (`label` NULL) render unchanged. Schema export round-trips the raw `label` (legacy payloads without a `label` key import as NULL); snapshot export carries the resolved `display_name`.
813
814
 
814
815
  ## Event hooks
815
816
 
@@ -45,6 +45,11 @@ module TypedEAV
45
45
 
46
46
  validates :name, presence: true, uniqueness: { scope: %i[entity_type scope parent_scope] }
47
47
  validates :name, exclusion: { in: RESERVED_NAMES, message: "is reserved" }
48
+ # `label` is optional free-text human display, independent of the machine
49
+ # slug `name` (issue #21). The ONLY guard is a max-length sanity bound —
50
+ # intentionally NO uniqueness and NO format/exclusion constraint. RESERVED_NAMES,
51
+ # slug-uniqueness, and rename detection all key on :name and never on :label.
52
+ validates :label, length: { maximum: 255 }, allow_nil: true
48
53
  validates :type, presence: true
49
54
  validates :entity_type, presence: true
50
55
  validate :validate_default_value
@@ -262,6 +267,16 @@ module TypedEAV
262
267
  self.class.name.demodulize.underscore
263
268
  end
264
269
 
270
+ # Canonical human-facing display string (issue #21). Returns the
271
+ # free-text `label` when present, otherwise falls back to humanizing the
272
+ # machine slug `name`. This is the ONE accessor all rendering should use;
273
+ # `name` stays the immutable machine key. A blank ("") label falls back
274
+ # too (via `presence`), so existing rows (label NULL) render exactly as
275
+ # they did before this column existed.
276
+ def display_name
277
+ label.presence || name.humanize
278
+ end
279
+
265
280
  def array_field?
266
281
  false
267
282
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddLabelToTypedEAVFields < ActiveRecord::Migration[7.1]
4
+ # Additive, nullable, free-text display label distinct from the machine
5
+ # slug `name` (issue #21). No index, no default, no backfill: `label` never
6
+ # participates in uniqueness, lookup, partitioning, or ordering, so an index
7
+ # would be dead weight. Reversible via `change` because `add_column`
8
+ # auto-inverts. Existing rows keep label NULL and render unchanged through
9
+ # the new `Field#display_name` (label.presence || name.humanize).
10
+ def change
11
+ add_column :typed_eav_fields, :label, :string, null: true
12
+ end
13
+ end
@@ -113,6 +113,12 @@ module TypedEAV
113
113
  "entity_type" => field.entity_type,
114
114
  "scope" => field.scope,
115
115
  "parent_scope" => field.parent_scope,
116
+ # Raw label (issue #21) — NOT the resolved display_name. The regular
117
+ # export round-trips the stored value verbatim so import reproduces
118
+ # it exactly and divergence detection (field_export_row_equal?) treats
119
+ # a differing label as a difference. Legacy payloads lack this key →
120
+ # entry["label"] is nil on import → label stays NULL (no version gate).
121
+ "label" => field.label,
116
122
  "required" => field.required,
117
123
  "sort_order" => field.sort_order,
118
124
  "field_dependent" => field.field_dependent,
@@ -145,6 +151,12 @@ module TypedEAV
145
151
  entry = {
146
152
  "name" => field.name,
147
153
  "field_type_name" => field.field_type_name,
154
+ # RESOLVED display_name (issue #21), NOT the raw label — snapshots are
155
+ # render-oriented (CONTEXT decision 3). This is intentionally
156
+ # asymmetric to the regular export's raw "label": a snapshot consumer
157
+ # gets the ready-to-render string (label when present, else
158
+ # name.humanize) without re-deriving it.
159
+ "display_name" => field.display_name,
148
160
  "required" => field.required,
149
161
  "sort_order" => field.sort_order,
150
162
  "options" => field.options,
@@ -247,6 +259,7 @@ module TypedEAV
247
259
 
248
260
  def overwrite_field!(existing, entry)
249
261
  existing.assign_attributes(
262
+ label: entry["label"],
250
263
  required: entry["required"],
251
264
  sort_order: entry["sort_order"],
252
265
  field_dependent: entry["field_dependent"],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedEAV
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_eav
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dchuk
@@ -86,6 +86,7 @@ files:
86
86
  - db/migrate/20260501000000_add_cascade_policy_to_typed_eav_fields.rb
87
87
  - db/migrate/20260505000000_create_typed_eav_value_versions.rb
88
88
  - db/migrate/20260506000001_add_version_group_id_to_typed_eav_value_versions.rb
89
+ - db/migrate/20260507000000_add_label_to_typed_eav_fields.rb
89
90
  - lib/generators/typed_eav/install/install_generator.rb
90
91
  - lib/generators/typed_eav/scaffold/scaffold_generator.rb
91
92
  - lib/generators/typed_eav/scaffold/templates/config/initializers/typed_eav.rb