eco-helpers 2.5.5 → 2.5.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d241b7bc57bd81e9b3a6b1d71ecab5f7530b7b6df176301adc21584722f2b247
4
- data.tar.gz: a8beafa1e6cd1e3425291121c55b78fe144f155021ab149148fcc6ce35569984
3
+ metadata.gz: ed268fb73bb35106da813f51a7b9469d0ee0fe167cee520ac44f98fed22c622b
4
+ data.tar.gz: c6b6e0f72037f2a1dac9313293317a78a4d25c52568c7577f88efdafb37d7994
5
5
  SHA512:
6
- metadata.gz: 2bad589998061077bac0f6adb500fdbb9402b77578da68bf43a84f83101a2bf862213acb00707a363f5583c47ec94a876c367360c562043865e573232b272c4c
7
- data.tar.gz: 7c33f358f5cbb7ab0b3118d7b0b20e86a9f686f2e1f49216e23bea5ab504ca4bb0bbb2eeab8bb37daf0d11b997af0160212ff7ab2d1fe382fa05a6b354529f48
6
+ metadata.gz: 21d9f1703574866e548daa22f1e196b237a8415fb750fe53bb6c4cd63e5fa90002a9e2d5f90e5f3aaca769f639e6d9c02f40b0e7a7a123a15965af41bff91828
7
+ data.tar.gz: a02e32a2945f0e4e7a5780e340e8ef400e434d00f2cf44d9969d8573bf84e94c5d35c4c0319e205e351837dc016eb9e389759db3d2291ceab493943af597170b
data/CHANGELOG.md CHANGED
@@ -1,12 +1,31 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [2.5.6] - 2023-08-xx
4
+ ## [2.5.7] - 2023-08-xx
5
5
 
6
6
  ### Added
7
7
  ### Changed
8
8
  ### Fixed
9
9
 
10
+ ## [2.5.6] - 2023-08-14
11
+
12
+ ### Changed
13
+ - `Eco::API::Session::Config::Tagtree#live_tree` remove memmoize tree: it now always trigger a query to the back-end
14
+ - Better messaging to know what's going on.
15
+
16
+ ### Fixed
17
+ - `Eco::API::Session#live_tree` was not forwarding target structure id `id`
18
+ - `Eco::Data::Locations::NodeLevel#override_lower_levels`
19
+ - By **default** should only override empty upper levels
20
+ - `Eco::Data::Locations::NodeLevel::Cleaner`
21
+ - `#tidy_nodes` when **gap** (merged parent) identified, `level` is still correct
22
+ - `Eco::Data::Locations::NodeLevel`
23
+ - `#nodes_from_csv` correctly identify parent with big gaps
24
+ - This fix ensures a correct normalization
25
+ - `Eco::API::UseCases::GraphQL::Samples::Location::Command::Result`
26
+ - **Batch remap tags** _built_ requires full path to keep from-filter specificity
27
+ - This change also wraps into a new class the remap tags table.
28
+
10
29
  ## [2.5.5] - 2023-08-03
11
30
 
12
31
  ### Added
data/eco-helpers.gemspec CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_dependency 'ecoportal-api', '>= 0.9.4', '< 0.10'
34
34
  spec.add_dependency 'ecoportal-api-v2', '>= 1.1.3', '< 1.2'
35
- spec.add_dependency 'ecoportal-api-graphql', '>= 0.3.9', '< 0.4'
35
+ spec.add_dependency 'ecoportal-api-graphql', '>= 0.3.10', '< 0.4'
36
36
  spec.add_dependency 'aws-sdk-s3', '>= 1.83.0', '< 2'
37
37
  spec.add_dependency 'aws-sdk-ses', '>= 1.36.0', '< 2'
38
38
  spec.add_dependency 'dotenv', '>= 2.7.6', '< 3'
@@ -12,6 +12,7 @@ module Eco
12
12
  # @return [Eco::API::Organization::TagTree]
13
13
  def scope_tree(enviro: nil, include_archived: true, raise_on_missing: true)
