Class: Etna::Clients::Magma::ModelSynchronizationWorkflow

Inherits:
Struct
  • Object
show all
Defined in:
lib/etna/clients/magma/workflows/model_synchronization_workflow.rb

Overview

Note! These workflows are not perfectly atomic, nor perfectly synchronized due to nature of the backend. These primitives are best effort locally synchronized, but cannot defend the backend or simultaneous system updates.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#model_nameObject

Returns the value of attribute model_name

Returns:

  • (Object)

    the current value of model_name



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def model_name
  @model_name
end

#plan_onlyObject

Returns the value of attribute plan_only

Returns:

  • (Object)

    the current value of plan_only



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def plan_only
  @plan_only
end

#renamesObject

Returns the value of attribute renames

Returns:

  • (Object)

    the current value of renames



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def renames
  @renames
end

#source_modelsObject

Returns the value of attribute source_models

Returns:

  • (Object)

    the current value of source_models



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def source_models
  @source_models
end

#target_clientObject

Returns the value of attribute target_client

Returns:

  • (Object)

    the current value of target_client



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def target_client
  @target_client
end

#target_projectObject

Returns the value of attribute target_project

Returns:

  • (Object)

    the current value of target_project



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def target_project
  @target_project
end

#update_blockObject

Returns the value of attribute update_block

Returns:

  • (Object)

    the current value of update_block



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def update_block
  @update_block
end

#use_versionsObject

Returns the value of attribute use_versions

Returns:

  • (Object)

    the current value of use_versions



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def use_versions
  @use_versions
end

#validateObject

Returns the value of attribute validate

Returns:

  • (Object)

    the current value of validate



9
10
11
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 9

def validate
  @validate
end

Class Method Details

.from_api_source(source_project:, source_client:, **kwds) ⇒ Object



102
103
104
105
106
107
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 102

def self.from_api_source(source_project:, source_client:, **kwds)
  self.new(
      source_models: source_client.retrieve(RetrievalRequest.new(project_name: source_project, model_name: 'all')).models,
      **kwds
  )
end

.models_affected_by(action) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 284

def self.models_affected_by(action)
  case action
  when Etna::Clients::Magma::RenameAttributeAction
    [action.model_name]
  when Etna::Clients::Magma::UpdateAttributeAction
    [action.model_name]
  when Etna::Clients::Magma::AddAttributeAction
    [action.model_name]
  when Etna::Clients::Magma::AddModelAction
    [action.model_name]
  when Etna::Clients::Magma::AddLinkAction
    action.links.map(&:model_name)
  else
    []
  end
end

Instance Method Details



45
46
47
48
49
50
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 45

def copy_link_into_target(link, reciprocal)
  attr = target_models.build_model(link.model_name).build_template.build_attributes.build_attribute(link.attribute_name)
  attr.attribute_type = link.type
  attr.name = attr.attribute_name = link.attribute_name
  attr.link_model_name = reciprocal.model_name
end

#ensure_model(model_name) ⇒ Object

Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle. This method, and it’s partner prepare_parent, should never call into any re-entrant or potentially cyclical method, like ensure_model_tree.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 244

def ensure_model(model_name)
  return unless (source_model = source_models.model(model_name))
  target_model_name = target_of_source(model_name)
  return if target_models.model_keys.include?(target_model_name)

  template = source_model.template

  add_model_action = AddModelAction.new(
      {
          model_name: target_model_name,
          identifier: template.identifier,
      }
  )

  parents = template.attributes.all.select { |a| a.attribute_type == AttributeType::PARENT }
  parent_model_name, parent_link_type = prepare_parent(model_name, template, parents)
  unless parent_model_name.nil?
    add_model_action.parent_model_name = parent_model_name
    add_model_action.parent_link_type = parent_link_type
  end

  queue_update(add_model_action)
end

#ensure_model_attribute(model_name, attribute_name) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 170

