eco-helpers 3.2.14 → 3.2.22
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 +4 -4
- data/CHANGELOG.md +207 -0
- data/lib/eco/api/usecases/CLAUDE.md +78 -0
- data/lib/eco/api/usecases/default/pages.rb +30 -0
- data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
- data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
- data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
- data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
- data/lib/eco/api/usecases/graphql/compat.rb +11 -0
- data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
- data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
- data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
- data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
- data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
- data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
- data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
- data/lib/eco/api/usecases/graphql/samples/pages.rb +9 -0
- data/lib/eco/api/usecases/graphql/samples.rb +1 -0
- data/lib/eco/api/usecases/graphql.rb +1 -0
- data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
- data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +13 -3
- data/lib/eco/version.rb +1 -1
- metadata +52 -15
- data/.gitignore +0 -23
- data/.idea/.gitignore +0 -10
- data/.markdownlint.json +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -103
- data/.ruby-version +0 -1
- data/.yardopts +0 -10
- data/Gemfile +0 -8
- data/Rakefile +0 -38
- data/eco-helpers.gemspec +0 -63
|
@@ -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
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers
|
|
2
|
+
module Pages
|
|
3
|
+
# Native GraphQL re-expression of
|
|
4
|
+
# OozeSamples::HelpersMigration::TypedFieldsPairing.
|
|
5
|
+
#
|
|
6
|
+
# It pairs fields of the SAME type with the SAME label name across two page
|
|
7
|
+
# models (a source `src` and a destination `dst`). This is the cross-template
|
|
8
|
+
# field-pairing used by migration cases — DISTINCT from the value-merge
|
|
9
|
+
# `OozeHandlers#merge_values` (which combines two values of one already-paired
|
|
10
|
+
# field).
|
|
11
|
+
#
|
|
12
|
+
# It runs two screenings:
|
|
13
|
+
# (1) exact label match,
|
|
14
|
+
# (2) then the rest, case-insensitive a-z only, content between brackets removed.
|
|
15
|
+
#
|
|
16
|
+
# Other features (ported verbatim from the v2 "World Wonder of 2022"):
|
|
17
|
+
# * tracks src / dst fields that were NOT paired
|
|
18
|
+
# * lets you `resolve` fields paired by other means out of the unpaired sets
|
|
19
|
+
# * tracks src fields with MULTIPLE candidate dst fields (`src_dst_multi`)
|
|
20
|
+
#
|
|
21
|
+
# == What differs from the v2 class
|
|
22
|
+
#
|
|
23
|
+
# Fields are grouped and compared by the GraphQL DataField TYPE STRING (the
|
|
24
|
+
# `__typename`-derived value in `Ecoportal::API::GraphQL::Base::Page::DataField::TYPE_MAP`,
|
|
25
|
+
# e.g. 'PlainText', 'Select', 'Date') rather than the v2 snake_case component type
|
|
26
|
+
# (`'plain_text'`, `'select'`, `'date'`). Same-type grouping still guarantees a
|
|
27
|
+
# pair only forms between fields of identical type. `EXCLUDED_TYPES` therefore
|
|
28
|
+
# lists the GraphQL type strings for the never-pair types (charts / tag fields).
|
|
29
|
+
#
|
|
30
|
+
# `ximport?` detects an import-marker field via its `#tooltip` (unchanged), and
|
|
31
|
+
# `object_reference` / `same_string?` come from the native `Shortcuts`.
|
|
32
|
+
class TypedFieldsPairing
|
|
33
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Shortcuts
|
|
34
|
+
|
|
35
|
+
# GraphQL DataField type strings that must never be paired (no meaningful,
|
|
36
|
+
# copyable content). Mirrors the v2 EXCLUDED_TYPES
|
|
37
|
+
# (%w[tag_field chart frequency_rate_chart]) in GraphQL terms — GraphQL has no
|
|
38
|
+
# distinct chart data-field types, so TagField is the practical excluded type;
|
|
39
|
+
# any additional non-content types can be added here.
|
|
40
|
+
EXCLUDED_TYPES = %w[TagField Chart FrequencyRateChart].freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :src, :dst
|
|
43
|
+
attr_reader :src_unpaired, :dst_unpaired
|
|
44
|
+
attr_reader :src_dst_pairs, :src_dst_multi
|
|
45
|
+
|
|
46
|
+
def initialize(source, destination, src_excluded: nil, dst_excluded: nil,
|
|
47
|
+
exclude_ximport: false)
|
|
48
|
+
@src = source
|
|
49
|
+
@dst = destination
|
|
50
|
+
@src_excluded = src_excluded || []
|
|
51
|
+
@dst_excluded = dst_excluded || []
|
|
52
|
+
@exclude_ximport = exclude_ximport
|
|
53
|
+
|
|
54
|
+
reset!
|
|
55
|
+
process(exact: true)
|
|
56
|
+
process
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def each_pair
|
|
60
|
+
src_dst_pairs.each do |(src_field, dst_field)|
|
|
61
|
+
yield(src_field, dst_field) if block_given?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def report_src_unpaired(only_present: true, only_indexed: false)
|
|
66
|
+
return if src_unpaired.empty?
|
|
67
|
+
|
|
68
|
+
msg = "Source entry with unpaired fields (#{object_reference(src)}):\n"
|
|
69
|
+
|
|
70
|
+
src_filtered = src_unpaired.values.flatten.reject do |src_fld|
|
|
71
|
+
only_present && field_empty?(src_fld)
|
|
72
|
+
end.reject do |src_fld|
|
|
73
|
+
only_indexed && src_fld.deindex
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return if src_filtered.empty?
|
|
77
|
+
|
|
78
|
+
msg << src_filtered.map do |src_fld|
|
|
79
|
+
" * #{object_reference(src_fld)}"
|
|
80
|
+
end.join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def report_dst_unpaired(only_required: false, exclude_ximport: true)
|
|
84
|
+
return if dst_unpaired.empty?
|
|
85
|
+
|
|
86
|
+
msg = "Destination entry with unpaired fields (#{object_reference(src)}):\n"
|
|
87
|
+
|
|
88
|
+
dst_filtered = dst_unpaired.values.flatten.reject do |dst_fld|
|
|
89
|
+
only_required && !dst_fld.required
|
|
90
|
+
end.reject do |dst_fld|
|
|
91
|
+
exclude_ximport && ximport?(dst_fld)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return if dst_filtered.empty?
|
|
95
|
+
|
|
96
|
+
msg << dst_filtered.map do |dst_fld|
|
|
97
|
+
" * #{object_reference(dst_fld)}"
|
|
98
|
+
end.join("\n")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def report_multi_pairs(only_present: true, only_indexed: false, dst_exclude_ximport: true)
|
|
102
|
+
return nil if src_dst_multi.empty?
|
|
103
|
+
|
|
104
|
+
''.tap do |msg|
|
|
105
|
+
msg << "Source fields in (#{object_reference(src)}) paired with multiple destination fields:\n"
|
|
106
|
+
msg_flds = ''
|
|
107
|
+
src_dst_multi.each_value do |pairs|
|
|
108
|
+
pairs.each do |(src_fld, dst_flds)|
|
|
109
|
+
next if (only_present && field_empty?(src_fld)) || (only_indexed && src_fld.deindex)
|
|
110
|
+
|
|
111
|
+
dst_filtered = dst_flds.reject do |dst_fld|
|
|
112
|
+
dst_exclude_ximport && ximport?(dst_fld)
|
|
113
|
+
end
|
|
114
|
+
next if dst_filtered.empty?
|
|
115
|
+
|
|
116
|
+
msg_flds << " * #{object_reference(src_fld)}\n"
|
|
117
|
+
msg_flds << dst_filtered.map do |dst_fld|
|
|
118
|
+
" * #{object_reference(dst_fld)}"
|
|
119
|
+
end.compact.join("\n")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return nil if msg_flds.empty?
|
|
124
|
+
|
|
125
|
+
msg << msg_flds
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Remove from unpaired tracking those `flds`.
|
|
130
|
+
def resolve(*flds)
|
|
131
|
+
flds.each do |fld|
|
|
132
|
+
next if unpaired_delete?(fld, src_unpaired)
|
|
133
|
+
|
|
134
|
+
unpaired_delete?(fld, dst_unpaired)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
multi_delete(*flds)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def some_multi?
|
|
141
|
+
!src_dst_multi.empty?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# When comparing, use only a-z letters.
|
|
147
|
+
def only_letters?
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# When comparing, remove anything between brackets from the field label name.
|
|
152
|
+
def non_bracked?
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# If `false`, it compares with all lower case.
|
|
157
|
+
def exact?
|
|
158
|
+
!(only_letters? || non_bracked?)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def process(exact: false)
|
|
162
|
+
src_unpaired.each do |type, src_pend|
|
|
163
|
+
next unless (dst_pend = dst_unpaired[type]) && !dst_pend.empty?
|
|
164
|
+
|
|
165
|
+
src_pend.dup.each do |src_fld|
|
|
166
|
+
dst_candidates = dst_pend.select do |dst_fld|
|
|
167
|
+
str_eq?(src_fld.label, dst_fld.label, exact: exact)
|
|
168
|
+
end
|
|
169
|
+
next if dst_candidates.empty?
|
|
170
|
+
|
|
171
|
+
if dst_candidates.one?
|
|
172
|
+
pair_add(src_fld, dst_candidates.first)
|
|
173
|
+
else
|
|
174
|
+
multi_add(type, src_fld, *dst_candidates)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def str_eq?(str_1, str_2, exact: false)
|
|
181
|
+
same_string?(str_1, str_2, **str_eq_params, exact: exact)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def str_eq_params
|
|
185
|
+
{
|
|
186
|
+
exact: exact?,
|
|
187
|
+
mild: only_letters?,
|
|
188
|
+
ignore: false
|
|
189
|
+
}.tap do |params|
|
|
190
|
+
params.merge!(ignore: bracked_regex) if non_bracked?
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def pair_add(src_fld, dst_fld)
|
|
195
|
+
resolve(src_fld, dst_fld)
|
|
196
|
+
src_dst_pairs << [src_fld, dst_fld]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Removes a field from a (src or dst) unpaired set.
|
|
200
|
+
# @return [Boolean] true when the field was present and removed.
|
|
201
|
+
def unpaired_delete?(fld, unpaired)
|
|
202
|
+
type = fld.type
|
|
203
|
+
return false unless unpaired.key?(type)
|
|
204
|
+
return false unless (flds = unpaired[type]).delete(fld)
|
|
205
|
+
|
|
206
|
+
unpaired.delete(type) if flds.empty?
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def multi_add(type, src_fld, *dst_flds)
|
|
211
|
+
(src_dst_multi[type] ||= []) << (pair = [src_fld, dst_flds])
|
|
212
|
+
@src_multi[src_fld] = pair
|
|
213
|
+
|
|
214
|
+
dst_flds.each do |dst_fld|
|
|
215
|
+
(@dst_multi[dst_fld] ||= []) << pair
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def multi_delete(*flds)
|
|
220
|
+
flds.each do |fld|
|
|
221
|
+
next unless (type_pairs = src_dst_multi[fld.type])
|
|
222
|
+
|
|
223
|
+
if (pairs = @dst_multi.delete(fld))
|
|
224
|
+
pairs.each do |pair|
|
|
225
|
+
src_fld = pair.first
|
|
226
|
+
dst_flds = pair.last
|
|
227
|
+
dst_flds.delete(fld)
|
|
228
|
+
if dst_flds.empty?
|
|
229
|
+
@src_multi.delete(src_fld)
|
|
230
|
+
type_pairs.delete(pair)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
elsif @src_multi.key?(fld)
|
|
234
|
+
pair = @src_multi.delete(fld)
|
|
235
|
+
type_pairs.delete(pair)
|
|
236
|
+
|
|
237
|
+
pair.last.each do |dst_fld|
|
|
238
|
+
pairs = @dst_multi[dst_fld]
|
|
239
|
+
pairs.delete(pair)
|
|
240
|
+
@dst_multi.delete(dst_fld) if pairs.empty?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
src_dst_multi.delete(fld.type) if type_pairs.empty?
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def by_type(components)
|
|
249
|
+
components.to_a.group_by(&:type).tap do |out|
|
|
250
|
+
EXCLUDED_TYPES.each {|type| out.delete(type)}
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def src_components
|
|
255
|
+
flds = components_of(src)
|
|
256
|
+
flds = flds.reject {|fld| ximport?(fld)} if @exclude_ximport
|
|
257
|
+
flds - @src_excluded
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def dst_components
|
|
261
|
+
flds = components_of(dst)
|
|
262
|
+
flds = flds.reject {|fld| ximport?(fld)} if @exclude_ximport
|
|
263
|
+
flds - @dst_excluded
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Duck-typed access to a page model's data fields. Native GraphQL page
|
|
267
|
+
# models expose `#components` (a SectionCollection-backed collection); the
|
|
268
|
+
# v2 class relied on the same method name.
|
|
269
|
+
def components_of(entry)
|
|
270
|
+
entry.components.to_a
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def ximport?(fld)
|
|
274
|
+
return false unless fld.respond_to?(:tooltip)
|
|
275
|
+
|
|
276
|
+
fld.tooltip.to_s.include?('ximport')
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# A field is "empty" when it carries no content. Native DataField types do
|
|
280
|
+
# not universally expose `#empty?`; fall back to `#value` emptiness so the
|
|
281
|
+
# report filters behave like the v2 ones.
|
|
282
|
+
def field_empty?(fld)
|
|
283
|
+
return fld.empty? if fld.respond_to?(:empty?)
|
|
284
|
+
return fld.value.to_s.strip.empty? if fld.respond_to?(:value)
|
|
285
|
+
|
|
286
|
+
false
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def reset!
|
|
290
|
+
@src_unpaired = by_type(src_components)
|
|
291
|
+
@dst_unpaired = by_type(dst_components)
|
|
292
|
+
@src_dst_pairs = []
|
|
293
|
+
reset_multi!
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def reset_multi!
|
|
297
|
+
@src_dst_multi = {}
|
|
298
|
+
@src_multi = {}
|
|
299
|
+
@dst_multi = {}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers
|
|
2
|
+
# Native GraphQL page-processing helper substrate.
|
|
3
|
+
#
|
|
4
|
+
# These mirror the *pure* parts of Eco::API::UseCases::OozeSamples::Helpers::* but drop the
|
|
5
|
+
# Ecoportal::API::V2 coupling: string/date utilities are ported verbatim, while the few
|
|
6
|
+
# methods that inspected V2 model types (object_reference, field_key_name) are re-expressed
|
|
7
|
+
# with duck-typing so they work against the GraphQL page/section/field models.
|
|
8
|
+
#
|
|
9
|
+
# This is Phase 1 of the ooze -> native GraphQL migration (build the shared substrate before
|
|
10
|
+
# any case). See ecoportal-api-graphql/.ai-assistance/projects/ooze-graphql-native-migration/.
|
|
11
|
+
module Pages
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative 'pages/shortcuts'
|
|
16
|
+
require_relative 'pages/filters'
|
|
17
|
+
require_relative 'pages/ooze_handlers'
|
|
18
|
+
require_relative 'pages/rescuable'
|
|
19
|
+
require_relative 'pages/creatable'
|
|
20
|
+
require_relative 'pages/typed_fields_pairing'
|
|
21
|
+
require_relative 'pages/copying'
|