ecs_on_rails 0.2.2 → 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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +304 -0
  3. data/README.md +365 -50
  4. data/lib/ecs_rails/catalogue/address.rb +39 -0
  5. data/lib/ecs_rails/catalogue/calendar_date.rb +27 -0
  6. data/lib/ecs_rails/catalogue/counter.rb +46 -0
  7. data/lib/ecs_rails/catalogue/discard.rb +36 -0
  8. data/lib/ecs_rails/catalogue/email.rb +35 -0
  9. data/lib/ecs_rails/catalogue/geolocation.rb +53 -0
  10. data/lib/ecs_rails/catalogue/identifier.rb +23 -0
  11. data/lib/ecs_rails/catalogue/image.rb +28 -0
  12. data/lib/ecs_rails/catalogue/link.rb +27 -0
  13. data/lib/ecs_rails/catalogue/marker.rb +38 -0
  14. data/lib/ecs_rails/catalogue/money.rb +86 -0
  15. data/lib/ecs_rails/catalogue/name.rb +37 -0
  16. data/lib/ecs_rails/catalogue/password.rb +50 -0
  17. data/lib/ecs_rails/catalogue/period.rb +57 -0
  18. data/lib/ecs_rails/catalogue/phone.rb +30 -0
  19. data/lib/ecs_rails/catalogue/position.rb +18 -0
  20. data/lib/ecs_rails/catalogue/rating.rb +25 -0
  21. data/lib/ecs_rails/catalogue/relationship.rb +159 -0
  22. data/lib/ecs_rails/catalogue/role.rb +23 -0
  23. data/lib/ecs_rails/catalogue/schema_diff.rb +198 -0
  24. data/lib/ecs_rails/catalogue/search_vector.rb +38 -0
  25. data/lib/ecs_rails/catalogue/state.rb +71 -0
  26. data/lib/ecs_rails/catalogue/tags.rb +64 -0
  27. data/lib/ecs_rails/catalogue/text.rb +29 -0
  28. data/lib/ecs_rails/catalogue/timestamp.rb +34 -0
  29. data/lib/ecs_rails/catalogue/token.rb +77 -0
  30. data/lib/ecs_rails/catalogue.rb +405 -0
  31. data/lib/ecs_rails/component.rb +13 -7
  32. data/lib/ecs_rails/config.rb +33 -11
  33. data/lib/ecs_rails/dsl.rb +463 -97
  34. data/lib/ecs_rails/entity.rb +21 -5
  35. data/lib/ecs_rails/errors.rb +3 -0
  36. data/lib/ecs_rails/inverses.rb +398 -0
  37. data/lib/ecs_rails/lazy.rb +86 -23
  38. data/lib/ecs_rails/markers.rb +197 -0
  39. data/lib/ecs_rails/preloading.rb +19 -8
  40. data/lib/ecs_rails/presence.rb +83 -30
  41. data/lib/ecs_rails/querying.rb +158 -12
  42. data/lib/ecs_rails/railtie.rb +10 -0
  43. data/lib/ecs_rails/registry.rb +66 -21
  44. data/lib/ecs_rails/relationships.rb +168 -160
  45. data/lib/ecs_rails/slots.rb +183 -0
  46. data/lib/ecs_rails/validations.rb +3 -5
  47. data/lib/ecs_rails/version.rb +2 -2
  48. data/lib/ecs_rails.rb +7 -3
  49. data/lib/generators/ecs_rails/catalogue_options.rb +66 -0
  50. data/lib/generators/ecs_rails/component/templates/migration.rb.tt +9 -5
  51. data/lib/generators/ecs_rails/component/templates/model.rb.tt +2 -1
  52. data/lib/generators/ecs_rails/entity/entity_generator.rb +93 -0
  53. data/lib/generators/ecs_rails/entity/templates/entity.rb.tt +9 -0
  54. data/lib/generators/ecs_rails/install/install_generator.rb +29 -12
  55. data/lib/generators/ecs_rails/install/templates/application_component.rb.tt +3 -2
  56. data/lib/generators/ecs_rails/install/templates/application_entity.rb.tt +2 -2
  57. data/lib/generators/ecs_rails/install/templates/initializer.rb.tt +2 -2
  58. data/lib/generators/ecs_rails/install/templates/install_migration.rb.tt +38 -0
  59. data/lib/generators/ecs_rails/templates/catalogue_class.rb.tt +10 -0
  60. data/lib/generators/ecs_rails/upgrade/templates/catalogue_migration.rb.tt +18 -0
  61. data/lib/generators/ecs_rails/upgrade/templates/markers_migration.rb.tt +31 -0
  62. data/lib/generators/ecs_rails/upgrade/templates/relationships_migration.rb.tt +33 -0
  63. data/lib/generators/ecs_rails/upgrade/templates/slots_migration.rb.tt +23 -0
  64. data/lib/generators/ecs_rails/upgrade/upgrade_generator.rb +281 -0
  65. metadata +61 -4
  66. data/lib/generators/ecs_rails/install/templates/migration.rb.tt +0 -20
  67. data/lib/generators/ecs_rails/relationship/relationship_generator.rb +0 -99
  68. data/lib/generators/ecs_rails/relationship/templates/migration.rb.tt +0 -32
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Moves markers onto the shared `markers` table (ADR-0018 §4).
4
+ #
5
+ # A marker has no state: an entity IS a moderator exactly when its row exists.
6
+ # Every marker is a row here, the slot being the marker name, so declaring
7
+ # `marker :moderator` no longer needs a table of its own.
8
+ #
9
+ # Generated by `rails g ecs_rails:upgrade`, which found the tables below by
10
+ # their shape: component tables with no attribute columns at all. The `markers`
11
+ # table itself is created by the catalogue migration that precedes this one.
12
+ # Review the list before running — each is copied into `markers` under its
13
+ # singular name and dropped. Irreversible, hence `up`/`down`.
14
+ class EcsRailsSharedMarkers < ActiveRecord::Migration[<%= migration_version %>]
15
+ def up
16
+ <% marker_tables.each do |marker| -%>
17
+
18
+ # <%= marker[:table] %> -> slot "<%= marker[:slot] %>"
19
+ execute <<~SQL
20
+ INSERT INTO markers (id, entity_id, slot, created_at, updated_at)
21
+ SELECT id, entity_id, '<%= marker[:slot] %>', created_at, updated_at
22
+ FROM <%= marker[:table] %>
23
+ SQL
24
+ drop_table :<%= marker[:table] %>
25
+ <% end -%>
26
+ end
27
+
28
+ def down
29
+ raise ActiveRecord::IrreversibleMigration, "the per-marker tables were dropped"
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Moves relationships onto the shared `relationships` table (ADR-0017).
4
+ #
5
+ # Every `relates_to` is a row here: the owner is entity_id, the relationship
6
+ # name is the slot, the pointed-at entity is target_id. Declaring a relationship
7
+ # no longer needs a migration; this is the one that gets an existing
8
+ # application there.
9
+ #
10
+ # Generated by `rails g ecs_rails:upgrade`, which found the tables below by
11
+ # their ADR-0013 shape (entity_id + one `<name>_id` + timestamps) and name
12
+ # (`<owner>_<names>`). The `relationships` table itself is created by the
13
+ # catalogue migration that precedes this one. Review the list before running:
14
+ # each is copied into `relationships` and dropped. Irreversible — the old
15
+ # tables are gone once the rows have moved — hence `up`/`down`.
16
+ class EcsRailsSharedRelationships < ActiveRecord::Migration[<%= migration_version %>]
17
+ def up
18
+ <% backing_tables.each do |backing| -%>
19
+
20
+ # <%= backing[:table] %> -> slot "<%= backing[:slot] %>" on <%= backing[:owner_model] %>
21
+ execute <<~SQL
22
+ INSERT INTO relationships (id, entity_id, slot, target_id, owner_model, exclusive, created_at, updated_at)
23
+ SELECT id, entity_id, '<%= backing[:slot] %>', <%= backing[:foreign_key] %>, '<%= backing[:owner_model] %>', false, created_at, updated_at
24
+ FROM <%= backing[:table] %>
25
+ SQL
26
+ drop_table :<%= backing[:table] %>
27
+ <% end -%>
28
+ end
29
+
30
+ def down
31
+ raise ActiveRecord::IrreversibleMigration, "the per-relationship tables were dropped"
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds the slot column to every existing component table (RFC-0014 / ADR-0015).
4
+ #
5
+ # A component may now be declared more than once on an entity under distinct
6
+ # labels (`component PostalAddress, prefix: :business`); the `slot` column is
7
+ # the label, "" for the singular default. The one-per-entity unique index
8
+ # (ADR-0005) becomes one-per-(entity, slot). Safe on existing data: every row
9
+ # already here is the single slot = "" row, so the new index admits exactly what
10
+ # the old one did.
11
+ #
12
+ # Generated by `rails g ecs_rails:upgrade`, which found these tables by looking
13
+ # for an entity_id column and no slot column.
14
+ class EcsRailsAddSlots < ActiveRecord::Migration[<%= migration_version %>]
15
+ def change
16
+ <% tables_missing_slot.each do |table| -%>
17
+ add_column :<%= table %>, :slot, :string, null: false, default: ""
18
+ remove_index :<%= table %>, column: :entity_id, unique: true
19
+ add_index :<%= table %>, [:entity_id, :slot], unique: true
20
+ <%= "\n" unless table == tables_missing_slot.last -%>
21
+ <% end -%>
22
+ end
23
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ # See the note in install_generator.rb: rails/generators/active_record/migration
4
+ # references ActiveRecord::Migration without loading it.
5
+ require "active_record"
6
+ require "rails/generators/base"
7
+ require "rails/generators/active_record/migration"
8
+
9
+ # Stands on its own requires, like the other generators (RFC-0008's isolation
10
+ # note and spec/generators/generator_isolation_spec.rb).
11
+ require "ecs_rails"
12
+ require_relative "../catalogue_options"
13
+
14
+ module EcsRails
15
+ module Generators
16
+ # `rails g ecs_rails:upgrade`
17
+ #
18
+ # Brings an existing application's schema up to the gem's current one.
19
+ # ADR-0018 makes this the *only* migration a user ever runs after
20
+ # `ecs_rails:install`. It has two jobs today, each its own migration file so
21
+ # that a later gem version can add a third without renaming either:
22
+ #
23
+ # 1. **Slots** (RFC-0014 / ADR-0015) — for every component table without a
24
+ # `slot` column:
25
+ #
26
+ # add_column :emails, :slot, :string, null: false, default: ""
27
+ # remove_index :emails, column: :entity_id, unique: true
28
+ # add_index :emails, [:entity_id, :slot], unique: true
29
+ #
30
+ # Safe on shipped data: every existing row is the single `slot = ""`, so
31
+ # the new composite index admits exactly the rows the old one did.
32
+ #
33
+ # 2. **Shared relationships** (ADR-0017) — creates the `relationships` table
34
+ # if it is missing, and moves every pre-0.3 per-relationship backing table
35
+ # (`post_authors`, `membership_users`, ...) into it: each row becomes a
36
+ # row with `slot` = the relationship name, `target_id` = the old
37
+ # `<name>_id`, `owner_model` = the owner's discriminator; the old table is
38
+ # then dropped. Irreversible, and written as `up`/`down` to say so.
39
+ #
40
+ # 3. **Shared markers** (ADR-0018 §4) — moves every pre-0.3 marker table (a
41
+ # component table with no attribute columns at all: `moderators`,
42
+ # `administrators`) into `markers`, slot = the table's singular name; the
43
+ # old table is then dropped. Irreversible.
44
+ #
45
+ # Before 2 and 3, **the catalogue** (ADR-0018): for every catalogue
46
+ # component in the selected `--sets` (and every catalogue table that already
47
+ # exists, whatever its set), a missing table is created and a table that
48
+ # predates a column or index is brought forward — both rendered from the
49
+ # gem's schema declarations, the same recordings install renders. Missing
50
+ # one-line classes are written too; existing files are never touched. This
51
+ # is how a newer gem version's catalogue additions, or a set added later,
52
+ # reach an application.
53
+ #
54
+ # Tables are found by inspecting the database, not the registry: a generator
55
+ # runs before the app's classes are loaded, and the registry would miss a
56
+ # component whose entity is never referenced. Any table with an `entity_id`
57
+ # column is a component table (architecture.md §2). A backing table is
58
+ # recognised by its shape — entity_id, exactly one other `<name>_id` uuid
59
+ # column, timestamps, nothing else — and its ADR-0013 name,
60
+ # `<owner>_<names>`. The developer reviews the file before running it, as
61
+ # with any generated migration. When nothing is pending, nothing is written
62
+ # and the generator says so.
63
+ class UpgradeGenerator < Rails::Generators::Base
64
+ include ActiveRecord::Generators::Migration
65
+ include CatalogueOptions
66
+
67
+ source_root File.expand_path("templates", __dir__)
68
+
69
+ desc "Upgrades an existing schema to the gem's current one: the slot column " \
70
+ "on component tables, and the shared relationships and markers tables."
71
+
72
+ # Inspects every selected catalogue table before any generator writes.
73
+ # An incompatible schema must not leave a partial set of upgrade files.
74
+ #
75
+ # @return [void]
76
+ def verify_catalogue_schema
77
+ catalogue_changes
78
+ rescue EcsRails::SchemaMismatch => error
79
+ raise Thor::Error, error.message
80
+ end
81
+
82
+ # Emits the slot migration for component tables that lack the column, or
83
+ # says there is nothing to do. Backing tables about to be moved into
84
+ # `relationships`, and marker tables about to be moved into `markers`, are
85
+ # skipped — they are dropped a migration later.
86
+ #
87
+ # A Thor task: invoked as a generator step, not called directly.
88
+ #
89
+ # @return [void]
90
+ def create_slots_migration
91
+ if tables_missing_slot.empty?
92
+ say "Every component table already has a slot column.", :green
93
+ return
94
+ end
95
+
96
+ migration_template(
97
+ "slots_migration.rb.tt",
98
+ File.join(db_migrate_path, "ecs_rails_add_slots.rb")
99
+ )
100
+ end
101
+
102
+ # Emits the catalogue migration — missing tables and missing columns or
103
+ # indexes on existing ones — or says everything is current, and writes any
104
+ # missing one-line classes for the selected sets.
105
+ #
106
+ # A Thor task: invoked as a generator step, not called directly.
107
+ #
108
+ # @return [void]
109
+ def create_catalogue_migration
110
+ create_catalogue_class_files(catalogue_components)
111
+
112
+ if catalogue_changes.empty?
113
+ say "Every catalogue table is current.", :green
114
+ return
115
+ end
116
+
117
+ migration_template(
118
+ "catalogue_migration.rb.tt",
119
+ File.join(db_migrate_path, "ecs_rails_catalogue.rb")
120
+ )
121
+ end
122
+
123
+ # Emits the shared-relationships data move when per-relationship backing
124
+ # tables remain, or says so. The `relationships` table itself is the
125
+ # catalogue migration's business, which runs first.
126
+ #
127
+ # A Thor task: invoked as a generator step, not called directly.
128
+ #
129
+ # @return [void]
130
+ def create_relationships_migration
131
+ if backing_tables.empty?
132
+ say "Relationships already live in the shared relationships table.", :green
133
+ return
134
+ end
135
+
136
+ migration_template(
137
+ "relationships_migration.rb.tt",
138
+ File.join(db_migrate_path, "ecs_rails_shared_relationships.rb")
139
+ )
140
+ end
141
+
142
+ # Emits the shared-markers data move when empty per-marker tables remain,
143
+ # or says so. The `markers` table itself is the catalogue migration's
144
+ # business, which runs first.
145
+ #
146
+ # A Thor task: invoked as a generator step, not called directly.
147
+ #
148
+ # @return [void]
149
+ def create_markers_migration
150
+ if marker_tables.empty?
151
+ say "Markers already live in the shared markers table.", :green
152
+ return
153
+ end
154
+
155
+ migration_template(
156
+ "markers_migration.rb.tt",
157
+ File.join(db_migrate_path, "ecs_rails_shared_markers.rb")
158
+ )
159
+ end
160
+
161
+ private
162
+
163
+ # Component tables (any table with an entity_id column, other than
164
+ # `entities` itself) that have no `slot` column yet — minus the backing
165
+ # tables the relationships migration drops. Sorted, so the migration is
166
+ # deterministic; uniq, because a multi-schema search_path can list one
167
+ # name twice (the columns then resolve to the first schema).
168
+ def tables_missing_slot
169
+ @tables_missing_slot ||= component_tables.select do |table|
170
+ !column_names(table).include?("slot") &&
171
+ backing_tables.none? { |b| b[:table] == table } &&
172
+ marker_tables.none? { |m| m[:table] == table }
173
+ end
174
+ end
175
+
176
+ # Pre-0.3 marker tables: a component table with no attribute columns at all
177
+ # — only id, entity_id, (slot,) timestamps — other than `markers` itself.
178
+ # Each becomes `{ table: "moderators", slot: "moderator" }`. Catalogue
179
+ # ownership wins over shape: missing attributes must never trigger a move.
180
+ def marker_tables
181
+ @marker_tables ||= component_tables.filter_map do |table|
182
+ next if table == "markers" || upgradeable_components.any? { |component| component.table == table }
183
+ next unless (column_names(table) - %w[id entity_id slot created_at updated_at]).empty?
184
+
185
+ { table: table, slot: table.singularize }
186
+ end
187
+ end
188
+
189
+ def component_tables
190
+ @component_tables ||= connection.tables.uniq.sort.select do |table|
191
+ table != "entities" && column_names(table).include?("entity_id")
192
+ end
193
+ end
194
+
195
+ # The catalogue components this upgrade brings forward: the selected sets,
196
+ # plus any component the application already has a one-line class for,
197
+ # whatever its set (an app that installed `commerce` earlier keeps getting
198
+ # its upgrades). The class file, not the table, is the evidence: a bespoke
199
+ # table that merely shares a catalogue name (`names`, say) must not be
200
+ # "upgraded" with the catalogue's columns. Within a selected set the table
201
+ # IS treated as the catalogue's — the developer asked for that set.
202
+ def upgradeable_components
203
+ @upgradeable_components ||= (catalogue_components +
204
+ EcsRails::Catalogue.components.select { |c| catalogue_class_file?(c) }).uniq
205
+ end
206
+
207
+ # Does the components directory hold a class including this concern?
208
+ def catalogue_class_file?(component)
209
+ dir = File.join(destination_root, EcsRails.config.components_path)
210
+ return false unless File.directory?(dir)
211
+
212
+ Dir.glob(File.join(dir, "*.rb")).any? do |file|
213
+ File.read(file).include?("include EcsRails::Catalogue::#{component.class_name}")
214
+ end
215
+ end
216
+
217
+ # `[{ component:, source: }]` — the migration source for each component
218
+ # whose table is missing, or is missing a column or index. Empty when the
219
+ # catalogue is current.
220
+ def catalogue_changes
221
+ @catalogue_changes ||= upgradeable_components.filter_map do |component|
222
+ table = component.table
223
+ source =
224
+ if connection.tables.include?(table)
225
+ component.schema.to_ruby_diff(
226
+ table_name: table,
227
+ connection: connection,
228
+ slot_upgrade: tables_missing_slot.include?(table)
229
+ )
230
+ else
231
+ component.schema.to_ruby(table_name: table)
232
+ end
233
+ { component: component, source: source } unless source.empty?
234
+ end
235
+ end
236
+
237
+ # ADR-0013 backing tables, recognised by shape and name: columns are
238
+ # exactly id, entity_id, one `<name>_id` uuid, timestamps (plus slot, if a
239
+ # slots upgrade already ran), and the table is `<owner>_<names>` with a
240
+ # non-empty owner. Each becomes
241
+ # `{ table:, slot: "author", foreign_key: "author_id", owner_model: "posts" }`.
242
+ # The `relationships` table itself is excluded by shape (it has more
243
+ # columns), and a bespoke component with a single foreign key but its own
244
+ # name (`sponsors`) is excluded by the name rule.
245
+ def backing_tables
246
+ @backing_tables ||= component_tables.filter_map do |table|
247
+ next if upgradeable_components.any? { |component| component.table == table }
248
+
249
+ columns = column_names(table) - %w[id entity_id slot created_at updated_at]
250
+ next unless columns.size == 1
251
+
252
+ foreign_key = columns.first
253
+ next unless foreign_key.end_with?("_id") && column_type(table, foreign_key) == :uuid
254
+
255
+ name = foreign_key.delete_suffix("_id")
256
+ suffix = "_#{name.pluralize}"
257
+ next unless table.end_with?(suffix) && table.length > suffix.length
258
+
259
+ owner = table.delete_suffix(suffix)
260
+ { table: table, slot: name, foreign_key: foreign_key, owner_model: owner.pluralize }
261
+ end
262
+ end
263
+
264
+ def column_names(table)
265
+ connection.columns(table).map(&:name)
266
+ end
267
+
268
+ def column_type(table, column)
269
+ connection.columns(table).find { |c| c.name == column }&.type
270
+ end
271
+
272
+ def connection
273
+ ActiveRecord::Base.connection
274
+ end
275
+
276
+ def migration_version
277
+ "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
278
+ end
279
+ end
280
+ end
281
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecs_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Hutchens
@@ -9,6 +9,26 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '2.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3.0'
12
32
  - !ruby/object:Gem::Dependency