def ensure_model_attribute(model_name, attribute_name)
  return unless (model = source_models.model(model_name))
  return unless (source_attribute = model.template.attributes.attribute(attribute_name))

  target_model_name = target_of_source(model_name)
  target_attribute, target_attribute_name = ensure_model_attribute_target_rename(model_name, attribute_name)

  if target_attribute.nil?
    add_attribute = AddAttributeAction.new(
        model_name: target_model_name,
        attribute_name: target_attribute_name,
    )

    Attribute.copy(source_attribute, add_attribute)
    queue_update(add_attribute)
  else
    # If there are is no diff, don't produce an action.
    target_attribute = Attribute.new(target_attribute.raw)
    target_attribute.set_field_defaults!

    source_attribute = Attribute.new(source_attribute.raw)
    source_attribute.set_field_defaults!

    if !source_attribute.is_edited?(target_attribute)
      return
    end

    update_attribute = UpdateAttributeAction.new(
        model_name: target_model_name,
        attribute_name: target_attribute_name,
    )

    Attribute.copy(source_attribute, update_attribute, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
    queue_update(update_attribute)
  end
end

#ensure_model_attribute_target_rename(model_name, attribute_name) ⇒ Object

Returns a tuple of the target’s attribute, post rename if necessary, if it exists, and the name of the target attribute cases here:

  1. There is no rename for the attribute

a. There target attribute already exists -> [target_attribute, attribute_name]
b. The target attribute does not exist -> [nil, attribute_name]
  1. There is an expected rename from the source

a. The target has neither the renamed attribute or the original attribute -> [nil, new_attribute_name]
b. The target has the renamed attribute already -> [renamed_attribute, new_attribute_name]
c. The target has the source attribute, which is not yet renamed. -> [renamed_attribute, new_attribute_name]


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 216

def ensure_model_attribute_target_rename(model_name, attribute_name)
  target_model_name = target_of_source(model_name)
  target_attribute_name = target_attribute_of_source(model_name, attribute_name)
  return nil unless (target_model = target_models.model(target_model_name))

  target_original_attribute = target_model.template.attributes.attribute(target_attribute_name)

  if renames && (attribute_renames = renames[model_name]) && (new_name = attribute_renames[attribute_name])
    new_name = target_attribute_of_source(model_name, new_name)

    unless target_model.template.attributes.attribute_keys.include?(new_name)
      if target_original_attribute
        rename = RenameAttributeAction.new(model_name: target_model_name, attribute_name: target_attribute_name, new_attribute_name: new_name)
        queue_update(rename)
      else
        return [nil, new_name]
      end
    end

    return [target_model.template.attributes.attribute(new_name), new_name]
  end

  [target_original_attribute, target_attribute_name]
end


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 147

def ensure_model_link(model_name, link_model_name, attribute_name, link_attribute_name)
  return unless (model = source_models.model(model_name))
  return unless (source_attribute = model.template.attributes.attribute(attribute_name))

  return unless (link_model = source_models.model(link_model_name))
  return unless (reciprocal = link_model.template.attributes.attribute(link_attribute_name))

  target_model_name = target_of_source(model_name)
  target_link_model_name = target_of_source(link_model_name)

  target_attributes = target_models.model(target_model_name).template.attributes
  return if target_attributes.attribute_keys.include?(target_link_model_name)
  return if target_attributes.attribute_keys.include?(reciprocal.attribute_name)

  # skip non-links for circular references so they don't get added twice
  return if link_model_name == model_name && reciprocal.attribute_type != 'link'

  add_link = AddLinkAction.new
  add_link.links << AddLinkDefinition.new(model_name: target_model_name, attribute_name: attribute_name, type: source_attribute.attribute_type)
  add_link.links << AddLinkDefinition.new(model_name: target_link_model_name, attribute_name: reciprocal.attribute_name, type: reciprocal.attribute_type)
  queue_update(add_link)
end

#ensure_model_tree(model_name, seen = Set.new) ⇒ Object

Potentially cyclical, protected against re-entry via the seen cache. Establishes the link attributes in a given model graph.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 121

def ensure_model_tree(model_name, seen = Set.new)
  return unless (source_model = source_models.model(model_name))
  return if seen.include?(model_name)
  seen.add(model_name)
  ensure_model(model_name)

  attributes = source_model.template.attributes

  attributes.all.each do |attribute|
    # Don't copy or update parent links.  Once a model has been setup with a parent someway.
    unless attribute.attribute_type == AttributeType::PARENT
      if attribute.link_model_name
        ensure_model(attribute.link_model_name)
        ensure_model_link(model_name, attribute.link_model_name, attribute.attribute_name, attribute.link_attribute_name)
      else
        ensure_model_attribute(model_name, attribute.attribute_name)
      end
    end

    # Even if it's a parent node, however, we still want to cascade the tree expansion to all links.
    unless attribute.link_model_name.nil?
      ensure_model_tree(attribute.link_model_name, seen)
    end
  end
end

#execute_planned!Object



31
32
33
34
35
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 31

def execute_planned!
  planned_actions.each do |action|
    execute_update(action)
  end
end

#execute_update(action) ⇒ Object



37
38
39
40
41
42
43
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 37

def execute_update(action)
  update = UpdateModelRequest.new(project_name: self.target_project)
  update_block.call(action) if update_block
  update.add_action(action)
  @target_models = nil
  target_client.update_model(update)
end

#plan_update(action) ⇒ Object

Applies the given action to the source models and ‘plans’ its execution.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 53

def plan_update(action)
  case action
  when UpdateAttributeAction
    attribute_update = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
    Attribute.copy(action, attribute_update, attributes: Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES)
  when AddAttributeAction
    new_attribute = target_models.build_model(action.model_name).build_template.build_attributes.build_attribute(action.attribute_name)
    Attribute.copy(action, new_attribute)
  when AddLinkAction
    first_link = action.links[0]
    second_link = action.links[1]
    copy_link_into_target(first_link, second_link)
    copy_link_into_target(second_link, first_link)
  when AddModelAction
    template = target_models.build_model(action.model_name).build_template
    template.name = action.model_name
    template.identifier = action.identifier
    template.parent = action.parent_model_name

    child_link = AddLinkDefinition.new(type: AttributeType::PARENT, model_name: template.name, attribute_name: template.parent)
    parent_link = AddLinkDefinition.new(type: action.parent_link_type, model_name: template.parent, attribute_name: template.name)

    copy_link_into_target(child_link, parent_link)
    copy_link_into_target(parent_link, child_link)

    ['created_at', 'updated_at'].each do |time_attr_name|
      template.build_attributes.build_attribute(time_attr_name).tap do |attr|
        attr.attribute_type = Etna::Clients::Magma::AttributeType::DATE_TIME
        attr.attribute_name = time_attr_name
      end
    end

    if action.parent_link_type != Etna::Clients::Magma::AttributeType::TABLE
      template.build_attributes.build_attribute(template.identifier).tap do |attr|
        attr.attribute_name = template.identifier
        attr.attribute_type = Etna::Clients::Magma::AttributeType::IDENTIFIER
      end
    end
  when RenameAttributeAction
    attributes = target_models.model(action.model_name).template.attributes
    attributes.raw[action.new_attribute_name] = attributes.raw.delete(action.attribute_name)
  else
    raise "Unexpected plan_action #{action}"
  end

  planned_actions << action
end

#planned_actionsObject



19
20
21
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 19

def planned_actions
  @planned_actions ||= []
end

#prepare_parent(model_name, template, parents) ⇒ Object

Non cyclical, non re-entrant due to the requirement that parents cannot form a cycle. This method, and it’s partner and ensure_model, should never call into any re-entrant or potentially cyclical method, like ensure_model_tree.



271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 271

def prepare_parent(model_name, template, parents)
  return [nil, nil] if parents.empty?
  return [nil, nil] unless (parent_model = source_models.model(template.parent))
  return [nil, nil] unless (child_attribute = parent_model.template.attributes.attribute(model_name))

  ensure_model(template.parent)

  [
      target_of_source(template.parent),
      child_attribute.attribute_type
  ]
end

#queue_update(action) ⇒ Object



23
24
25
26
27
28
29
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 23

def queue_update(action)
  if plan_only
    plan_update(action)
  else
    execute_update(action)
  end
end

#target_attribute_of_source(model_name, attribute_name) ⇒ Object

Subclass and override when the source <-> target attribute mapping is not 1:1.



115
116
117
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 115

def target_attribute_of_source(model_name, attribute_name)
  attribute_name
end

#target_modelsObject



13
14
15
16
17
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 13

def target_models
  @target_models ||= begin
    target_client.retrieve(RetrievalRequest.new(project_name: self.target_project, model_name: 'all')).models
  end
end

#target_of_source(model_name) ⇒ Object

Subclass and override when the source <-> target mapping is not 1:1.



110
111
112
# File 'lib/etna/clients/magma/workflows/model_synchronization_workflow.rb', line 110

def target_of_source(model_name)
  model_name
end