Module: TypedEAV::SchemaPortability

Defined in:
lib/typed_eav/schema_portability.rb,
lib/typed_eav/schema_portability/preview.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, Preview

Class Method Summary collapse

Class Method Details

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



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

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



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

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



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/typed_eav/schema_portability.rb', line 109

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

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

Compare an exported schema with the exact target partition without invoking the import pipeline. The result is a JSON-safe, read-only snapshot of the definitions currently in the database and the conditional action the requested conflict policy would take.

:error marks divergent definitions as blocked, :skip predicts leaving them unchanged, and :overwrite predicts replacement. Type changes are always blocked because the importer refuses unsafe typed value conversions under every policy. Omitted target definitions are intentionally absent from the result: import_schema never deletes them. A preview is advisory and does not lock or reserve the target; custom validations and concurrent changes can still affect a later import. Every field/section entry must also repeat the exact entity_type/scope/parent_scope envelope identity; inconsistent payloads are rejected instead of being silently retargeted.



105
106
107
# File 'lib/typed_eav/schema_portability.rb', line 105

def preview_schema(hash, on_conflict: :error)
  TypedEAV::SchemaPortability::Preview.new(hash, on_conflict: on_conflict).call
end