13
33
  name: activerecord
14
34
  requirement: !ruby/object:Gem::Requirement
@@ -86,31 +106,68 @@ files:
86
106
  - README.md
87
107
  - lib/ecs_on_rails.rb
88
108
  - lib/ecs_rails.rb
109
+ - lib/ecs_rails/catalogue.rb
110
+ - lib/ecs_rails/catalogue/address.rb
111
+ - lib/ecs_rails/catalogue/calendar_date.rb
112
+ - lib/ecs_rails/catalogue/counter.rb
113
+ - lib/ecs_rails/catalogue/discard.rb
114
+ - lib/ecs_rails/catalogue/email.rb
115
+ - lib/ecs_rails/catalogue/geolocation.rb
116
+ - lib/ecs_rails/catalogue/identifier.rb
117
+ - lib/ecs_rails/catalogue/image.rb
118
+ - lib/ecs_rails/catalogue/link.rb
119
+ - lib/ecs_rails/catalogue/marker.rb
120
+ - lib/ecs_rails/catalogue/money.rb
121
+ - lib/ecs_rails/catalogue/name.rb
122
+ - lib/ecs_rails/catalogue/password.rb
123
+ - lib/ecs_rails/catalogue/period.rb
124
+ - lib/ecs_rails/catalogue/phone.rb
125
+ - lib/ecs_rails/catalogue/position.rb
126
+ - lib/ecs_rails/catalogue/rating.rb
127
+ - lib/ecs_rails/catalogue/relationship.rb
128
+ - lib/ecs_rails/catalogue/role.rb
129
+ - lib/ecs_rails/catalogue/schema_diff.rb
130
+ - lib/ecs_rails/catalogue/search_vector.rb
131
+ - lib/ecs_rails/catalogue/state.rb
132
+ - lib/ecs_rails/catalogue/tags.rb
133
+ - lib/ecs_rails/catalogue/text.rb
134
+ - lib/ecs_rails/catalogue/timestamp.rb
135
+ - lib/ecs_rails/catalogue/token.rb
89
136
  - lib/ecs_rails/component.rb