14
14
  return @tagtree if instance_variable_defined?(:@tagtree) && @tagtree.enviro == enviro
15
+
15
16
  if tree_file = self.file
16
17
  if (tree = file_manager.load_json(tree_file)) && !tree.empty?
17
18
  @tagtree = Eco::API::Organization::TagTree.new(tree, enviro: enviro)
@@ -34,7 +35,14 @@ module Eco
34
35
  # Among all the locations structures it selects the one with more location nodes
35
36
  # If `id` is provided, it only retrieves this locations structure.
36
37
  def live_tree(id: nil, enviro: nil, include_archived: false, **kargs, &block)
37
- return @live_tree if instance_variable_defined?(:@live_tree) && @live_tree.enviro == enviro
38
+ existing_cache = !!@live_tree
39
+ first_load = !existing_cache
40
+
41
+ target_change = existing_cache && id && @live_tree.id != id
42
+ enviro_change = existing_cache && enviro && @live_tree.enviro != enviro
43
+
44
+ switching_target = existing_cache && (target_change || enviro_change)
45
+ refresh_cache = existing_cache && !switching_target
38
46
 
39
47
  kargs = {
40
48
  enviro: enviro,
@@ -45,7 +53,6 @@ module Eco
45
53
  args = { id: id }.merge(kargs)
46
54
  @live_tree = live_tree_get(**args, &block)
47
55
  else
48
- kargs
49
56
  trees = live_trees(**kargs, &block)
50
57
  @live_tree = trees.reject do |tree|
51
58
  tree.empty?
@@ -54,8 +61,16 @@ module Eco
54
61
  end
55
62
  end.tap do |tree|
56
63
  if tree
57
- msg = "Using LIVE LOCATIONS Structure: '#{tree.name}' (#{tree.count} nodes)"
58
- session_logger.info(msg)
64
+ msg = "LIVE LOCATIONS Structure (#{tree.id}): '#{tree.name}' (#{tree.count} nodes)"
65
+ if first_load
66
+ session_logger.info("Using #{msg}")
67
+ elsif switching_target
68
+ session_logger.info("Switched to #{msg}")
69
+ else # refresh_cache
70
+ session_logger.debug("Reloading #{msg}")
71
+ end
72
+ else
73
+ session_logger.info "Could not retrive live tree (#{id})"
59
74
  end
60
75
  end
61
76
  end
@@ -74,6 +89,7 @@ module Eco
74
89
  }.merge(kargs)
75
90
 
76
91
  return nil unless tree = graphql.currentOrganization.locationStructure(**kargs, &block)
92
+
77
93
  args = { enviro: enviro, id: tree.id, name: tree.name }
78
94
  Eco::API::Organization::TagTree.new(tree.treeify, **args)
79
95
  end
@@ -250,11 +250,7 @@ module Eco
250
250
  # @note it requires graphql connection configuration parameters
251
251
  # @return [Eco::API::Organization::TagTree]
252
252
  def live_tree(id: nil, enviro: nil, **kargs, &block)
253
- if id
254
- tagtree_config.live_tree_get(id: id, enviro: enviro, **kargs, &block)
255
- else
256
- tagtree_config.live_tree(enviro: enviro, **kargs, &block)
257
- end
253
+ tagtree_config.live_tree(id: id, enviro: enviro, **kargs, &block)
258
254
  end
259
255
 
260
256
  # @return [Eco::API::Organization::PolicyGroups]
@@ -46,7 +46,7 @@ module Eco
46
46
 
47
47
  # @see Eco::API::Session::Config#live_tree
48
48
  def live_tree(id: nil, include_archived: false, **kargs, &block)
49
- config.live_tree(id: nil, include_archived: include_archived, enviro: enviro, **kargs, &block)
49
+ config.live_tree(id: id, include_archived: include_archived, enviro: enviro, **kargs, &block)
50
50
  end
51
51
 
52
52
  # @see Eco::API::Session::Config#schemas
