Class: Ecoportal::API::GraphQL::Diff::CommandSynthesizer

Inherits:
Object
  • Object
show all
Defined in:
lib/ecoportal/api/graphql/diff/command_synthesizer.rb

Overview

Turns a set of Diff::Change objects (from VersionDiff) into an ordered batch of built WorkflowCommand hashes — the replayable "commit".

changes = VersionDiff.new(v1, v2).changes synth = CommandSynthesizer.new(changes) synth.commands # => [{ editFieldConfiguration: ... }, { removeField: ... }, ...] synth.unsupported # => Change, ...

SCOPE — self-version diff by default. The ids carried by each Change are REAL server ids of the SAME object across two of its own versions, so they can be replayed as-is. For a cross-object diff (UAT<->PROD, page<->template) every id in the emitted commands must first be translated through a pairing map before replay.

RESOLVER — some edits address a TARGET node by an id the Change does not carry (a field move needs the destination section id; a section move needs the destination stage id). VersionDiff records those targets by their human key (section heading, stage name) because that is all a structural diff knows. Pass a resolver that maps resolve(kind, key) => id (e.g. the pairing engine's ledger, or a lookup built from the target doc) and those moves become supported. WITHOUT a resolver they stay UNSUPPORTED — the synthesiser never guesses a target id.

ORDERING — structure before children, and within a kind: removes, then adds, then moves, then edits. Structural creates (stages, sections, fields) precede the option-level edits that depend on them, so a straight left-to-right replay stays valid.

SAFETY — a change with no faithful command is NEVER guessed. It is collected into unsupported for the caller to inspect. In particular a field type change has no edit command in the schema (there is no editFieldType), so it is reported as unsupported rather than silently rebuilt (a rebuild would drop the field's data and history).

Constant Summary collapse

KIND_ORDER =

Emit order: structure first (stages -> sections -> fields), then options; and within a kind, removes -> adds -> moves -> edits, so a straight left-to-right replay stays valid.

{ stage: 0, section: 1, field: 2, field_config: 3, gauge_stop: 4, option: 5 }.freeze
OP_ORDER =
{ removed: 0, added: 1, moved: 2, changed: 3 }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(changes, resolver: nil, thread_placeholders: false) ⇒ CommandSynthesizer

Returns a new instance of CommandSynthesizer.

Parameters:

  • changes (Array<Change>)
  • resolver (#resolve, nil) (defaults to: nil)

    maps (kind, human-key) -> target id for move commands. Any object answering resolve(kind, key) (returning an id or nil). When nil, moves that need a target id are reported as unsupported.

  • thread_placeholders (Boolean) (defaults to: false)

    when true, structural creates (addStage/addSection/ addField) emit a client-chosen placeholderId, and any later command in the SAME batch that references a node created here substitutes that placeholder for the (target-invalid) source id. This keeps add-then-reference sequences self-consistent within one executeWorkflowCommands call. Off by default (self-version replay uses the real ids).



49
50
51
52
53
54
# File 'lib/ecoportal/api/graphql/diff/command_synthesizer.rb', line 49

def initialize(changes, resolver: nil, thread_placeholders: false)
  @changes             = Array(changes)
  @resolver            = resolver
  @thread_placeholders = thread_placeholders
  @unsupported         = []
end

Instance Method Details

#commandsObject

Ordered Array of built command hashes (each { commandKey => {..input..} }).



57
58
59
# File 'lib/ecoportal/api/graphql/diff/command_synthesizer.rb', line 57

def commands
  @commands ||= build_all
end

#unsupportedObject

Changes that could not be mapped to a command. Populated as a side effect of commands.



62
63
64
65
# File 'lib/ecoportal/api/graphql/diff/command_synthesizer.rb', line 62

def unsupported
  commands
  @unsupported
end