90
137
  - lib/ecs_rails/config.rb
91
138
  - lib/ecs_rails/dsl.rb
92
139
  - lib/ecs_rails/entity.rb
93
140
  - lib/ecs_rails/errors.rb
141
+ - lib/ecs_rails/inverses.rb
94
142
  - lib/ecs_rails/lazy.rb
143
+ - lib/ecs_rails/markers.rb
95
144
  - lib/ecs_rails/preloading.rb
96
145
  - lib/ecs_rails/presence.rb
97
146
  - lib/ecs_rails/querying.rb
98
147
  - lib/ecs_rails/railtie.rb
99
148
  - lib/ecs_rails/registry.rb
100
149
  - lib/ecs_rails/relationships.rb
150
+ - lib/ecs_rails/slots.rb
101
151
  - lib/ecs_rails/validations.rb
102
152
  - lib/ecs_rails/version.rb
153
+ - lib/generators/ecs_rails/catalogue_options.rb
103
154
  - lib/generators/ecs_rails/component/component_generator.rb
104
155
  - lib/generators/ecs_rails/component/templates/component_spec.rb.tt
105
156
  - lib/generators/ecs_rails/component/templates/migration.rb.tt
106
157
  - lib/generators/ecs_rails/component/templates/model.rb.tt
158
+ - lib/generators/ecs_rails/entity/entity_generator.rb
159
+ - lib/generators/ecs_rails/entity/templates/entity.rb.tt
107
160
  - lib/generators/ecs_rails/install/install_generator.rb