@@ -0,0 +1,66 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers::Location
2
+ class TagsRemap
3
+ class TagsMap
4
+ attr_reader :from, :to
5
+
6
+ def initialize(from, to)
7
+ @from = TagsSet.new(from)
8
+ @to = TagsSet.new(to)
9
+ end
10
+
11
+ def to_csv_row
12
+ [from.to_s, to.to_s]
13
+ end
14
+
15
+ # @note to create a stable sort we assume `self` is a sorted element
16
+ def <=>(other)
17
+ return -1 if maps? && !other.maps?
18
+ return 1 if !maps? && other.maps?
19
+ return -1 if rename? && other.move?
20
+ return 1 if move? && other.rename?
21
+ return -1 if rename? && other.rename?
22
+ # both are being moved (specific/long mappings first)
23
+ return 1 if from.subset_of?(other.from)
24
+ return -1 if from.superset_of?(other.from)
25
+ return -1 if (from & other.from).empty?
26
+ return -1 if from.length >= other.from.length
27
+ return 1 if from.length < other.from.length
28
+ -1
29
+ end
30
+
31
+ # @note to create a stable sort we assume `self` is a sorted element
32
+ def goes_before?(other)
33
+ (self <=> other) == -1
34
+ end
35
+
36
+ # @note to create a stable sort we assume `self` is a sorted element
37
+ def goes_after?(other)
38
+ (self <=> other) == 1
39
+ end
40
+
41
+ def both?(&block)
42
+ [from, to].all?(&block)
43
+ end
44
+
45
+ def any?(&block)
46
+ [from, to].any?(&block)
47
+ end
48
+
49
+ def maps?
50
+ return false if any?(&:empty?)
51
+ return false if from == to
52
+ true
53
+ end
54
+
55
+ def rename?
56
+ return false unless maps?
57
+ both? {|set| set.length == 1}
58
+ end
59
+
60
+ def move?
61
+ return false unless maps?
62
+ !rename?
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,95 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers::Location
2
+ class TagsRemap
3
+ class TagsSet
4
+ class << self
5
+ def attr_compare(*attrs)
6
+ attrs.each do |attr|
7
+ meth = "#{attr}".to_sym
8
+ define_method meth do |value|
9
+ set.send(meth, to_set(value))
10
+ end
11
+ end
12
+ end
13
+ def attr_operate(*attrs)
14
+ attrs.each do |attr|
15
+ meth = "#{attr}".to_sym
16
+ define_method meth do |value|
17
+ self.class.new(set.send(meth, to_set(value)))
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ COMPARE_METHODS = %i[< <= == > >=].freeze
24
+ OPERATE_METHIDS = %i[& + - ^].freeze
25
+
26
+ attr_compare *COMPARE_METHODS
27
+ attr_operate *OPERATE_METHIDS
28
+
29
+ attr_reader :tags, :set
30
+
31
+ def initialize(tags)
32
+ @ini_tags = value_to_a(tags)
33
+ @set = to_set(tags)
34
+ end
35
+
36
+ def length
37
+ set.length
38
+ end
39
+
40
+ def tags
41
+ ini_tags.compact.map(&:upcase)
42
+ end
43
+
44
+ def to_a
45
+ tags
46
+ end
47
+
48
+ def to_s
49
+ tags.join('|')
50
+ end
51
+
52
+ def empty?
53
+ set.empty?
54
+ end
55
+
56
+ def include?(value)
57
+ value = value.to_s.strip
58
+ return false if value.empty?
59
+ set.include?(value)
60
+ end
61
+
62
+ def include_any?(value)
63
+ value.to_a.any? {|tag| self.include?(tag)}
64
+ end
65
+
66
+ def subset_of?(value)
67
+ set.subset?(to_set(value))
68
+ end
69
+
70
+ def superset_of?(value)
71
+ set.superset?(to_set(value))
72
+ end
73
+
74
+ protected
75
+
76
+ def ini_tags
77
+ @ini_tags
78
+ end
79
+
80
+ private
81
+
82
+ def value_to_a(value)
83
+ return value.ini_tags.dup if value.is_a?(self.class)
84
+ return value.dup if value.is_a?(Array)
85
+ return value.to_a if value.is_a?(Set)
86
+ raise ArgumentError, "Expecting #{self.class}, Set or Array. Given: #{value.class}"
87
+ end
88
+
89
+ def to_set(value)
90
+ values = value_to_a(value)
91
+ Set.new.merge(values.compact.map(&:upcase))
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,88 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers::Location
2
+ class TagsRemap
3
+ def self.correct_pair?(pair)
4
+ correct_pair = pair.is_a?(Array)
5
+ correct_pair && pair.length == 2
6
+ end
7
+
8
+ attr_reader :src_maps
9
+ attr_reader :from, :to
10
+
11
+ def each(&block)
12
+ return to_enum(:each) unless block
13
+ src_maps.each(&block)
14
+ end
15
+
16
+ def to_csv(filename)
17
+ CSV.open(filename, "w") do |fd|
18
+ fd << ["src_tags", "dst_tags"]
19
+ each do |tags_map|
20
+ fd << tags_map.to_csv_row
21
+ end
22
+ end
23
+ end
24
+
25
+ def to_s
26
+ "".tap do |str|
27
+ each.map do |tags_map|
28
+ from, to = tags_map.to_csv_row
29
+ str << " • from: #{from}\n"
30
+ str << " • to: #{to}\n"
31
+ end
32
+ end
33
+ end
34
+
35
+ # Sorts the maps in the correct order
36
+ # @note it assumes that:
37
+ # 1. renames go before moving nodes
38
+ # 2. moving nodes does not include a rename
39
+ # 3. moving nodes includes all the path on both, source and destination tags
40
+ # @note DISABLED: the order should be that in what the operations happened!
41
+ # An important thing is that all the path is always included in the mappings.
42
+ # * The only way that this would work is to apply upper (previous) tags_maps
43
+ # to subsequent ones. But the order should still be kept!
44
+ # def sorted_maps
45
+ # [].tap do |sorted|
46
+ # src_maps.each do |curr_map|
47
+ # pos = nil
48
+ # sorted.each_with_index.reverse_each do |prev_map, idx|
49
+ # pos = idx + 1 if prev_map.goes_before?(curr_map)
50
+ # break if pos
51
+ # end
52
+ # sorted.insert(pos || 0, curr_map)
53
+ # end
54
+ # end
55
+ # end
56
+
57
+ def empty?
58
+ src_maps.empty?
59
+ end
60
+
61
+ def <<(pair)
62
+ raise ArgumentError, "Expecting pair of Array in Array. Given: #{pair}" unless self.class.correct_pair?(pair)
63
+ add(*pair)
64
+ end
65
+
66
+ def add(from, to)
67
+ raise ArgumentError, "Expecting Array. Given: #{from.class}" unless from.is_a?(Array)
68
+ raise ArgumentError, "Expecting Array. Given: #{to.class}" unless to.is_a?(Array)
69
+ new_src TagsMap.new(from, to)
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :src_maps
75
+ attr_reader :from_key, :to_key
76
+
77
+ def new_src(tags_map)
78
+ src_maps << tags_map
79
+ end
80
+
81
+ def src_maps
82
+ @src_maps ||= []
83
+ end
84
+ end
85
+ end
86
+
87
+ require_relative 'tags_remap/tags_map'
88
+ require_relative 'tags_remap/tags_set'
@@ -4,4 +4,5 @@ module Eco::API::UseCases::GraphQL::Helpers
4
4
  end
