ecs_on_rails 0.2.1 → 0.3.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/.yardopts +12 -0
- data/CHANGELOG.md +332 -0
- data/README.md +365 -50
- data/lib/ecs_rails/catalogue/address.rb +39 -0
- data/lib/ecs_rails/catalogue/calendar_date.rb +27 -0
- data/lib/ecs_rails/catalogue/counter.rb +46 -0
- data/lib/ecs_rails/catalogue/discard.rb +36 -0
- data/lib/ecs_rails/catalogue/email.rb +35 -0
- data/lib/ecs_rails/catalogue/geolocation.rb +53 -0
- data/lib/ecs_rails/catalogue/identifier.rb +23 -0
- data/lib/ecs_rails/catalogue/image.rb +28 -0
- data/lib/ecs_rails/catalogue/link.rb +27 -0
- data/lib/ecs_rails/catalogue/marker.rb +38 -0
- data/lib/ecs_rails/catalogue/money.rb +86 -0
- data/lib/ecs_rails/catalogue/name.rb +37 -0
- data/lib/ecs_rails/catalogue/password.rb +50 -0
- data/lib/ecs_rails/catalogue/period.rb +57 -0
- data/lib/ecs_rails/catalogue/phone.rb +30 -0
- data/lib/ecs_rails/catalogue/position.rb +18 -0
- data/lib/ecs_rails/catalogue/rating.rb +25 -0
- data/lib/ecs_rails/catalogue/relationship.rb +159 -0
- data/lib/ecs_rails/catalogue/role.rb +23 -0
- data/lib/ecs_rails/catalogue/schema_diff.rb +198 -0
- data/lib/ecs_rails/catalogue/search_vector.rb +38 -0
- data/lib/ecs_rails/catalogue/state.rb +71 -0
- data/lib/ecs_rails/catalogue/tags.rb +64 -0
- data/lib/ecs_rails/catalogue/text.rb +29 -0
- data/lib/ecs_rails/catalogue/timestamp.rb +34 -0
- data/lib/ecs_rails/catalogue/token.rb +77 -0
- data/lib/ecs_rails/catalogue.rb +405 -0
- data/lib/ecs_rails/component.rb +13 -7
- data/lib/ecs_rails/config.rb +45 -10
- data/lib/ecs_rails/dsl.rb +503 -88
- data/lib/ecs_rails/entity.rb +21 -5
- data/lib/ecs_rails/errors.rb +3 -0
- data/lib/ecs_rails/inverses.rb +398 -0
- data/lib/ecs_rails/lazy.rb +86 -23
- data/lib/ecs_rails/markers.rb +197 -0
- data/lib/ecs_rails/preloading.rb +33 -8
- data/lib/ecs_rails/presence.rb +124 -20
- data/lib/ecs_rails/querying.rb +195 -17
- data/lib/ecs_rails/railtie.rb +10 -0
- data/lib/ecs_rails/registry.rb +113 -18
- data/lib/ecs_rails/relationships.rb +241 -159
- data/lib/ecs_rails/slots.rb +183 -0
- data/lib/ecs_rails/validations.rb +3 -5
- data/lib/ecs_rails/version.rb +4 -1
- data/lib/ecs_rails.rb +51 -6
- data/lib/generators/ecs_rails/catalogue_options.rb +66 -0
- data/lib/generators/ecs_rails/component/component_generator.rb +5 -0
- data/lib/generators/ecs_rails/component/templates/migration.rb.tt +9 -5
- data/lib/generators/ecs_rails/component/templates/model.rb.tt +2 -1
- data/lib/generators/ecs_rails/entity/entity_generator.rb +93 -0
- data/lib/generators/ecs_rails/entity/templates/entity.rb.tt +9 -0
- data/lib/generators/ecs_rails/install/install_generator.rb +35 -8
- data/lib/generators/ecs_rails/install/templates/application_component.rb.tt +3 -2
- data/lib/generators/ecs_rails/install/templates/application_entity.rb.tt +2 -2
- data/lib/generators/ecs_rails/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ecs_rails/install/templates/install_migration.rb.tt +38 -0
- data/lib/generators/ecs_rails/templates/catalogue_class.rb.tt +10 -0
- data/lib/generators/ecs_rails/upgrade/templates/catalogue_migration.rb.tt +18 -0
- data/lib/generators/ecs_rails/upgrade/templates/markers_migration.rb.tt +31 -0
- data/lib/generators/ecs_rails/upgrade/templates/relationships_migration.rb.tt +33 -0
- data/lib/generators/ecs_rails/upgrade/templates/slots_migration.rb.tt +23 -0
- data/lib/generators/ecs_rails/upgrade/upgrade_generator.rb +281 -0
- metadata +65 -6
- data/lib/generators/ecs_rails/install/templates/migration.rb.tt +0 -20
- data/lib/generators/ecs_rails/relationship/relationship_generator.rb +0 -94
- data/lib/generators/ecs_rails/relationship/templates/migration.rb.tt +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5ee13ec49dd2b98779e03c1fe5f29e4e9dd26c2fc7efdcc4be69dbb548fc65d
|
|
4
|
+
data.tar.gz: 49a77b4f33c89abd4ffbc1a21f6e4554ebc8540f74f6f562ca94ff73e6ec3b5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a48ce6bd28e4db48233251a28e5d7991c14a830a04c51e83f321762a544ab74fef9fb321722bac48600a0acd028f67de23343c91104d154bb4f8aa0cf0447451
|
|
7
|
+
data.tar.gz: a8eaa377b244e0a694251aef678ebe6114a67062052824da9d35694095d3a3e340654871f2fb0a025b85643bf809e59bef1bf31156fd1fa2cd2a168b25646198
|
data/.yardopts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
--markup markdown
|
|
2
|
+
--markup-provider kramdown
|
|
3
|
+
--readme README.md
|
|
4
|
+
--title "ECS Rails - an Entity-Component-System reimagining of ActiveRecord"
|
|
5
|
+
--charset utf-8
|
|
6
|
+
--no-private
|
|
7
|
+
--hide-void-return
|
|
8
|
+
--embed-mixins
|
|
9
|
+
lib/**/*.rb
|
|
10
|
+
-
|
|
11
|
+
CHANGELOG.md
|
|
12
|
+
LICENSE.txt
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,338 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] — 2026-09-27
|
|
10
|
+
|
|
11
|
+
**The zero-migrations release.** After `rails g ecs_rails:install` and one
|
|
12
|
+
`db:migrate`, entities, labelled slots, relationships, markers and systems are
|
|
13
|
+
plain Ruby: the catalogue ships the components, one shared table holds every
|
|
14
|
+
relationship, another every marker. Breaking, pre-1.0.
|
|
15
|
+
|
|
16
|
+
### Upgrading from 0.2
|
|
17
|
+
|
|
18
|
+
Back up the database and try this on a copy first. The gem README's
|
|
19
|
+
"Upgrading an existing installation" has the full steps.
|
|
20
|
+
|
|
21
|
+
1. Update the Gemfile to `gem "ecs_on_rails", "~> 0.3.0"` and run
|
|
22
|
+
`bundle update ecs_on_rails`.
|
|
23
|
+
2. In development, run `bin/rails generate ecs_rails:upgrade` (add
|
|
24
|
+
`--sets core commerce` for `Money`), review the migrations, then
|
|
25
|
+
`bin/rails db:migrate`. It adds `slot` to every component table and moves
|
|
26
|
+
the unique index to `(entity_id, slot)`, creates `relationships`, `markers`
|
|
27
|
+
and the catalogue tables, and moves rows from per-relationship and marker
|
|
28
|
+
tables into the shared ones, keeping their IDs. Running it again generates
|
|
29
|
+
nothing.
|
|
30
|
+
3. **Delegated names are prefixed with the reader**: `user.address` becomes
|
|
31
|
+
`user.email_address`. Add `prefix: false` to a declaration to keep bare names.
|
|
32
|
+
4. **Markers are declared by name**: `component Moderator` becomes
|
|
33
|
+
`marker :moderator`, and `add(Moderator)` becomes `add(:moderator)`. Delete
|
|
34
|
+
the old marker class after migrating.
|
|
35
|
+
5. **`rails g ecs_rails:relationship` is gone**: `relates_to` needs no
|
|
36
|
+
migration. Nested preloads name `target`:
|
|
37
|
+
`preload(author_relationship: { target: :name })`.
|
|
38
|
+
6. Run `bin/rails zeitwerk:check` and your tests.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Demo journey, identity and accessibility** (ECS-35): the demo opens on a
|
|
43
|
+
four-step tour quoting each step's declaration; a session-backed "acting
|
|
44
|
+
as" bar replaces per-form user pickers; a scripted rendered and axe-core
|
|
45
|
+
review (`demo/script/qa`) brought 21 pages and 7 states to zero WCAG 2.1 AA
|
|
46
|
+
violations in light and dark schemes and fixed phone layouts. See
|
|
47
|
+
[the review](../docs/design/demo-qa.md). No gem change.
|
|
48
|
+
|
|
49
|
+
- **Geocoder map in the demo** (ECS-44): `/geocoder` draws located
|
|
50
|
+
Geolocations on a server-rendered SVG world map (Natural Earth 1:110m, public
|
|
51
|
+
domain; no JavaScript or tiles), with non-overlapping labels, merged markers
|
|
52
|
+
for nearby points, a text list of places, and filter links by entity type that
|
|
53
|
+
narrow map and table with `Geolocation.where(entity: User.all)`. No gem change.
|
|
54
|
+
|
|
55
|
+
- **Geocoder system in the demo** (ECS-8): `Demo::Geocoder` pairs every
|
|
56
|
+
`Address` with a `Geolocation` in the same slot across users (shipping and
|
|
57
|
+
billing) and companies without naming either, skips owners that declare no
|
|
58
|
+
pair (order/invoice snapshots), redoes only new or edited addresses, records
|
|
59
|
+
unknown or blank places without coordinates, and resolves overlapping runs on
|
|
60
|
+
the `(entity_id, slot)` unique index. Lookups are simulated. The demo's
|
|
61
|
+
`/geocoder` page shows each address's state, runs it and lists the one
|
|
62
|
+
migration. `Geolocation#locate` now takes `at:` (default `Time.current`) so a
|
|
63
|
+
system can stamp the address version the coordinates describe, and documents
|
|
64
|
+
`locate(nil, nil)` as a lookup that found nothing.
|
|
65
|
+
|
|
66
|
+
- **Entity generator** (ECS-11): `rails generate ecs_rails:entity Person name
|
|
67
|
+
email mobile:phone work:phone home:address` writes a plain entity class from
|
|
68
|
+
existing components, including labelled slots and namespaces. Validates names,
|
|
69
|
+
concrete references and duplicates before writing; respects configured paths
|
|
70
|
+
and normal file collision handling. Emits no migration. Quickstart/package
|
|
71
|
+
checks now exercise generated classes and independent persisted slots.
|
|
72
|
+
|
|
73
|
+
- **Executable onboarding** (ECS-34): the gem README starts with a contacts
|
|
74
|
+
tutorial covering composition, querying, a cross-entity system and rendering.
|
|
75
|
+
Package CI executes its actual snippets, upgrade commands and bespoke storage
|
|
76
|
+
example. The demo guide documents setup, resets, tests and a guided journey;
|
|
77
|
+
lifecycle guidance covers virtual reads, replacements, targets and deletion.
|
|
78
|
+
|
|
79
|
+
- **Source walkthrough and readability review** (ECS-36): follow installation,
|
|
80
|
+
composition, checkout and entity-independent indexing through the gem and demo.
|
|
81
|
+
Simplified checkout orchestration and Period branching; corrected catalogue
|
|
82
|
+
examples, generated comments and lifecycle/query/token documentation to match
|
|
83
|
+
existing behavior. No API, schema or dependency changes.
|
|
84
|
+
|
|
85
|
+
- **Reproducible performance evidence** (ECS-33): the companion demo includes
|
|
86
|
+
equivalent ECS/conventional Rails catalogue, detail, checkout, indexing and
|
|
87
|
+
slot-preload workloads, behavioral verification and a CI smoke run. Published
|
|
88
|
+
local measurements include latency distributions, allocations, SQL, storage
|
|
89
|
+
and query plans at 100/1,000/5,000 products. See
|
|
90
|
+
[the comparison](../docs/design/performance-comparison.md) for costs and
|
|
91
|
+
limitations; no gem API or production schema change.
|
|
92
|
+
|
|
93
|
+
- **Component queries beyond equality, and ordering** (RFC-0018).
|
|
94
|
+
`with_component` takes a block run on the component's relation
|
|
95
|
+
(`{ where("amount_cents < ?", 5000) }`, `{ tagged("ruby") }`,
|
|
96
|
+
`{ matching(q) }`) and `where`-style positional conditions
|
|
97
|
+
(`with_component(Rating, "stars >= ?", 4)`), all inside the same correlated
|
|
98
|
+
`EXISTS`. New `order_by_component(Money, :amount_cents, :desc, prefix:
|
|
99
|
+
:price)` orders by a component column through a correlated scalar subquery —
|
|
100
|
+
no join, no duplicate rows — with the column defaulting to the component's
|
|
101
|
+
primary attribute and entities lacking the row sorting last.
|
|
102
|
+
|
|
103
|
+
- **Inverse relationships** (RFC-0015): `has_many :comments, via: :post` and
|
|
104
|
+
`has_one :invoice, via: :order` on the parent expand to native `has_many
|
|
105
|
+
:through` over the shared `relationships` table — a real `CollectionProxy` of
|
|
106
|
+
the child class, `<<`, `create!`, scopes, the component query DSL chained on,
|
|
107
|
+
`includes(:comments)`. The child is inferred from the reader, or named
|
|
108
|
+
(`"Comment"`, or the class), and the pair is validated on first use and at
|
|
109
|
+
boot under `eager_load` — never at class-load time, which autoloading makes
|
|
110
|
+
unsafe. `has_one` requires the child's `unique: true`. `dependent: :destroy`
|
|
111
|
+
/ `:delete_all` removes the link rows with the parent. `entity.referrers`
|
|
112
|
+
lists every relationship row pointing at an entity. Without `via:` both
|
|
113
|
+
macros are ActiveRecord's own.
|
|
114
|
+
|
|
115
|
+
- **Primary attributes** (RFC-0014 amendment). A component declares
|
|
116
|
+
`primary :value`; a labelled slot of it then also delegates the bare slot
|
|
117
|
+
name, so `component Text, prefix: :title` gives `post.title` / `post.title=`
|
|
118
|
+
(→ `title_text.value`) beside `post.title_text` and `post.title_text_value`.
|
|
119
|
+
The catalogue declares them on `Text`, `Identifier`, `Counter`, `Timestamp`,
|
|
120
|
+
`CalendarDate`, `Rating`, `Position` and `Role`. Decided by the forum rebuild.
|
|
121
|
+
|
|
122
|
+
- **The catalogue** (ADR-0018, RFC-0017): 25 standard components shipped as
|
|
123
|
+
concerns under `EcsRails::Catalogue` — `Name`, `Email`, `Password`, `Phone`,
|
|
124
|
+
`Address`, `Geolocation`, `Link`, `Text`, `Identifier`, `Counter`, `Rating`,
|
|
125
|
+
`Timestamp`, `CalendarDate`, `Period`, `Position`, `State`, `Tags`,
|
|
126
|
+
`SearchVector`, `Discard`, `Image`, `Role`, `Token`, `Marker`, `Relationship`
|
|
127
|
+
(`core`) and `Money` (`commerce`). Each carries its behaviour, validations and
|
|
128
|
+
a schema declaration; `rails g ecs_rails:install` writes a one-line class per
|
|
129
|
+
component and ONE migration creating every table in the chosen `--sets`, so
|
|
130
|
+
an application builds from the catalogue with no further migration.
|
|
131
|
+
`--rename money:Price` is the name-collision remedy. `rails g
|
|
132
|
+
ecs_rails:upgrade` diffs the declarations against the database and writes
|
|
133
|
+
what is missing.
|
|
134
|
+
|
|
135
|
+
- **Markers on one shared table** (ADR-0018 §4, RFC-0016). `marker :moderator`
|
|
136
|
+
declares a marker as slot "moderator" of the app's `Marker` component
|
|
137
|
+
(`EcsRails::Catalogue::Marker`, one-line class written by install), with the
|
|
138
|
+
bare `user.moderator?` predicate, a Boolean `user.moderator = true` writer,
|
|
139
|
+
`add(:moderator)` / `has?` / `remove` by name, and `User.with_marker(:moderator)`
|
|
140
|
+
/ `without_marker`. No table per marker; a zero-attribute component on its own
|
|
141
|
+
table still works. `EcsRails.config.marker_class_name` names the class.
|
|
142
|
+
`ecs_rails:upgrade` gains a third job that creates `markers` and moves every
|
|
143
|
+
empty per-marker table into it.
|
|
144
|
+
|
|
145
|
+
- **One shared `relationships` table** (ADR-0017). Every `relates_to` is now a
|
|
146
|
+
row in a table created at install, under a slot named for the relationship;
|
|
147
|
+
declaring a relationship needs no migration. The public API is unchanged
|
|
148
|
+
(`relates_to`, `post.author`, `post.author=`, `with_related`,
|
|
149
|
+
`without_related`, `includes_related`), plus `post.author_id` /
|
|
150
|
+
`post.author_id=`.
|
|
151
|
+
- `relates_to :order, Order, unique: true` — at most one owner of this type per
|
|
152
|
+
target, enforced by a partial unique index created once at install.
|
|
153
|
+
- `EcsRails::Catalogue::Relationship`, the first catalogue concern (ADR-0018);
|
|
154
|
+
`rails g ecs_rails:install` writes the one-line `Relationship` class that
|
|
155
|
+
includes it. `EcsRails.config.relationship_class_name` names it (default
|
|
156
|
+
`"Relationship"`).
|
|
157
|
+
- The target type is checked on assignment: `post.author = team` raises
|
|
158
|
+
`EcsRails::InvalidRelationship`.
|
|
159
|
+
- `rails g ecs_rails:upgrade` gains a second job: create `relationships` and
|
|
160
|
+
move every pre-0.3 per-relationship table into it (recognised by shape and
|
|
161
|
+
name), then drop them. Irreversible; review the generated file.
|
|
162
|
+
|
|
163
|
+
- **Labelled (plural) components — slots** (RFC-0014 / ADR-0015). A component
|
|
164
|
+
may be declared more than once on an entity under distinct labels, each a
|
|
165
|
+
singleton with its own reader: `component Address` and `component Address,
|
|
166
|
+
prefix: :business` give `user.address` and `user.business_address`, backed by
|
|
167
|
+
one `addresses` table and told apart by a new `slot` column. Everything
|
|
168
|
+
singular works per slot with no new API — the lazy reader (a virtual is
|
|
169
|
+
built with its slot preset), delegation (`business_address_line1`), presence
|
|
170
|
+
(`add(Address, prefix: :business)`, `user.business_address?`), validation
|
|
171
|
+
keys (`errors[:"business_address.postcode"]`), querying
|
|
172
|
+
(`with_component(Address, prefix: :business, region: "WA")`) and preloading
|
|
173
|
+
(`includes_components(Address)` batches every slot).
|
|
174
|
+
- `component Foo, delegate: false` — reader and predicate only, no delegated
|
|
175
|
+
methods, for when even prefixed names get long.
|
|
176
|
+
- **Per-slot options.** A component declares what it accepts with
|
|
177
|
+
`slot_option :states, default: []`; the declaration site passes them as extra
|
|
178
|
+
keywords (`component State, prefix: :order, states: %w[pending paid]`); the
|
|
179
|
+
instance reads them back (`order_state.states`, `slot_options`). Unknown
|
|
180
|
+
options raise at declaration time.
|
|
181
|
+
- `Entity.declaration_for(Component, prefix:)`; `Registry::Declaration#slot`,
|
|
182
|
+
`#reader_name`, `#prefix`, `#slot_options`.
|
|
183
|
+
- **`rails g ecs_rails:upgrade`** — one migration that adds the `slot` column
|
|
184
|
+
and the `(entity_id, slot)` unique index to every existing component table
|
|
185
|
+
that lacks them, found by inspecting the database. Safe on shipped data.
|
|
186
|
+
This is the only migration a 0.2.x app needs to run.
|
|
187
|
+
|
|
188
|
+
- `component Foo, prefix: false` — bare delegation for one declaration, where
|
|
189
|
+
the prefix would be redundant (`post.state`, not `post.publish_state_state`).
|
|
190
|
+
`prefix: true` is the explicit default. A Symbol (RFC-0014's slot label)
|
|
191
|
+
raises `ArgumentError` until labelled slots are implemented.
|
|
192
|
+
- **Flat mass assignment.** Because the prefixed writers exist,
|
|
193
|
+
`User.create!(name_first: "Ada", email_address: "a@b.com")` routes each key
|
|
194
|
+
to its component, dirties it and persists it through the save cascade;
|
|
195
|
+
`update!` too. Rails multiparameter form fields (`date_select` →
|
|
196
|
+
`group_founded_on(1i)`…) route the same way. Unknown keys still raise
|
|
197
|
+
`ActiveModel::UnknownAttributeError`.
|
|
198
|
+
|
|
199
|
+
### Fixed
|
|
200
|
+
|
|
201
|
+
- **Bounded demo indexing** (ECS-32): the component indexer batches 100 owners,
|
|
202
|
+
reads every eligible owner's text slots together, and batches owner discovery
|
|
203
|
+
and existing vector reads. Preserves slot ordering, declaration eligibility
|
|
204
|
+
and vector identity without loading the full Text table.
|
|
205
|
+
|
|
206
|
+
- **Bounded demo lists** (ECS-31): market, bulletin, people, sellers, groups,
|
|
207
|
+
order history and growing detail lists use 24-row Kaminari pages. Stable
|
|
208
|
+
UUID tie-breakers prevent ambiguous ordering; navigation retains filters,
|
|
209
|
+
normalizes invalid pages and preserves total counts. Component and
|
|
210
|
+
relationship preloads apply only to the selected page.
|
|
211
|
+
|
|
212
|
+
- The Rails-compatible JSON `< 3` constraint is now a runtime dependency,
|
|
213
|
+
protecting installed gem consumers as well as development bundles (ECS-30).
|
|
214
|
+
CI covers each Rails 7.1/7.2/8.0/8.1 series, Ruby 3.2 through 4.0 representative
|
|
215
|
+
combinations, the demo suite, documentation and real packaged install/upgrade
|
|
216
|
+
flows against isolated PostgreSQL databases.
|
|
217
|
+
|
|
218
|
+
- Catalogue upgrades verify live column definitions, UUID primary keys,
|
|
219
|
+
unique/partial indexes and validated foreign keys with deletion behavior
|
|
220
|
+
before writing files (ECS-29). Missing compatible structures generate additive
|
|
221
|
+
migrations; incompatible definitions report actual/expected properties and
|
|
222
|
+
require explicit repair. Known catalogue tables with missing attributes are
|
|
223
|
+
no longer mistaken for legacy marker tables. No data conversion is inferred.
|
|
224
|
+
|
|
225
|
+
- Demo checkout and basket edits now coordinate on the basket; stock updates
|
|
226
|
+
lock the actual Counter rows in product order (ECS-28). Submitted revisions
|
|
227
|
+
identify completed orders so successful retries return the original result,
|
|
228
|
+
stale forms are rejected, and declines can retry without partial changes.
|
|
229
|
+
Order/invoice allocation uses transaction advisory locks and numeric suffixes.
|
|
230
|
+
No new demo migration is required. Demo specs refuse non-test environments.
|
|
231
|
+
|
|
232
|
+
- Demo price filters and ordering now treat an absent Money row as the displayed
|
|
233
|
+
zero price, matching persisted free products (ECS-27). The product-specific
|
|
234
|
+
queries use a slot-scoped left join and `COALESCE`, with stable price ties;
|
|
235
|
+
the gem's persisted component-presence semantics are unchanged.
|
|
236
|
+
|
|
237
|
+
- Component assignment now replaces a singleton atomically and updates both
|
|
238
|
+
the lazy memo and association cache (ECS-26), including labelled slots.
|
|
239
|
+
Invalid replacements preserve the old row; new owners defer persistence,
|
|
240
|
+
default-only replacements remain virtual, and nil removes the row. Cross-owner
|
|
241
|
+
and persisted cross-slot moves are rejected. Component reload/reset helpers
|
|
242
|
+
clear both caches; generated build/create helpers now raise an intentional
|
|
243
|
+
error directing callers to reader-based attribute updates.
|
|
244
|
+
|
|
245
|
+
- Explicitly assigned new relationship targets now save with their owner in
|
|
246
|
+
one transaction instead of being silently dropped (ECS-25). The lazy
|
|
247
|
+
cascade recognizes pending targets, and Rails autosave validates and saves
|
|
248
|
+
them and their touched components. Invalid targets fail through `save` /
|
|
249
|
+
`save!`; simply reading an unset relationship still creates no row.
|
|
250
|
+
|
|
251
|
+
- Relationship target IDs now validate against the declared target class,
|
|
252
|
+
including flat mass assignment and direct Relationship saves (ECS-24).
|
|
253
|
+
Wrong-type and nonexistent IDs return validation errors through `save` /
|
|
254
|
+
`save!`; subclasses and nil targets remain valid, and validation reuses
|
|
255
|
+
loaded targets. Wrong-type object assignments still raise immediately.
|
|
256
|
+
|
|
257
|
+
- `EcsRails::Catalogue::Password` no longer raises at class-load time when
|
|
258
|
+
bcrypt is absent — which, under `eager_load`, stopped an application that
|
|
259
|
+
installed `core` from booting. `has_secure_password` is set up when bcrypt is
|
|
260
|
+
available; otherwise `password=` and `authenticate` raise a LoadError saying
|
|
261
|
+
what to add. Surfaced by eager-loading the forum.
|
|
262
|
+
- The catalogue's format validators (`Email#address`, `Phone#e164` and
|
|
263
|
+
`#extension`, `Link#url`, `Address#country`, `Image#url`) use `allow_blank`,
|
|
264
|
+
not `allow_nil`: a blank form field no longer reads "is invalid" on top of
|
|
265
|
+
the application's own presence rule. Surfaced by the forum rebuild (ECS-17).
|
|
266
|
+
|
|
267
|
+
### Removed
|
|
268
|
+
|
|
269
|
+
- `rails g ecs_rails:relationship` and the per-relationship backing tables
|
|
270
|
+
(`post_authors`, ...). The dynamic `Post::AuthorRelationship` classes are gone;
|
|
271
|
+
`Post.components` lists `Relationship` instead.
|
|
272
|
+
|
|
273
|
+
### Changed
|
|
274
|
+
|
|
275
|
+
- The install migration is now `ecs_rails_install` (`EcsRailsInstall`) and
|
|
276
|
+
creates both `entities` and `relationships`.
|
|
277
|
+
- `RelationshipMeta` is `(name, target_class_name, unique)` with `slot` and
|
|
278
|
+
`reader_name` derived; `backing_class_name` and `foreign_key` are gone.
|
|
279
|
+
- The nested-preload key for a relationship's target is `target`:
|
|
280
|
+
`preload(author_relationship: { target: :name })`.
|
|
281
|
+
- **Every component table carries `slot string NOT NULL DEFAULT ''`, and the
|
|
282
|
+
unique index moves from `entity_id` to `(entity_id, slot)`.** The
|
|
283
|
+
`ecs_rails:component` and `ecs_rails:relationship` generators emit the new
|
|
284
|
+
shape; existing tables need `rails g ecs_rails:upgrade` (above). **Breaking
|
|
285
|
+
for existing databases** until upgraded: the gem's has_ones are slot-scoped
|
|
286
|
+
and will not find rows in a table with no slot column.
|
|
287
|
+
- `Entity.components` lists each component type once however many slots it is
|
|
288
|
+
declared under; `component_declarations` lists each slot.
|
|
289
|
+
- A new reader colliding with a name the entity already answers (a sibling's
|
|
290
|
+
reader or delegated method) now raises `DelegationConflict` at declaration.
|
|
291
|
+
- `EcsRails::Registry#register` takes `slot:` and `slot_options:`; the same
|
|
292
|
+
component in a different slot is a distinct declaration, the same slot twice
|
|
293
|
+
is still `DuplicateComponent`.
|
|
294
|
+
|
|
295
|
+
- **Delegation is component-prefixed by default** (ADR-0016). A component's
|
|
296
|
+
delegated methods are named `#{reader}_#{method}`: `component Email` now gives
|
|
297
|
+
`user.email_address`, `user.email_verified` and
|
|
298
|
+
`user.email_send_welcome_email`, all routed through `user.email`. The rule is
|
|
299
|
+
uniform — attributes and behaviour alike — so two components can never
|
|
300
|
+
collide on a shared attribute name, and a component reader can never be
|
|
301
|
+
shadowed by a delegated `belongs_to`. The reader (`user.email`) and the
|
|
302
|
+
presence predicate (`user.email?`) are unchanged.
|
|
303
|
+
**Breaking:** every bare delegated call (`user.address`) becomes
|
|
304
|
+
`user.email_address` unless the declaration opts out. `only:`/`except:`
|
|
305
|
+
still name the component's own methods (`except: [:title]`).
|
|
306
|
+
- `EcsRails::DelegationConflict` is now raised on the *entity-level* names, so
|
|
307
|
+
`Name#title` and `Group#title` coexist as `name_title` / `group_title` with no
|
|
308
|
+
`except:`. A conflict, and the reader-collision raise, now need two bare
|
|
309
|
+
declarations; both messages name dropping `prefix: false` as a way out.
|
|
310
|
+
- `relates_to` declares its backing component bare, so `post.author` /
|
|
311
|
+
`post.author=` keep their shape.
|
|
312
|
+
|
|
313
|
+
## [0.2.2] — 2026-07-23
|
|
314
|
+
|
|
315
|
+
### Added
|
|
316
|
+
|
|
317
|
+
- **YARD API documentation across the whole public API** — `@param`, `@return`,
|
|
318
|
+
`@raise`, `@example` and `@see` tags on every public module, class, attribute
|
|
319
|
+
and method (100% documented, verified by `yard stats`). The existing prose
|
|
320
|
+
rationale is kept; the tags are additive, so
|
|
321
|
+
[rubydoc.info/gems/ecs_on_rails](https://rubydoc.info/gems/ecs_on_rails) now
|
|
322
|
+
renders a real API reference rather than bare comments.
|
|
323
|
+
- A `.yardopts`, shipped in the gem so rubydoc.info honours it. Notably
|
|
324
|
+
`--embed-mixins`, without which the DSL that `EcsRails::Entity` gains by
|
|
325
|
+
`extend`ing {EcsRails::DSL}, `Querying`, `Preloading` and `Relationships`
|
|
326
|
+
would not appear on `Entity` at all — which is where users look for it.
|
|
327
|
+
- `documentation_uri` in the gemspec, pointing at the rubydoc.info API
|
|
328
|
+
reference. The narrative documentation (architecture, ADRs, RFCs) is reached
|
|
329
|
+
via the homepage and `source_code_uri`.
|
|
330
|
+
|
|
331
|
+
### Fixed
|
|
332
|
+
|
|
333
|
+
- `changelog_uri` in the gemspec pointed at `<repo>/blob/main/CHANGELOG.md`,
|
|
334
|
+
which 404s: the gem lives in `gem/` of a monorepo and there is no changelog at
|
|
335
|
+
the repo root, so the Changelog link on the 0.2.1 RubyGems page was broken.
|
|
336
|
+
Gem metadata is immutable once published, so this needed a release to correct.
|
|
337
|
+
- `source_code_uri` now points at the `gem/` subtree rather than the repo root.
|
|
338
|
+
|
|
339
|
+
No code change — 0.2.2 is identical to 0.2.1 apart from gemspec metadata.
|
|
340
|
+
|
|
9
341
|
## [0.2.1] — 2026-07-23
|
|
10
342
|
|
|
11
343
|
First RubyGems release.
|