108
161
  - lib/generators/ecs_rails/install/templates/application_component.rb.tt
109
162
  - lib/generators/ecs_rails/install/templates/application_entity.rb.tt
110
163
  - lib/generators/ecs_rails/install/templates/initializer.rb.tt
111
- - lib/generators/ecs_rails/install/templates/migration.rb.tt
112
- - lib/generators/ecs_rails/relationship/relationship_generator.rb
113
- - lib/generators/ecs_rails/relationship/templates/migration.rb.tt
164
+ - lib/generators/ecs_rails/install/templates/install_migration.rb.tt
165
+ - lib/generators/ecs_rails/templates/catalogue_class.rb.tt
166
+ - lib/generators/ecs_rails/upgrade/templates/catalogue_migration.rb.tt
167
+ - lib/generators/ecs_rails/upgrade/templates/markers_migration.rb.tt
168
+ - lib/generators/ecs_rails/upgrade/templates/relationships_migration.rb.tt
169
+ - lib/generators/ecs_rails/upgrade/templates/slots_migration.rb.tt
170
+ - lib/generators/ecs_rails/upgrade/upgrade_generator.rb
114
171
  homepage: https://github.com/kranzky/ecs_rails
115
172
  licenses:
116
173
  - MIT
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Creates the single `entities` table every ECS Rails entity shares.
4
- # See docs/architecture.md §2 and docs/adr/0002-single-entities-table.md.
5
- class EcsRailsCreateEntities < ActiveRecord::Migration[<%= migration_version %>]
6
- def change
7
- # gen_random_uuid() lives in pgcrypto on PostgreSQL < 13; enabling it is
8
- # harmless on 13+, where the function is built in.
9
- enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
10
-
11
- create_table :entities, id: :uuid, default: -> { "gen_random_uuid()" } do |t|
12
- # The entity subclass discriminator, e.g. "users". Indexed because
13
- # User.all compiles to WHERE model = 'users'.
14
- t.string :model, null: false, index: true
15
- t.datetime :created_at, null: false
16
- # No updated_at — an entity is written once and never changes.
17
- # See RFC-0001 and docs/architecture.md §1.
18
- end
19
- end
20
- end
@@ -1,99 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # See the note in component_generator.rb: rails/generators/active_record/migration
4
- # references ActiveRecord::Migration without loading it, so these are required
5
- # explicitly for load-isolation (generator_isolation_spec.rb).
6
- require "active_record"
7
- require "rails/generators/named_base"
8
- require "rails/generators/active_record/migration"
9
-
10
- # ADR-0010: the generator reads EcsRails.config. Require the library explicitly so
11
- # the generator stands on its own requires (RFC-0008's isolation note).
12
- require "ecs_rails"
13
-
14
- module EcsRails
15
- module Generators
16
- # `rails g ecs_rails:relationship OWNER name:Target`
17
- #
18
- # rails g ecs_rails:relationship Post author:User
19
- #
20
- # Implements RFC-0012 / ADR-0013. Unlike the component generator, this emits
21
- # ONLY a migration — `relates_to` defines the backing component dynamically,
22
- # so there is no model file to write. It prints a reminder to add the
23
- # `relates_to` line to the entity.
24
- #
25
- # The migration creates the owner-scoped backing table `post_authors`:
26
- # - uuid PK,
27
- # - entity_id: not-null, UNIQUE index, ON DELETE CASCADE FK to entities —
28
- # the owner side (a post has at most one author; destroying the post
29
- # destroys the link),
30
- # - author_id: indexed, ON DELETE **NULLIFY** FK to entities — the target
31
- # side (destroying the target nullifies, does not cascade to the owner),
32
- # - timestamps.
33
- #
34
- # The single `name:Target` argument does not fit Rails' GeneratedAttribute
35
- # (there is no `:Target` column type), so it is parsed by hand — see
36
- # #parse_relationship!.
37
- class RelationshipGenerator < Rails::Generators::NamedBase
38
- include ActiveRecord::Generators::Migration
39
-
40
- source_root File.expand_path("templates", __dir__)
41
-
42
- argument :relationship, type: :string, banner: "name:Target"
43
-
44
- desc "Creates the migration for a relationship's backing table (no model)."
45
-
46
- # Emits the backing relationship table migration.
47
- #
48
- # A Thor task: invoked as a generator step, not called directly.
49
- #
50
- # @return [void]
51
- def create_migration_file
52
- parse_relationship!
53
- migration_template(
54
- "migration.rb.tt",
55
- File.join(db_migrate_path, "create_#{backing_table_name}.rb")
56
- )
57
- end
58
-
59
- # RFC-0012: no component file. The DSL defines the backing component; the
60
- # developer only has to declare the relationship on the entity.
61
- def print_relates_to_reminder
62
- entity_path = File.join(EcsRails.config.entities_path, class_path, "#{file_name}.rb")
63
- say "Add the relationship to #{entity_path}:", :green
64
- say " relates_to :#{relation_name}, #{target_class_name}"
65
- end
66
-
67
- private
68
-
69
- # Splits the `name:Target` argument. `file_name` (from NamedBase) is the
70
- # OWNER, e.g. "post"; `relationship` is "author:User".
71
- def parse_relationship!
72
- @relation_name, @target_class_name = relationship.split(":", 2)
73
-
74
- return unless @relation_name.to_s.empty? || @target_class_name.to_s.empty?
75
-
76
- raise Thor::Error,
77
- "expected OWNER name:Target, e.g. " \
78
- "`rails g ecs_rails:relationship Post author:User` (got #{relationship.inspect})"
79
- end
80
-
81
- attr_reader :relation_name, :target_class_name
82
-
83
- # The owner-scoped backing table: #{owner.singular}_#{relation.plural} —
84
- # `post_authors`. Mirrors the derivation in EcsRails::Relationships.
85
- def backing_table_name
86
- "#{singular_table_name}_#{relation_name.pluralize}"
87
- end
88
-
89
- # The target FK column: `author_id`. Mirrors EcsRails::Relationships.
90
- def target_foreign_key
91
- "#{relation_name}_id"
92
- end
93
-
94
- def migration_version
95
- "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
96
- end
97
- end
98
- end
99
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Creates the <%= backing_table_name %> relationship backing table
4
- # (RFC-0012 / ADR-0013). This is what `relates_to :<%= relation_name %>,
5
- # <%= target_class_name %>` reads; there is no relationship component file — the
6
- # DSL defines the backing component dynamically.
7
- #
8
- # The owner side (entity_id) and the target side (<%= target_foreign_key %>) are
9
- # deliberately asymmetric:
10
- # - entity_id: not-null, UNIQUE (ADR-0005: at most one per owner), ON DELETE
11
- # CASCADE — destroying the owner destroys the link.
12
- # - <%= target_foreign_key %>: nullable, ON DELETE NULLIFY — destroying the
13
- # TARGET nullifies the link, it does NOT cascade to the owner.
14
- class Create<%= backing_table_name.camelize %> < ActiveRecord::Migration[<%= migration_version %>]
15
- def change
16
- create_table :<%= backing_table_name %>, id: :uuid, default: -> { "gen_random_uuid()" } do |t|
17
- t.uuid :entity_id, null: false
18
- t.uuid :<%= target_foreign_key %>, default: nil
19
- t.timestamps
20
- end
21
-
22
- # ADR-0005: at most one relationship row per owner entity.
23
- add_index :<%= backing_table_name %>, :entity_id, unique: true
24
-
25
- # Destroying the owner destroys the link, at the database level.
26
- add_foreign_key :<%= backing_table_name %>, :entities, column: :entity_id, on_delete: :cascade
27
-
28
- # Destroying the TARGET nullifies the link — it does not cascade to the owner.
29
- add_index :<%= backing_table_name %>, :<%= target_foreign_key %>
30
- add_foreign_key :<%= backing_table_name %>, :entities, column: :<%= target_foreign_key %>, on_delete: :nullify
31
- end
32
- end