5
5
 
6
6
  require_relative 'location/base'
7
+ require_relative 'location/tags_remap'
7
8
  require_relative 'location/command'
@@ -18,6 +18,10 @@ class Eco::API::UseCases::GraphQL::Samples::Location
18
18
  Eco::API::UseCases::GraphQL::Helpers::Location::Command::Results
19
19
  end
20
20
 
21
+ def tags_remap_class
22
+ Eco::API::UseCases::GraphQL::Helpers::Location::TagsRemap
23
+ end
24
+
21
25
  # Capture results
22
26
  def results
23
27
  @results ||= {}
@@ -26,7 +30,7 @@ class Eco::API::UseCases::GraphQL::Samples::Location
26
30
  # The maps of tags to be used in batch remap tags
27
31
  # @return [Array<Array>] source/destination pairs of `Array<String>`
28
32
  def tags_remap_table
29
- @tags_remap_table ||= []
33
+ @tags_remap_table ||= tags_remap_class.new
30
34
  end
31
35
 
32
36
  # Errors tracking/logging.
@@ -87,17 +91,17 @@ class Eco::API::UseCases::GraphQL::Samples::Location
87
91
  node_id, parent_id = result.command_input_data.values_at(:nodeId, :parentId)
88
92
  prev_node = previous_tree.node(node_id)
