typed_eav 0.7.0 → 0.8.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 +94 -2
- data/README.md +223 -3
- data/lib/typed_eav/bulk_read.rb +146 -58
- data/lib/typed_eav/bulk_upsert.rb +10 -14
- data/lib/typed_eav/entity_query.rb +156 -4
- data/lib/typed_eav/filter_query.rb +2 -2
- data/lib/typed_eav/has_typed_eav/dirty_tracking.rb +207 -0
- data/lib/typed_eav/has_typed_eav/instance_methods.rb +2 -2
- data/lib/typed_eav/has_typed_eav.rb +7 -3
- data/lib/typed_eav/partition/definition_batch.rb +70 -0
- data/lib/typed_eav/partition.rb +2 -0
- data/lib/typed_eav/scalar_query.rb +228 -0
- data/lib/typed_eav/schema_portability/preview.rb +379 -0
- data/lib/typed_eav/schema_portability.rb +20 -0
- data/lib/typed_eav/version.rb +1 -1
- data/lib/typed_eav/versioned.rb +8 -6
- data/lib/typed_eav.rb +1 -0
- metadata +5 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypedEAV
|
|
4
|
+
module SchemaPortability
|
|
5
|
+
# Read-only comparator for the portable schema wire format. This class is
|
|
6
|
+
# intentionally separate from import_schema: a preview must not run
|
|
7
|
+
# validations, callbacks, jobs, conversions, or a dry-run transaction.
|
|
8
|
+
# Each incoming entry must carry the exact entity_type/scope/parent_scope
|
|
9
|
+
# identity declared by the envelope; preview does not retarget entries.
|
|
10
|
+
# rubocop:disable Metrics/ClassLength -- the validator and comparator form
|
|
11
|
+
# one small, private implementation behind the single preview API.
|
|
12
|
+
class Preview
|
|
13
|
+
FIELD_IDENTITY_KEYS = %w[name entity_type scope parent_scope].freeze
|
|
14
|
+
SECTION_IDENTITY_KEYS = %w[code entity_type scope parent_scope].freeze
|
|
15
|
+
OPTION_KEYS = %w[label value sort_order].freeze
|
|
16
|
+
CONFLICT_STATUSES = %w[changed conflict].freeze
|
|
17
|
+
def initialize(hash, on_conflict:)
|
|
18
|
+
@hash = hash
|
|
19
|
+
@on_conflict = on_conflict
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
validate_input!
|
|
24
|
+
|
|
25
|
+
fields = @hash["fields"]
|
|
26
|
+
sections = @hash["sections"]
|
|
27
|
+
import_index = ImportIndex.new(fields, sections)
|
|
28
|
+
field_previews = preview_fields(fields, import_index)
|
|
29
|
+
section_previews = preview_sections(sections, import_index)
|
|
30
|
+
entries = field_previews + section_previews
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
"schema_version" => @hash["schema_version"],
|
|
34
|
+
"entity_type" => @hash["entity_type"],
|
|
35
|
+
"scope" => @hash["scope"],
|
|
36
|
+
"parent_scope" => @hash["parent_scope"],
|
|
37
|
+
"on_conflict" => @on_conflict.to_s,
|
|
38
|
+
"importable" => entries.none? { |entry| entry["action"] == "error" },
|
|
39
|
+
"summary" => summary_for(entries),
|
|
40
|
+
"fields" => field_previews,
|
|
41
|
+
"sections" => section_previews,
|
|
42
|
+
"risks" => entries.flat_map { |entry| entry["risks"] }.uniq.sort,
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def validate_input!
|
|
49
|
+
raise ArgumentError, "schema preview expects a Hash export" unless @hash.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
SchemaPortability.send(:validate_schema_version!, @hash)
|
|
52
|
+
SchemaPortability.send(:validate_conflict_policy!, @on_conflict)
|
|
53
|
+
|
|
54
|
+
required_keys = %w[entity_type scope parent_scope fields sections]
|
|
55
|
+
missing_keys = required_keys.reject { |key| @hash.key?(key) }
|
|
56
|
+
unless missing_keys.empty?
|
|
57
|
+
raise ArgumentError, "Schema preview is missing required keys: #{missing_keys.join(", ")}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
validate_target!
|
|
61
|
+
validate_entries!(@hash["fields"], FIELD_IDENTITY_KEYS, "field") do |entry|
|
|
62
|
+
validate_field_entry!(entry)
|
|
63
|
+
end
|
|
64
|
+
validate_entries!(@hash["sections"], SECTION_IDENTITY_KEYS, "section") do |entry|
|
|
65
|
+
validate_section_entry!(entry)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_target!
|
|
70
|
+
unless @hash["entity_type"].is_a?(String) && @hash["entity_type"].present?
|
|
71
|
+
raise ArgumentError, "Schema preview entity_type must be a non-empty String"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
validate_scope_slot!(@hash["scope"], "scope")
|
|
75
|
+
validate_scope_slot!(@hash["parent_scope"], "parent_scope")
|
|
76
|
+
return if TypedEAV::ScopeTuple.invariant_satisfied?(@hash["scope"], @hash["parent_scope"])
|
|
77
|
+
|
|
78
|
+
raise ArgumentError, "Schema preview target parent_scope requires a non-blank scope"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_scope_slot!(value, name)
|
|
82
|
+
return if value.nil? || value.is_a?(String)
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, "Schema preview #{name} must be a String or nil"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_entries!(entries, identity_keys, kind)
|
|
88
|
+
raise ArgumentError, "Schema preview #{kind}s must be an Array" unless entries.is_a?(Array)
|
|
89
|
+
|
|
90
|
+
seen = {}
|
|
91
|
+
entries.each do |entry|
|
|
92
|
+
validate_entry_identity!(entry, identity_keys, kind, seen)
|
|
93
|
+
yield(entry) if block_given?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def validate_entry_identity!(entry, identity_keys, kind, seen)
|
|
98
|
+
raise ArgumentError, "Schema preview #{kind} entries must be Hashes" unless entry.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
validate_identity_keys!(entry, identity_keys, kind)
|
|
101
|
+
validate_identity_values!(entry, identity_keys, kind)
|
|
102
|
+
identity = entry.values_at(*identity_keys)
|
|
103
|
+
raise ArgumentError, "Duplicate #{kind} identity in schema preview: #{identity.inspect}" if seen.key?(identity)
|
|
104
|
+
|
|
105
|
+
seen[identity] = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_identity_keys!(entry, identity_keys, kind)
|
|
109
|
+
missing_keys = identity_keys.reject { |key| entry.key?(key) }
|
|
110
|
+
return if missing_keys.empty?
|
|
111
|
+
|
|
112
|
+
raise ArgumentError,
|
|
113
|
+
"Schema preview #{kind} entry is missing identity keys: #{missing_keys.join(", ")}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_identity_values!(entry, identity_keys, kind)
|
|
117
|
+
name_key = identity_keys.first
|
|
118
|
+
unless entry[name_key].is_a?(String) && entry[name_key].present?
|
|
119
|
+
raise ArgumentError, "Schema preview #{kind} #{name_key} must be a non-empty String"
|
|
120
|
+
end
|
|
121
|
+
return if entry.values_at("entity_type", "scope", "parent_scope") ==
|
|
122
|
+
@hash.values_at("entity_type", "scope", "parent_scope") &&
|
|
123
|
+
TypedEAV::ScopeTuple.invariant_satisfied?(entry["scope"], entry["parent_scope"])
|
|
124
|
+
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"Schema preview #{kind} entry #{entry[name_key].inspect} identity must match " \
|
|
127
|
+
"the declared entity_type/scope/parent_scope target"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_field_entry!(entry)
|
|
131
|
+
validate_field_type!(entry)
|
|
132
|
+
validate_option_rows!(entry) if entry.key?("options_data")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_section_entry!(entry)
|
|
136
|
+
return if entry["name"].is_a?(String) && entry["name"].present?
|
|
137
|
+
|
|
138
|
+
raise ArgumentError, "Schema preview section #{entry["code"].inspect} name must be a non-empty String"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_field_type!(entry)
|
|
142
|
+
type = entry["type"]
|
|
143
|
+
unless type.is_a?(String) && type.present?
|
|
144
|
+
raise ArgumentError, "Schema preview field #{entry["name"].inspect} type must be a non-empty String"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
type_class = type.safe_constantize
|
|
148
|
+
return if type_class.is_a?(Class) && type_class <= TypedEAV::Field::Base
|
|
149
|
+
|
|
150
|
+
raise ArgumentError,
|
|
151
|
+
"Schema preview field #{entry["name"].inspect} type #{type.inspect} " \
|
|
152
|
+
"must be a TypedEAV::Field::Base subclass"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def validate_option_rows!(entry)
|
|
156
|
+
options = entry["options_data"]
|
|
157
|
+
unless options.is_a?(Array)
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"Schema preview field #{entry["name"].inspect} options_data must be an Array"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
seen_values = {}
|
|
163
|
+
options.each do |option|
|
|
164
|
+
validate_option_row!(entry, option)
|
|
165
|
+
|
|
166
|
+
value = option["value"]
|
|
167
|
+
if seen_values.key?(value)
|
|
168
|
+
raise ArgumentError,
|
|
169
|
+
"Duplicate option value #{value.inspect} for field #{entry["name"].inspect} in schema preview"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
seen_values[value] = true
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# rubocop:disable Metrics/AbcSize -- validation errors stay adjacent to
|
|
177
|
+
# the wire-format checks so malformed payloads fail before any query.
|
|
178
|
+
def validate_option_row!(entry, option)
|
|
179
|
+
unless option.is_a?(Hash)
|
|
180
|
+
raise ArgumentError, "Schema preview field #{entry["name"].inspect} option rows must be Hashes"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
missing_keys = OPTION_KEYS.reject { |key| option.key?(key) }
|
|
184
|
+
unless missing_keys.empty?
|
|
185
|
+
raise ArgumentError,
|
|
186
|
+
"Schema preview field #{entry["name"].inspect} option is missing key #{missing_keys.first.inspect}"
|
|
187
|
+
end
|
|
188
|
+
unless option["label"].is_a?(String) && option["label"].present?
|
|
189
|
+
raise ArgumentError,
|
|
190
|
+
"Schema preview field #{entry["name"].inspect} option label must be a non-empty String"
|
|
191
|
+
end
|
|
192
|
+
unless option["value"].is_a?(String) && option["value"].present?
|
|
193
|
+
raise ArgumentError,
|
|
194
|
+
"Schema preview field #{entry["name"].inspect} option value must be a non-empty String"
|
|
195
|
+
end
|
|
196
|
+
return if option["sort_order"].nil? || option["sort_order"].is_a?(Integer)
|
|
197
|
+
|
|
198
|
+
raise ArgumentError,
|
|
199
|
+
"Schema preview field #{entry["name"].inspect} option sort_order must be an Integer or nil"
|
|
200
|
+
end
|
|
201
|
+
# rubocop:enable Metrics/AbcSize
|
|
202
|
+
|
|
203
|
+
def preview_fields(entries, import_index)
|
|
204
|
+
entries.map { |entry| preview_field(entry, import_index.fields[field_identity(entry)]) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def preview_sections(entries, import_index)
|
|
208
|
+
entries.map { |entry| preview_section(entry, import_index.sections[section_identity(entry)]) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def preview_field(entry, existing)
|
|
212
|
+
identity = identity_hash(entry, "name")
|
|
213
|
+
options = existing ? option_diff(existing_entry(existing), entry) : option_diff({}, entry)
|
|
214
|
+
return added_preview(identity, options) unless existing
|
|
215
|
+
|
|
216
|
+
current = existing_entry(existing)
|
|
217
|
+
changes = field_changes(current, entry)
|
|
218
|
+
risks = field_risks(current, entry, options)
|
|
219
|
+
status = field_status(current, entry)
|
|
220
|
+
action = field_action(status)
|
|
221
|
+
|
|
222
|
+
entry_preview(
|
|
223
|
+
identity,
|
|
224
|
+
status: status,
|
|
225
|
+
action: action,
|
|
226
|
+
changes: changes,
|
|
227
|
+
options: options,
|
|
228
|
+
risks: risks,
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def field_status(current, incoming)
|
|
233
|
+
return "conflict" if current["type"] != incoming["type"]
|
|
234
|
+
return "unchanged" if current == incoming
|
|
235
|
+
|
|
236
|
+
"changed"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def field_action(status)
|
|
240
|
+
return "error" if status == "conflict"
|
|
241
|
+
return "unchanged" if status == "unchanged"
|
|
242
|
+
|
|
243
|
+
conflict_action
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def field_changes(current, incoming)
|
|
247
|
+
changes = attribute_changes(current, incoming, comparable_keys(current, incoming, FIELD_IDENTITY_KEYS))
|
|
248
|
+
same_options = current["options_data"] == incoming["options_data"] &&
|
|
249
|
+
current.key?("options_data") == incoming.key?("options_data")
|
|
250
|
+
return changes if same_options
|
|
251
|
+
|
|
252
|
+
changes["options_data"] = presence_change(current, incoming, "options_data")
|
|
253
|
+
changes
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def preview_section(entry, existing)
|
|
257
|
+
identity = identity_hash(entry, "code")
|
|
258
|
+
return entry_preview(identity, status: "added", action: "create", changes: {}, risks: []) unless existing
|
|
259
|
+
|
|
260
|
+
current = SchemaPortability.send(:export_section_entry, existing)
|
|
261
|
+
changes = attribute_changes(current, entry, comparable_keys(current, entry, SECTION_IDENTITY_KEYS))
|
|
262
|
+
if current == entry
|
|
263
|
+
entry_preview(identity, status: "unchanged", action: "unchanged", changes: changes, risks: [])
|
|
264
|
+
else
|
|
265
|
+
entry_preview(identity, status: "changed", action: conflict_action, changes: changes, risks: [])
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def existing_entry(existing)
|
|
270
|
+
SchemaPortability.send(:export_field_entry, existing)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def identity_hash(entry, key)
|
|
274
|
+
{
|
|
275
|
+
key => entry[key],
|
|
276
|
+
"entity_type" => entry["entity_type"],
|
|
277
|
+
"scope" => entry["scope"],
|
|
278
|
+
"parent_scope" => entry["parent_scope"],
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def field_identity(entry)
|
|
283
|
+
entry.values_at(*FIELD_IDENTITY_KEYS)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def section_identity(entry)
|
|
287
|
+
entry.values_at(*SECTION_IDENTITY_KEYS)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def added_preview(identity, options)
|
|
291
|
+
entry_preview(identity, status: "added", action: "create", changes: {}, options: options, risks: [])
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# rubocop:disable Metrics/ParameterLists -- these are the explicit
|
|
295
|
+
# dimensions of one stable result entry.
|
|
296
|
+
def entry_preview(identity, status:, action:, changes:, risks:, options: nil)
|
|
297
|
+
result = {
|
|
298
|
+
"identity" => identity.deep_dup,
|
|
299
|
+
"status" => status,
|
|
300
|
+
"action" => action,
|
|
301
|
+
"changes" => changes.deep_dup,
|
|
302
|
+
"risks" => risks,
|
|
303
|
+
}
|
|
304
|
+
result["options"] = options.deep_dup if options
|
|
305
|
+
result
|
|
306
|
+
end
|
|
307
|
+
# rubocop:enable Metrics/ParameterLists
|
|
308
|
+
|
|
309
|
+
def attribute_changes(current, incoming, keys)
|
|
310
|
+
keys.each_with_object({}) do |key, changes|
|
|
311
|
+
next if current[key] == incoming[key] && current.key?(key) == incoming.key?(key)
|
|
312
|
+
|
|
313
|
+
changes[key] = { "from" => current[key], "to" => incoming[key] }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def option_diff(current, incoming)
|
|
318
|
+
current_by_value = options_by_value(current)
|
|
319
|
+
incoming_by_value = options_by_value(incoming)
|
|
320
|
+
|
|
321
|
+
{
|
|
322
|
+
"added" => option_rows(incoming_by_value, incoming_by_value.keys - current_by_value.keys),
|
|
323
|
+
"removed" => option_rows(current_by_value, current_by_value.keys - incoming_by_value.keys),
|
|
324
|
+
"changed" => changed_option_rows(current_by_value, incoming_by_value),
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def options_by_value(entry)
|
|
329
|
+
Array(entry["options_data"]).index_by { |row| row["value"] }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def option_rows(rows_by_value, values)
|
|
333
|
+
values.sort.map { |value| rows_by_value[value] }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def changed_option_rows(current_by_value, incoming_by_value)
|
|
337
|
+
(current_by_value.keys & incoming_by_value.keys).sort.filter_map do |value|
|
|
338
|
+
next if current_by_value[value] == incoming_by_value[value]
|
|
339
|
+
|
|
340
|
+
{ "from" => current_by_value[value], "to" => incoming_by_value[value] }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def presence_change(current, incoming, key)
|
|
345
|
+
{ "from" => current[key], "to" => incoming[key] }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def comparable_keys(current, incoming, identity_keys)
|
|
349
|
+
(current.keys | incoming.keys) - identity_keys - ["options_data"]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def field_risks(current, incoming, options)
|
|
353
|
+
risks = []
|
|
354
|
+
risks << "type_change" if current["type"] != incoming["type"]
|
|
355
|
+
risks << "option_removal" unless options["removed"].empty?
|
|
356
|
+
risks << "required_false_to_true" if current["required"] == false && incoming["required"] == true
|
|
357
|
+
risks << "potentially_breaking_options" if current["options"] != incoming["options"]
|
|
358
|
+
risks << "default_value_change" if current["default_value_meta"] != incoming["default_value_meta"]
|
|
359
|
+
risks << "field_dependent_change" if current["field_dependent"] != incoming["field_dependent"]
|
|
360
|
+
risks
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def conflict_action
|
|
364
|
+
@on_conflict == :error ? "error" : @on_conflict.to_s
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def summary_for(entries)
|
|
368
|
+
{
|
|
369
|
+
"unchanged" => entries.count { |entry| entry["status"] == "unchanged" },
|
|
370
|
+
"added" => entries.count { |entry| entry["status"] == "added" },
|
|
371
|
+
"changed" => entries.count { |entry| entry["status"] == "changed" },
|
|
372
|
+
"conflicts" => entries.count { |entry| CONFLICT_STATUSES.include?(entry["status"]) },
|
|
373
|
+
"risks" => entries.sum { |entry| entry["risks"].size },
|
|
374
|
+
}
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
# rubocop:enable Metrics/ClassLength
|
|
378
|
+
end
|
|
379
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "schema_portability/import_index"
|
|
4
|
+
require_relative "schema_portability/preview"
|
|
4
5
|
|
|
5
6
|
module TypedEAV
|
|
6
7
|
# Export and import field + section definitions for an exact partition
|
|
@@ -86,6 +87,25 @@ module TypedEAV
|
|
|
86
87
|
}
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
# Compare an exported schema with the exact target partition without
|
|
91
|
+
# invoking the import pipeline. The result is a JSON-safe, read-only
|
|
92
|
+
# snapshot of the definitions currently in the database and the
|
|
93
|
+
# conditional action the requested conflict policy would take.
|
|
94
|
+
#
|
|
95
|
+
# `:error` marks divergent definitions as blocked, `:skip` predicts
|
|
96
|
+
# leaving them unchanged, and `:overwrite` predicts replacement. Type
|
|
97
|
+
# changes are always blocked because the importer refuses unsafe typed
|
|
98
|
+
# value conversions under every policy. Omitted target definitions are
|
|
99
|
+
# intentionally absent from the result: import_schema never deletes
|
|
100
|
+
# them. A preview is advisory and does not lock or reserve the target;
|
|
101
|
+
# custom validations and concurrent changes can still affect a later
|
|
102
|
+
# import. Every field/section entry must also repeat the exact
|
|
103
|
+
# entity_type/scope/parent_scope envelope identity; inconsistent
|
|
104
|
+
# payloads are rejected instead of being silently retargeted.
|
|
105
|
+
def preview_schema(hash, on_conflict: :error)
|
|
106
|
+
TypedEAV::SchemaPortability::Preview.new(hash, on_conflict: on_conflict).call
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
def import_schema(hash, on_conflict: :error)
|
|
90
110
|
validate_schema_version!(hash)
|
|
91
111
|
validate_conflict_policy!(on_conflict)
|
data/lib/typed_eav/version.rb
CHANGED
data/lib/typed_eav/versioned.rb
CHANGED
|
@@ -62,12 +62,14 @@ module TypedEAV
|
|
|
62
62
|
|
|
63
63
|
# Re-register with versioned: true. Preserve the existing types:
|
|
64
64
|
# restriction by reading the current Registry entry.
|
|
65
|
-
# has_typed_eav already
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# — shouldn't happen post-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
# has_typed_eav already registered the canonical Rails polymorphic
|
|
66
|
+
# name. Reuse it here so including this concern on an STI subclass
|
|
67
|
+
# enables versioning for the base entity_type actually stored on Value.
|
|
68
|
+
# If the entry doesn't exist (defensive — shouldn't happen post-
|
|
69
|
+
# has_typed_eav), default types to nil.
|
|
70
|
+
entity_type = polymorphic_name
|
|
71
|
+
existing = TypedEAV.registry.entities[entity_type] || {}
|
|
72
|
+
TypedEAV.registry.register(entity_type, types: existing[:types], versioned: true)
|
|
71
73
|
end
|
|
72
74
|
end
|
|
73
75
|
end
|
data/lib/typed_eav.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typed_eav
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dchuk
|
|
@@ -157,12 +157,16 @@ files:
|
|
|
157
157
|
- lib/typed_eav/field_deletion.rb
|
|
158
158
|
- lib/typed_eav/filter_query.rb
|
|
159
159
|
- lib/typed_eav/has_typed_eav.rb
|
|
160
|
+
- lib/typed_eav/has_typed_eav/dirty_tracking.rb
|
|
160
161
|
- lib/typed_eav/has_typed_eav/instance_methods.rb
|
|
161
162
|
- lib/typed_eav/partition.rb
|
|
163
|
+
- lib/typed_eav/partition/definition_batch.rb
|
|
162
164
|
- lib/typed_eav/query_builder.rb
|
|
163
165
|
- lib/typed_eav/registry.rb
|
|
166
|
+
- lib/typed_eav/scalar_query.rb
|
|
164
167
|
- lib/typed_eav/schema_portability.rb
|
|
165
168
|
- lib/typed_eav/schema_portability/import_index.rb
|
|
169
|
+
- lib/typed_eav/schema_portability/preview.rb
|
|
166
170
|
- lib/typed_eav/scope_tuple.rb
|
|
167
171
|
- lib/typed_eav/version.rb
|
|
168
172
|
- lib/typed_eav/versioned.rb
|