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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +304 -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 +33 -11
- data/lib/ecs_rails/dsl.rb +463 -97
- 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 +19 -8
- data/lib/ecs_rails/presence.rb +83 -30
- data/lib/ecs_rails/querying.rb +158 -12
- data/lib/ecs_rails/railtie.rb +10 -0
- data/lib/ecs_rails/registry.rb +66 -21
- data/lib/ecs_rails/relationships.rb +168 -160
- data/lib/ecs_rails/slots.rb +183 -0
- data/lib/ecs_rails/validations.rb +3 -5
- data/lib/ecs_rails/version.rb +2 -2
- data/lib/ecs_rails.rb +7 -3
- data/lib/generators/ecs_rails/catalogue_options.rb +66 -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 +29 -12
- 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 +61 -4
- data/lib/generators/ecs_rails/install/templates/migration.rb.tt +0 -20
- data/lib/generators/ecs_rails/relationship/relationship_generator.rb +0 -99
- 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.
|
|
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/
|
|
112
|
-
- lib/generators/ecs_rails/
|
|
113
|
-
- lib/generators/ecs_rails/
|
|
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
|