89
93
  curr_node = current_tree.node(node_id)
90
- lost_tags = prev_node.path - curr_node.path
91
- new_tags = curr_node.path - prev_node.path
94
+ prev_path = prev_node.path.reverse
95
+ new_path = curr_node.path.reverse
92
96
 
93
97
  curr_parent = curr_node.parent.top? ? nil : curr_node.parent
94
98
  unless curr_parent&.id == parent_id
95
- msg = "Node '#{node_id}' was moved uner '#{parent_id}', "
99
+ msg = "Node '#{node_id}' was moved under '#{parent_id}', "
96
100
  msg << "but in current structure has parent '#{curr_parent&.id}'"
97
101
  log(:warn) { msg }
98
102
  end
99
103
 
100
- tags_remap_table << [lost_tags.unshift(node_id), new_tags.unshift(node_id)]
104
+ tags_remap_table << [prev_path, new_path]
101
105
  end
102
106
  end
103
107
  end
@@ -108,8 +112,8 @@ class Eco::API::UseCases::GraphQL::Samples::Location
108
112
  timestamp_file(filename).tap do |file|
109
113
  CSV.open(file, 'w') do |csv|
110
114
  csv << ["source_tags", "destination_tags"]
111
- tags_remap_table.each do |(src_tags, dst_tags)|
112
- csv << [src_tags.join('|'), dst_tags.join('|')]
115
+ tags_remap_table.each do |tags_remap|
116
+ csv << tags_remap.to_csv_row
113
117
  end
114
118
  end
115
119
  log(:info) { "Generated file '#{file}'" }
@@ -28,10 +28,11 @@ class Eco::Data::Locations::NodeLevel
28
28
  msg << "\n Adding missing upper level(s): " + missing_nodes.map(&:raw_tag).pretty_inspect
29
29
  log(:info) { msg }
30
30
 
31
- out.push(*tidy_nodes(missing_nodes, prev_level: prev_level, main: false))
32
- # puts node.actual_level
33
- # pp node.tags_array
34
- level = prev_level + 1
31
+ # The very top missing node (first in list) should be checked against prev_level
32
+ # alongside any descendants in missing_nodes (when gap 2+)
33
+ tidied_nodes = tidy_nodes(missing_nodes, prev_level: prev_level, main: false)
34
+ out.push(*tidied_nodes)
35
+ #level = prev_level + 1 # <= we are actually on level and filled in the gaps
35
36
  end
36
37
  out << node
37
38
  done_ids << node_id
@@ -44,6 +45,8 @@ class Eco::Data::Locations::NodeLevel
44
45
  end
45
46
 
46
47
  # Sets the `parentId` property.
48
+ # Although with normalized nodes parents are self-contained
49
+ # we use this method
47
50
  def fill_in_parents(nodes)
48
51
  nodes.tap do |nodes|
49
52
  prev_nodes = empty_level_tracker_hash(11)
@@ -31,7 +31,13 @@ class Eco::Data::Locations::NodeLevel
31
31
  prev_level = nil
32
32
  prev_node = nil
33
33
  prev_nodes = empty_level_tracker_hash(11)
