Module: TypedEAV::SchemaPortability

Defined in:
lib/typed_eav/schema_portability.rb,
lib/typed_eav/schema_portability/import_index.rb

Overview

Export and import field + section definitions for an exact partition tuple. Value rows are intentionally out of scope.

Defined Under Namespace

Classes: ImportIndex

Class Method Summary collapse

Class Method Details

.export_schema(entity_type:, scope: nil, parent_scope: nil) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/typed_eav/schema_portability.rb', line 10

def export_schema(entity_type:, scope: nil, parent_scope: nil)
  fields = TypedEAV::Field::Base
           .where(entity_type: entity_type, scope: scope, parent_scope: parent_scope)
           .includes(:field_options)
           .order(:sort_order)
           .map { |field| export_field_entry(field) }

  sections = TypedEAV::Section
             .where(entity_type: entity_type, scope: scope, parent_scope: parent_scope)
             .order(:sort_order)
             .map { |section| export_section_entry(section) }

  {
    "schema_version" => 1,
    "entity_type" => entity_type,
    "scope" => scope,
    "parent_scope" => parent_scope,
    "fields" => fields,
    "sections" => sections,
  }
end

.export_snapshot_schema(entity_type:, scope: nil, parent_scope: nil) ⇒ Hash

Lean, restore-oriented projection of the field schema for a partition tuple. Sibling to export_schema — same partition filter, narrower per-field surface, no sections, no partition-identity keys.

The envelope is:

{
"snapshot_schema_version" => 1,
"fields" => [ <snapshot_field_entry>, ... ]   # ordered by sort_order
}

The snapshot_schema_version integer will be bumped explicitly when the inner per-field shape evolves in a non-additive way — it is NOT frozen forever. Consumers should branch on the version to handle cross-version snapshots.

Each per-field entry is a strict subset of the full export_field_entry shape:

{
"name" => field.name,
"field_type_name" => field.field_type_name,
"required" => field.required,
"sort_order" => field.sort_order,
"options" => field.options,
"options_data" => [...]   # ONLY present when field.optionable?
}

Omitted vs the full schema export: entity_type, scope, parent_scope, type (the AR STI class name), field_dependent, and default_value_meta. Non-optionable fields omit options_data entirely (absent, not nil, not an empty array).

The field_type_name value is the documented field-type dispatch identifier — robust to namespace relocations of the field class because it strips the namespace via demodulize before underscore-ing. It is NOT robust to renames of the leaf class itself: Field::Select"select", but renaming the class to Field::Status would change the dispatch identifier to "status".

Parameters:

  • entity_type (String)

    host AR model class name (e.g. "Contact")

  • scope (String, nil) (defaults to: nil)

    first partition axis

  • parent_scope (String, nil) (defaults to: nil)

    second partition axis

Returns:

  • (Hash)

    versioned snapshot envelope



76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/typed_eav/schema_portability.rb', line 76

def export_snapshot_schema(entity_type:, scope: nil, parent_scope: nil)
  fields = TypedEAV::Field::Base
           .where(entity_type: entity_type, scope: scope, parent_scope: parent_scope)
           .includes(:field_options)
           .order(:sort_order)
           .map { |field| export_snapshot_field_entry(field) }

  {
    "snapshot_schema_version" => 1,
    "fields" => fields,
  }
end

.import_schema(hash, on_conflict: :error) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/typed_eav/schema_portability.rb', line 89

def import_schema(hash, on_conflict: :error)
  validate_schema_version!(hash)
  validate_conflict_policy!(on_conflict)

  result = { "created" => 0, "updated" => 0, "skipped" => 0, "unchanged" => 0, "errors" => [] }
  field_entries = Array(hash["fields"])
  section_entries = Array(hash["sections"])

  TypedEAV::Field::Base.transaction do
    import_index = ImportIndex.new(field_entries, section_entries)

    field_entries.each do |entry|
      import_field_entry(entry, on_conflict, result, import_index)
    end

    section_entries.each do |entry|
      import_section_entry(entry, on_conflict, result, import_index)
    end
  end

  result
end