eco-helpers 3.2.16 → 3.2.17

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -0
  3. data/eco-helpers.gemspec +2 -2
  4. data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
  5. data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
  6. data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
  7. data/lib/eco/api/usecases/graphql/compat.rb +5 -0
  8. data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
  9. data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
  10. data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
  11. data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
  12. data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
  13. data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
  14. data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
  15. data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
  16. data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
  17. data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
  18. data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
  19. data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
  20. data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
  21. data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
  22. data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
  23. data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
  24. data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
  25. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
  26. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
  27. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
  28. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
  29. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
  30. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
  31. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
  32. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
  33. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
  34. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
  35. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
  36. data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
  37. data/lib/eco/api/usecases/graphql/samples/pages.rb +2 -0
  38. data/lib/eco/version.rb +1 -1
  39. metadata +34 -5
@@ -0,0 +1,114 @@
1
+ # rubocop:disable Lint/SymbolConversion, Style/QuotedSymbols
2
+ module Eco::API::UseCases::GraphQL::Helpers
3
+ module Pages
4
+ # Search-filter + time-math helpers, ported from OozeSamples::Helpers::Filters.
5
+ #
6
+ # The time/date/tag builders are pure and ported verbatim. #field_key_name's field branch
7
+ # is re-expressed with duck-typing (was a `V2::Page::Component` case) — see the note there.
8
+ # A `weekday` helper is added because the original relied on one defined outside Filters
9
+ # (undefined in lib/), which made the sunday/monday chain latent-broken.
10
+ module Filters
11
+ FILTER_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
12
+
13
+ def tags_filter(tags, any: true, negate: false)
14
+ tags = [tags].flatten.compact
15
+ return nil if tags.empty?
16
+ key, name = field_key_name(:tags)
17
+ {
18
+ "tags": tags,
19
+ "mode": any ? "any" : "all",
20
+ "negate": negate,
21
+ "key": key,
22
+ "name": name,
23
+ "type": "tag_filter"
24
+ }
25
+ end
26
+
27
+ def date_range_filter(from: nil, to: nil, key: :updated_at)
28
+ return nil unless from || to
29
+ key, name = field_key_name(key)
30
+ {
31
+ "relstart": "today",
32
+ "time_zone": "Pacific/Auckland",
33
+ "relative": false,
34
+ "key": key,
35
+ "name": name,
36
+ "type": "date_filter"
37
+ }.tap do |out|
38
+ out.merge!("lbound": to_date_filter(from)) if from
39
+ out.merge!("ubound": to_date_filter(to)) if to
40
+ end
41
+ end
42
+
43
+ def to_date_filter(date)
44
+ daystart(date).utc.strftime(FILTER_TIME_FORMAT)
45
+ end
46
+
47
+ def set_time(date, hour, min, sec)
48
+ Time.new(date.year, date.month, date.day, hour, min, sec)
49
+ end
50
+
51
+ def weeks(num)
52
+ num * days(7)
53
+ end
54
+
55
+ def days(num)
56
+ num * 60 * 60 * 24
57
+ end
58
+
59
+ def today
60
+ Date.today.to_time
61
+ end
62
+
63
+ # 0 = Sunday .. 6 = Saturday (Date#wday). Added: the original relied on a `weekday`
64
+ # helper defined outside Filters (not present in lib/), leaving the chain below broken.
65
+ def weekday(date)
66
+ (date.respond_to?(:to_date) ? date.to_date : date).wday
67
+ end
68
+
69
+ def sunday(date)
70
+ date + days(7 - weekday(date))
71
+ end
72
+
73
+ def midnight(date)
74
+ set_time(date, 23, 59, 59)
75
+ end
76
+
77
+ def daystart(date)
78
+ set_time(date, 0, 0, 0)
79
+ end
80
+
81
+ def previous_sunday(date)
82
+ midnight(sunday(date - weeks(1)))
83
+ end
84
+
85
+ def this_monday(date)
86
+ set_time(previous_sunday(date) + days(1), 0, 0, 0)
87
+ end
88
+
89
+ # Resolve a filter [key, name] pair for the register search.
90
+ #
91
+ # @note **Search-key gotcha:** the register search matches on the field DEFINITION key,
92
+ # not the field's name/label. For a data-field object we therefore prefer its backend
93
+ # reference key. The exact GraphQL accessor is duck-typed here (ref_backend -> key) and
94
+ # must be confirmed against the live field model before relying on field-scoped search
95
+ # filters (a wrong key silently matches 0 rows). See the CANS search-key note.
96
+ def field_key_name(value)
97
+ case value
98
+ when :updated_at then ["updated_at", "Last updated"]
99
+ when :created_at then ["created_at", "Page created"]
100
+ when :tags then ["tags", "Location Tags"]
101
+ else
102
+ if value.respond_to?(:ref_backend)
103
+ [value.ref_backend, (value.respond_to?(:label) ? value.label : nil)]
104
+ elsif value.respond_to?(:key)
105
+ [value.key, (value.respond_to?(:label) ? value.label : nil)]
106
+ else
107
+ [nil, nil]
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ # rubocop:enable Lint/SymbolConversion, Style/QuotedSymbols
@@ -0,0 +1,112 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Pages
3
+ # Native GraphQL re-expression of OozeSamples::Helpers::OozeHandlers.
4
+ #
5
+ # `merge_values` combines two values of a data field of the *same* type and
6
+ # label into one, matching the v2 semantics but dispatching on the GraphQL
7
+ # DataField TYPE STRING (e.g. 'PlainText', 'Select', 'Date') rather than on
8
+ # `Ecoportal::API::V2::Page::Component::*` classes. The type strings are the
9
+ # `__typename`-derived values in
10
+ # `Ecoportal::API::GraphQL::Base::Page::DataField::TYPE_MAP`.
11
+ #
12
+ # `merge_arrays` / `array_indexes` are pure array utilities — ported verbatim.
13
+ module OozeHandlers
14
+ # Per-type merge strategy.
15
+ # :join → concatenate origin + append with a delimiter (text, select,
16
+ # reference, people, checklist, action list, files, images, geo, law)
17
+ # :origin → keep the origin value untouched (date, number, gauge — no sensible
18
+ # textual concatenation; the v2 case returned `origin` for these)
19
+ #
20
+ # Text-ish types always merge with a NEWLINE delimiter in v2 (RichText /
21
+ # PlainText) regardless of the caller's `delimiter:`; the rest honour it.
22
+ MERGE_STRATEGY = {
23
+ 'PlainText' => %i[join newline],
24
+ 'RichText' => %i[join newline],
25
+ 'Select' => %i[join delimiter],
26
+ 'CrossReference' => %i[join delimiter],
27
+ 'People' => %i[join delimiter],
28
+ 'Checklist' => %i[join delimiter],
29
+ 'ActionsList' => %i[join delimiter],
30
+ 'FileField' => %i[join delimiter],
31
+ 'File' => %i[join delimiter],
32
+ 'ImageGallery' => %i[join delimiter],
33
+ 'Geo' => %i[join delimiter],
34
+ 'Law' => %i[join delimiter],
35
+ 'DateField' => %i[origin],
36
+ 'Date' => %i[origin],
37
+ 'Number' => %i[origin],
38
+ 'Gauge' => %i[origin]
39
+ }.freeze
40
+
41
+ # Merge values of fields of the same type and label name.
42
+ #
43
+ # @param origin [Object] the value to keep as the base.
44
+ # @param append [Object] the value to merge onto `origin`.
45
+ # @param type [String, Symbol, nil] the GraphQL DataField type string
46
+ # (e.g. 'PlainText', 'Select'). Nil / unknown → default join. Accepts a
47
+ # field object responding to #type too (convenience).
48
+ # @param delimiter [String] the delimiter for :delimiter-strategy types.
49
+ # @return [Object] the merged value.
50
+ def merge_values(origin, append, type: nil, delimiter: "\n")
51
+ return origin if !append || append.to_s.strip.empty?
52
+ return append if !origin || origin.to_s.strip.empty?
53
+
54
+ type_key = resolve_field_type(type)
55
+ strategy, delim_kind = MERGE_STRATEGY.fetch(type_key, %i[join delimiter])
56
+
57
+ return origin if strategy == :origin
58
+
59
+ used_delimiter = delim_kind == :newline ? "\n" : delimiter
60
+ [origin, append].join(used_delimiter)
61
+ end
62
+
63
+ # Merges arrays `ref` and `plus` by keeping the order of `ref`
64
+ # but inserting additional elements of `plus` in the correct position
65
+ # (i.e. when those elements are between two elements that exist in `ref`).
66
+ # @note this can be used to combine two different header sequences
67
+ # by using `ref` as a reference.
68
+ def merge_arrays(ref, plus) # rubocop:disable Metrics/AbcSize
69
+ return ref if ref == plus
70
+
71
+ href_idx = array_indexes(ref)
72
+ hplus_idx = array_indexes(plus)
73
+ shared = ref & plus
74
+ hshared_ref_idx = href_idx.slice(*shared)
75
+
76
+ shared_plus_idxs = hplus_idx.values_at(*shared).sort.reverse
77
+
78
+ only_plus = plus - ref
79
+ honly_plus_idx = hplus_idx.slice(*only_plus)
80
+
81
+ hplus_ref_idx = honly_plus_idx.each_with_object({}) do |(e, idx), href_idx_plus|
82
+ ps_idx = shared_plus_idxs.bsearch {|i| idx >= i}
83
+ rs_idx = ps_idx ? hshared_ref_idx[plus[ps_idx]] : -1
84
+ href_idx_plus[e] = rs_idx
85
+ end.group_by do |_e, idx|
86
+ idx
87
+ end.to_a.sort_by do |(idx, _pairs)|
88
+ idx
89
+ end.reverse.to_h.transform_values do |pairs|
90
+ pairs.map(&:first)
91
+ end
92
+
93
+ hplus_ref_idx.each_with_object(ref.dup) do |(idx, elements), rf|
94
+ rf.insert(idx + 1, *elements)
95
+ end
96
+ end
97
+
98
+ def array_indexes(ary)
99
+ ary.each_with_index.to_h
100
+ end
101
+
102
+ private
103
+
104
+ # Accept either a type string/symbol or a field object that responds to #type.
105
+ def resolve_field_type(type)
106
+ return type.type.to_s if type.respond_to?(:type)
107
+
108
+ type.to_s
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,52 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Pages
3
+ # Prevents a script from stopping on a per-item error, prompting the operator to
4
+ # continue or abort. Ported from OozeSamples::Helpers::Rescuable; the only change is
5
+ # the base-type guard, which now requires a native GraphQL::Base case.
6
+ module Rescuable
7
+ module InstanceMethods
8
+ private
9
+
10
+ # Helper to prevent script from stopping
11
+ def with_rescue(reference, trace_count: 1)
12
+ raise ArgumentError, 'Expecting block, but not given' unless block_given?
13
+ yield
14
+ rescue StandardError => e
15
+ str_err = e.patch_full_message(trace_count: trace_count)
16
+ log(:error) { [reference, str_err].join(" => \n") }
17
+ lines = []
18
+ lines << "\nThere was an error. Choose one option:\n"
19
+ lines << ' (C) - Continue/resume. Just ignore this one.'
20
+ lines << ' (A) - Abort. Just break this run.'
21
+
22
+ session.prompt_user('Type one option (C/a):', explanation: lines.join("\n"), default: 'C') do |res|
23
+ res = res.upcase
24
+ raise if res.start_with?('A')
25
+
26
+ # 'C'
27
+ log(:warn) { "Script resumed after error...\n * #{reference}" }
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ class << self
34
+ def included(base)
35
+ super
36
+ validate_base_type!(base)
37
+ base.include(InstanceMethods)
38
+ end
39
+
40
+ def validate_base_type!(base)
41
+ return super if defined?(super)
42
+
43
+ msg = "#{self} can only be included in Eco::API::UseCases::GraphQL::Base"
44
+ msg << "\nCan't be included in #{base}"
45
+ raise LoadError, msg unless base <= Eco::API::UseCases::GraphQL::Base
46
+
47
+ true
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,186 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Pages
3
+ # String simplification / comparison helpers, ported from
4
+ # OozeSamples::Helpers::Shortcuts. Everything here is pure except #object_reference,
5
+ # which is re-expressed with duck-typing (no Ecoportal::API::V2 constants) so it
6
+ # describes GraphQL page/stage/section/field/force models.
7
+ module Shortcuts
8
+ # Basic simplification pattern (matches all but a-z and blank space)
9
+ def non_letters_regex
10
+ @non_letters_regex ||= /[^a-z ]+/
11
+ end
12
+
13
+ # Matches anything between two consecutive (), inclusive
14
+ def bracked_regex
15
+ @bracked_regex ||= /(?<bracked>\([^\)]+?\))/ # rubocop:disable Style/RedundantRegexpEscape
16
+ end
17
+
18
+ # It always downcase, trim and remove double spaces.
19
+ # @param ignore [Boolean, Regexp, String, Array] ignored when `exact` is `true`
20
+ # * when `false`: it does not do anything additional
21
+ # * when `Regexp`: it removes all characters that match the expression.
22
+ # * when `String`: each character listed is removed.
23
+ # * when `Array`: reduces `str` processing `ignore` in order.
24
+ # * when `true` (or otherwise): it removes all non a-zA-Z characters but blank spaces.
25
+ def simplify_string(str, ignore: false)
26
+ str = str.to_s.strip.downcase.gsub(/\s+/, ' ')
27
+ return str unless ignore
28
+
29
+ sub = non_letters_regex
30
+ case ignore
31
+ when Regexp then sub = ignore
32
+ when String then sub = /[#{ignore}]+/
33
+ when Array
34
+ return ignore.reduce(str) do |out, sub_ignore|
35
+ simplify_string(out, ignore: sub_ignore)
36
+ end
37
+ end
38
+ str.gsub(sub, '').gsub(/\s+/, ' ').strip
39
+ end
40
+
41
+ # Offers multiple simplification methods to compare two strings
42
+ # @note only one of the values can be a Regexp
43
+ def same_string?(value_1, value_2, exact: false, mild: false, ignore: false) # rubocop:disable Metrics/AbcSize
44
+ return true if value_1.to_s.strip.empty? && value_2.to_s.strip.empty?
45
+ return false if value_1.to_s.strip.empty? || value_2.to_s.strip.empty?
46
+
47
+ val_1 = value_1
48
+ val_2 = value_2
49
+
50
+ unless exact
51
+ if val_1.is_a?(String)
52
+ val_1 = simplify_string(val_1, ignore: ignore) if ignore
53
+ val_1 = simplify_string(val_1, ignore: non_letters_regex) if mild
54
+ end
55
+ if val_2.is_a?(String)
56
+ val_2 = simplify_string(val_2, ignore: ignore) if ignore
57
+ val_2 = simplify_string(val_2, ignore: non_letters_regex) if mild
58
+ end
59
+ end
60
+
61
+ if val_1.is_a?(String) && val_2.is_a?(String)
62
+ val_1 == val_2
63
+ elsif val_1.is_a?(Regexp) && val_2.is_a?(String)
64
+ val_2 =~ val_1
65
+ elsif val_1.is_a?(String) && val_2.is_a?(Regexp)
66
+ val_1 =~ val_2
67
+ else
68
+ msg = 'Expected at least one String, and either a String or Regex. '
69
+ msg << "Given: (1: #{val_1.class}) and (2: #{val_2.class})"
70
+ raise ArgumentError, msg
71
+ end
72
+ end
73
+
74
+ def titleize(str)
75
+ return nil unless str
76
+ return str if str.strip.empty?
77
+
78
+ str.split(/\s+/).map do |part|
79
+ part[0] = part[0].upcase
80
+ part[1..] = part[1..].downcase
81
+ part
82
+ end.join(' ')
83
+ end
84
+
85
+ def normalize_string(str)
86
+ return nil unless str
87
+
88
+ str.gsub(/[^[:print:]]/, '').
89
+ gsub(/[[:space:]]+/, ' ').
90
+ gsub(/[[:space:]]$/, '').
91
+ gsub(/[-‑‒–]/, '-').then do |aux|
92
+ aux = yield(aux) if block_given?
93
+ aux
94
+ end
95
+ end
96
+
97
+ def clean_question(str)
98
+ return nil unless str
99
+
100
+ normalize_string(str) do |x|
101
+ x.gsub("\r\n", ' ').then do |aux|
102
+ aux = yield(aux) if block_given?
103
+ aux
104
+ end
105
+ end
106
+ end
107
+
108
+ # Human-readable reference for a GraphQL page-model object, for logs/errors.
109
+ #
110
+ # Re-expressed with duck-typing (was a V2 `case/when` on Page/Stage/Section/Component/
111
+ # Force/Binding). We inspect the object's shape rather than its class so this works
112
+ # across Base::/Model:: GraphQL types without coupling to constants that may move.
113
+ def object_reference(obj)
114
+ return 'No reference' unless obj
115
+
116
+ if binding_like?(obj)
117
+ "Binding '#{safe_name(obj)}' in #{object_reference(obj.force)}"
118
+ elsif force_like?(obj)
119
+ "Force '#{safe_name(obj)}'"
120
+ elsif component_like?(obj)
121
+ label = obj.respond_to?(:label) ? obj.label : nil
122
+ type = obj.respond_to?(:type) ? obj.type : nil
123
+ ref = "Component '#{label || "(unnamed of type '#{type}')"}'"
124
+ ref += " in #{object_reference(obj.section)}" if obj.respond_to?(:section) && obj.section
125
+ ref
126
+ elsif section_like?(obj)
127
+ heading = obj.respond_to?(:heading) ? obj.heading : nil
128
+ "Section '#{heading || '(unnamed)'}'"
129
+ elsif stage_like?(obj)
130
+ named_ref('Stage', obj)
131
+ elsif page_like?(obj)
132
+ named_ref("Page (#{obj.id})", obj)
133
+ else
134
+ named_ref('', obj).strip
135
+ end
136
+ end
137
+
138
+ def to_i(value)
139
+ Float(value).to_i
140
+ end
141
+
142
+ # https://stackoverflow.com/a/5661695/4352306
143
+ # Kept as `is_number?` for naming parity with OozeSamples::Helpers::Shortcuts.
144
+ def is_number?(value) # rubocop:disable Naming/PredicatePrefix
145
+ true if Float(value)
146
+ rescue ArgumentError, TypeError
147
+ false
148
+ end
149
+
150
+ private
151
+
152
+ def binding_like?(obj)
153
+ obj.respond_to?(:force) && obj.respond_to?(:name) && !obj.respond_to?(:bindings)
154
+ end
155
+
156
+ def force_like?(obj)
157
+ obj.respond_to?(:bindings) && obj.respond_to?(:name)
158
+ end
159
+
160
+ def component_like?(obj)
161
+ obj.respond_to?(:label) && (obj.respond_to?(:type) || obj.respond_to?(:section))
162
+ end
163
+
164
+ def section_like?(obj)
165
+ obj.respond_to?(:heading) && obj.respond_to?(:components)
166
+ end
167
+
168
+ def stage_like?(obj)
169
+ obj.respond_to?(:ordering) && obj.respond_to?(:state)
170
+ end
171
+
172
+ def page_like?(obj)
173
+ obj.respond_to?(:id) && obj.respond_to?(:stages)
174
+ end
175
+
176
+ def safe_name(obj)
177
+ obj.respond_to?(:name) ? obj.name : nil
178
+ end
179
+
180
+ def named_ref(prefix, obj)
181
+ name = safe_name(obj)
182
+ name ? "#{prefix} '#{name}'" : prefix
183
+ end
184
+ end
185
+ end
186
+ end