34
-
34
+ prev_node_get = proc do |raw_level|
35
+ prev = nil
36
+ (1..raw_level).to_a.reverse.each do |lev|
37
+ prev ||= prev_nodes[lev]
38
+ end
39
+ prev
40
+ end
35
41
  # Convert to Eco::CSV::Table for a fresh start
36
42
  csv = Eco::CSV.parse(csv.to_csv).nil_blank_cells.add_index_column(:row_num)
37
43
 
@@ -42,10 +48,12 @@ class Eco::Data::Locations::NodeLevel
42
48
 
43
49
  # If node is nested in prev_node or is a sibling thereof
44
50
  if prev_node.raw_level <= node.raw_level
45
- # Make sure parent is among upper level tags
51
+ # Make sure upper level tags are there (including parent)
52
+ # This normalizes input to all upper level tags always filled in
53
+ # which allows to node#actual_level to work
46
54
  node.set_high_levels(prev_node)
47
55
  else
48
- if parent_node = prev_nodes[node.raw_level - 1]
56
+ if parent_node = prev_node_get[node.raw_level - 1]
49
57
  node.set_high_levels(parent_node)
50
58
  elsif node.raw_level == 1
51
59
  # It is expected not to have parent (as it's top level tag)
@@ -47,6 +47,13 @@ module Eco::Data::Locations
47
47
  tags_array.index(raw_tag) + 1
48
48
  end
49
49
 
50
+ def raw_prev_empty_level
51
+ tags_array[0..raw_level-1].each_with_index.reverse_each do |value, idx|
52
+ return idx + 1 unless value
53
+ end
54
+ nil
55
+ end
56
+
50
57
  def tag_idx
51
58
  tags_array.index(raw_tag)
52
59
  end
@@ -106,6 +113,7 @@ module Eco::Data::Locations
106
113
  true
107
114
  end
108
115
 
116
+ # Cleanses deepest tags
109
117
  def override_upper_levels(src_tags_array, from_level: self.raw_level + 1)
110
118
  target_lev = Array(from_level..tag_attrs_count)
111
119
  target_tags = src_tags_array[level_to_idx(from_level)..level_to_idx(tag_attrs_count)]
@@ -115,7 +123,9 @@ module Eco::Data::Locations
115
123
  self
116
124
  end
117
125
 
118
- def override_lower_levels(src_tags_array, to_level: self.raw_level - 1)
126
+ # Ensures parent is among the upper level tags
127
+ def override_lower_levels(src_tags_array, to_level: self.raw_prev_empty_level)
128
+ return self unless to_level
119
129
  target_lev = Array(1..to_level)
120
130
  target_tags = src_tags_array[level_to_idx(1)..level_to_idx(to_level)]
121
131
  target_lev.zip(target_tags).each do |(n, tag)|
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = "2.5.5"
2
+ VERSION = "2.5.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eco-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.5
4
+ version: 2.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
@@ -156,7 +156,7 @@ dependencies:
156
156
  requirements:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: 0.3.9
159
+ version: 0.3.10
160
160
  - - "<"
161
161
  - !ruby/object:Gem::Version
162
162
  version: '0.4'
@@ -166,7 +166,7 @@ dependencies:
166
166
  requirements:
167
167
  - - ">="
168
168
  - !ruby/object:Gem::Version
169
- version: 0.3.9
169
+ version: 0.3.10
170
170
  - - "<"
171
171
  - !ruby/object:Gem::Version
172
172
  version: '0.4'
@@ -598,6 +598,9 @@ files:
598
598
  - lib/eco/api/usecases/graphql/helpers/location/command.rb
599
599
  - lib/eco/api/usecases/graphql/helpers/location/command/result.rb
600
600
  - lib/eco/api/usecases/graphql/helpers/location/command/results.rb
601
+ - lib/eco/api/usecases/graphql/helpers/location/tags_remap.rb
602
+ - lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_map.rb
603
+ - lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_set.rb
601
604
  - lib/eco/api/usecases/graphql/samples.rb
602
605
  - lib/eco/api/usecases/graphql/samples/location.rb
603
606
  - lib/eco/api/usecases/graphql/samples/location/command.rb