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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -0
- data/eco-helpers.gemspec +2 -2
- 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 +5 -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/location/command/service/tree_update.rb +1 -1
- 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 +2 -0
- data/lib/eco/version.rb +1 -1
- metadata +34 -5
|
@@ -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'
|
|
@@ -57,7 +57,7 @@ class Eco::API::UseCases::GraphQL::Samples::Location
|
|
|
57
57
|
digest_msgs << "\n#{exception.patch_full_message(trace_count: 3)}" if exception
|
|
58
58
|
|
|
59
59
|
session.mail(subject: subject, body: digest_msgs.join).tap do
|
|
60
|
-
options.deep_merge!({
|
|
60
|
+
options.deep_merge!({workflow: {no_email: true}})
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Native GraphQL re-expression of OozeSamples::RegisterUpdateCase.
|
|
3
|
+
#
|
|
4
|
+
# Register-scoped page processing: iterate the entries of a register in
|
|
5
|
+
# cursor-paginated batches, hand each page to `process_page` (a.k.a.
|
|
6
|
+
# `process_ooze` for naming parity with the v2 case), and track KPI counters
|
|
7
|
+
# over the whole run. Supports creating new entries via the native Creatable.
|
|
8
|
+
#
|
|
9
|
+
# This reproduces the SHAPE of RegisterUpdateCase (batched search, dedup-by-id,
|
|
10
|
+
# search/retrieved/updated/created/failed counters, a dry-run preview) natively,
|
|
11
|
+
# without the Ecoportal::API::V2 queue/enqueue coupling: the GraphQL update path
|
|
12
|
+
# is per-page (graphql.pages.update) rather than a batched patch queue, so the
|
|
13
|
+
# v2 batch_queue / enqueue / queue_shift machinery is intentionally dropped.
|
|
14
|
+
#
|
|
15
|
+
# == Subclass interface
|
|
16
|
+
#
|
|
17
|
+
# class Custom::UseCase::CloseOldEntries < Eco::API::UseCases::GraphQL::Samples::Pages::Register::Base
|
|
18
|
+
# name 'close-old-entries'
|
|
19
|
+
# register_id 'REG_ABC123'
|
|
20
|
+
# batch_size 25
|
|
21
|
+
#
|
|
22
|
+
# def process_page(page)
|
|
23
|
+
# return skip('not stale') unless stale?(page)
|
|
24
|
+
# page.name = "[CLOSED] #{page.name}"
|
|
25
|
+
# update_page(page)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# `register_id` may also come from `options[:source][:register_id]` (v2 parity),
|
|
30
|
+
# which takes precedence over the class-level declaration when present.
|
|
31
|
+
class Register::Base < Page::Base
|
|
32
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Shortcuts
|
|
33
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::OozeHandlers
|
|
34
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Rescuable
|
|
35
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Creatable
|
|
36
|
+
|
|
37
|
+
name 'graphql-register-base'
|
|
38
|
+
type :other
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
def batch_size(size = nil)
|
|
42
|
+
@batch_size ||= 25
|
|
43
|
+
return @batch_size unless size
|
|
44
|
+
|
|
45
|
+
@batch_size = size
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :total_search_pages, :dupped_search_pages
|
|
50
|
+
attr_reader :retrieved_pages, :page_result_ids
|
|
51
|
+
attr_reader :created_pages, :page_create_attempts
|
|
52
|
+
|
|
53
|
+
# Entry point — called by GraphQL::Base#main.
|
|
54
|
+
def process
|
|
55
|
+
init_kpis
|
|
56
|
+
results_preview
|
|
57
|
+
each_page do |page|
|
|
58
|
+
process_page(page)
|
|
59
|
+
@processed_pages += 1
|
|
60
|
+
end
|
|
61
|
+
log_kpis
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# == Subclass override points ==============================================
|
|
65
|
+
|
|
66
|
+
# Transform or act on one page. Call update_page(page) to persist changes.
|
|
67
|
+
# Alias `process_ooze` is provided for naming parity with the v2 case.
|
|
68
|
+
def process_page(_page)
|
|
69
|
+
raise NotImplementedError, "Implement #process_page in #{self.class}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Naming-parity alias for the v2 case. Forwards (rather than alias_method) so a
|
|
73
|
+
# subclass overriding #process_page is honoured when called as #process_ooze.
|
|
74
|
+
def process_ooze(page)
|
|
75
|
+
process_page(page)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Return the register id to scope the search to.
|
|
79
|
+
# `options[:source][:register_id]` (v2 parity) wins over the class declaration.
|
|
80
|
+
def register_id
|
|
81
|
+
options.dig(:source, :register_id) || self.class.register_id
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Return a SearchConf for the page search. Override to add filters.
|
|
85
|
+
# Default: scoped to register_id (raises if none configured — a register case
|
|
86
|
+
# without a register would silently scan the whole org).
|
|
87
|
+
def search_conf
|
|
88
|
+
reg = register_id
|
|
89
|
+
raise ArgumentError, 'No register_id configured (class register_id or options[:source][:register_id])' unless reg
|
|
90
|
+
|
|
91
|
+
sc.new.filter(in_register(reg))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
protected
|
|
95
|
+
|
|
96
|
+
# Typed value merge for a GraphQL data field. Convenience over OozeHandlers#merge_values
|
|
97
|
+
# that reads the field's own #type so a subclass need not pass it explicitly.
|
|
98
|
+
#
|
|
99
|
+
# fld = page.components.get_by_name('Notes')
|
|
100
|
+
# fld.value = merge_field_values(fld, incoming_notes)
|
|
101
|
+
#
|
|
102
|
+
# @param field [#type] a GraphQL DataField (or anything responding to #type).
|
|
103
|
+
# @param append [Object] the value to merge onto the field's current value.
|
|
104
|
+
# @param origin [Object] the base value; defaults to the field's current #value.
|
|
105
|
+
# @param delimiter [String] delimiter for :delimiter-strategy types.
|
|
106
|
+
# @return [Object] the merged value.
|
|
107
|
+
def merge_field_values(field, append, origin: nil, delimiter: "\n")
|
|
108
|
+
base = origin.nil? && field.respond_to?(:value) ? field.value : origin
|
|
109
|
+
merge_values(base, append, type: field, delimiter: delimiter)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Launches the actual creation of the entry (hook driven by Creatable).
|
|
113
|
+
# Respects simulate? for dry-run; on live create, bumps the created counter.
|
|
114
|
+
# @return [Ecoportal::API::GraphQL::Compat::PageReference, false]
|
|
115
|
+
def create_ooze(draft, template_id: draft&.template_id)
|
|
116
|
+
@page_create_attempts += 1
|
|
117
|
+
if simulate?
|
|
118
|
+
dry_run_feedback(draft)
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
graphql.pages.create(draft, from: template_id).tap do |result|
|
|
123
|
+
@created_pages += 1 if result.respond_to?(:page_id) && result.page_id
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Cursor-paginated iteration over the register's pages. Counts total search
|
|
128
|
+
# results and flags duplicated page ids (dedup-by-id, v2 parity).
|
|
129
|
+
def each_page(conf: search_conf, first: self.class.batch_size)
|
|
130
|
+
graphql.pages.each(search_conf: conf.to_h, first: first) do |page|
|
|
131
|
+
@total_search_pages += 1
|
|
132
|
+
if page_result_ids[page.id]
|
|
133
|
+
@dupped_search_pages += 1
|
|
134
|
+
else
|
|
135
|
+
page_result_ids[page.id] = true
|
|
136
|
+
@total_pages += 1
|
|
137
|
+
end
|
|
138
|
+
@retrieved_pages += 1
|
|
139
|
+
yield page
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Dry-run preview of what a create WOULD send. Faithful but scoped: the
|
|
146
|
+
# GraphQL cursor search offers no cheap "total before filtering" count like the
|
|
147
|
+
# v2 registers.search(only_first:), so there is no up-front count prompt here.
|
|
148
|
+
def dry_run_feedback(draft)
|
|
149
|
+
log(:info) { "Simulate — would create page from template #{draft&.template_id}" }
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def results_preview
|
|
154
|
+
log(:info) { "Processing register '#{register_id}' in batches of #{self.class.batch_size}" }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def init_kpis
|
|
158
|
+
super
|
|
159
|
+
@total_search_pages = 0
|
|
160
|
+
@dupped_search_pages = 0
|
|
161
|
+
@retrieved_pages = 0
|
|
162
|
+
@page_result_ids = {}
|
|
163
|
+
@created_pages = 0
|
|
164
|
+
@page_create_attempts = 0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def kpis_message
|
|
168
|
+
[
|
|
169
|
+
'Run end:',
|
|
170
|
+
" * Search results: #{total_search_pages}",
|
|
171
|
+
" * Duplicated: #{dupped_search_pages}",
|
|
172
|
+
" * Retrieved: #{retrieved_pages}",
|
|
173
|
+
" * Processed: #{processed_pages}",
|
|
174
|
+
" * Updated: #{updated_pages} (of #{total_pages} unique)",
|
|
175
|
+
" * Created: #{created_pages} (of #{page_create_attempts} attempts)",
|
|
176
|
+
" * Skipped: #{skipped_pages}",
|
|
177
|
+
" * Failed: #{failed_pages}"
|
|
178
|
+
].join("\n")
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Native GraphQL re-expression of OozeSamples::RegisterMigrationCase.
|
|
3
|
+
#
|
|
4
|
+
# Migrates the entries of a register into NEW entries of a (possibly different)
|
|
5
|
+
# template, copying data across by PAIRING typed fields between the source page
|
|
6
|
+
# and the freshly drafted destination page. Field pairing is delegated to the
|
|
7
|
+
# native `Helpers::Pages::TypedFieldsPairing` (label + type matching over
|
|
8
|
+
# GraphQL DataField types); the actual per-field content copy dispatches on the
|
|
9
|
+
# GraphQL DataField TYPE STRING (mirroring how OozeHandlers#merge_values ports
|
|
10
|
+
# the v2 type-class dispatch).
|
|
11
|
+
#
|
|
12
|
+
# It reproduces the SHAPE of RegisterMigrationCase natively:
|
|
13
|
+
# * iterate the source register (inherited from Register::Base#each_page)
|
|
14
|
+
# * draft a new entry of TEMPLATE_ID (via the native Creatable)
|
|
15
|
+
# * pair the source fields to the draft fields and copy content
|
|
16
|
+
# * run the unpaired / multi-pair reports (data-loss visibility)
|
|
17
|
+
# * let a subclass finalise the draft via #custom_processing
|
|
18
|
+
#
|
|
19
|
+
# As with the rest of the native samples layer this is NON-BREAKING and
|
|
20
|
+
# additive: no OozeSamples name is flipped and the legacy
|
|
21
|
+
# HelpersMigration::TypedFieldsPairing / RegisterMigrationCase are untouched.
|
|
22
|
+
#
|
|
23
|
+
# == What differs from the v2 case
|
|
24
|
+
# * dispatch is on GraphQL DataField type strings (PlainText/Select/...) not
|
|
25
|
+
# V2 component classes / snake_case types
|
|
26
|
+
# * the per-page persistence is the native create path (Creatable#create_ooze →
|
|
27
|
+
# graphql.pages.create), not the v2 batch queue
|
|
28
|
+
# * the v2 JSON `field_maps` / `copy_hooked_fields` mapping layer is NOT ported
|
|
29
|
+
# here (that consumes the v2 `with_fields`/regex-hook helpers); this scaffold
|
|
30
|
+
# covers the generic typed pairing. Mapped-field hooks are a later increment.
|
|
31
|
+
#
|
|
32
|
+
# == Subclass interface
|
|
33
|
+
#
|
|
34
|
+
# class Custom::UseCase::MigrateTraining < Eco::API::UseCases::GraphQL::Samples::Pages::Register::MigrationCase
|
|
35
|
+
# name 'migrate-training'
|
|
36
|
+
# register_id 'REG_SOURCE' # source register to read from
|
|
37
|
+
# TEMPLATE_ID = 'TPL_DEST' # destination template to create entries of
|
|
38
|
+
#
|
|
39
|
+
# # Optional: finalise the draft after generic pairing has run.
|
|
40
|
+
# def custom_processing(draft, source, fields_tracker: nil)
|
|
41
|
+
# draft.name = source.name
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# Expects `options[:source][:register_id]` (or class `register_id`) for the
|
|
46
|
+
# source scope, and a destination template id via the `TEMPLATE_ID` constant
|
|
47
|
+
# (overridable through #destination_template_id).
|
|
48
|
+
class Register::MigrationCase < Register::Base
|
|
49
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Copying
|
|
50
|
+
|
|
51
|
+
name 'graphql-register-migration-case'
|
|
52
|
+
type :other
|
|
53
|
+
|
|
54
|
+
# Destination template constant (subclasses set this).
|
|
55
|
+
TEMPLATE_ID = nil
|
|
56
|
+
|
|
57
|
+
# whether or not it should try to pair src <-> dst ximport fields
|
|
58
|
+
EXCLUDE_XIMPORT = false
|
|
59
|
+
# whether or not it should warn on unpaired destination ximport fields
|
|
60
|
+
REPORT_DST_XIMPORT = true
|
|
61
|
+
# whether or not it should warn on unpaired source fields that are empty (no data loss)
|
|
62
|
+
REPORT_SRC_EMPTY = false
|
|
63
|
+
# whether or not it should warn on unpaired source fields that are deindexed
|
|
64
|
+
REPORT_SRC_DEINDEX = false
|
|
65
|
+
|
|
66
|
+
# One source entry → one new destination entry.
|
|
67
|
+
#
|
|
68
|
+
# Drafts a destination page of `destination_template_id`, pairs the source
|
|
69
|
+
# fields to the draft's fields, copies content across, runs the reports, and
|
|
70
|
+
# (optionally) hands the draft to #custom_processing for finalising.
|
|
71
|
+
def process_page(source)
|
|
72
|
+
draft_reference = object_reference(source)
|
|
73
|
+
creating_new_page(draft_reference, template_id: destination_template_id) do |draft|
|
|
74
|
+
pairing_tracking = copy_pairings(source, draft)
|
|
75
|
+
|
|
76
|
+
if respond_to?(:custom_processing, true)
|
|
77
|
+
custom_processing(draft, source, fields_tracker: pairing_tracking)
|
|
78
|
+
elsif block_given?
|
|
79
|
+
yield(draft, source, fields_tracker: pairing_tracking)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
report_pairing(pairing_tracking)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
protected
|
|
87
|
+
|
|
88
|
+
# The destination template to create entries of. Override or set TEMPLATE_ID.
|
|
89
|
+
def destination_template_id
|
|
90
|
+
tpl = self.class::TEMPLATE_ID
|
|
91
|
+
raise ArgumentError, "No destination template configured (set #{self.class}::TEMPLATE_ID)" unless tpl
|
|
92
|
+
|
|
93
|
+
tpl
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Pair the generic typed fields and copy their content. Returns the tracker so
|
|
99
|
+
# the caller can inspect unpaired / multi-paired fields.
|
|
100
|
+
def copy_pairings(src, dst)
|
|
101
|
+
after_copy = respond_to?(:paired_fields_post_callback, true) ? method(:paired_fields_post_callback) : nil
|
|
102
|
+
|
|
103
|
+
copy_generic_paired_fields(
|
|
104
|
+
src,
|
|
105
|
+
dst,
|
|
106
|
+
exclude_ximport: self.class::EXCLUDE_XIMPORT,
|
|
107
|
+
&after_copy
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def report_pairing(tracker)
|
|
112
|
+
msg = tracker.report_src_unpaired(
|
|
113
|
+
only_present: !self.class::REPORT_SRC_EMPTY,
|
|
114
|
+
only_indexed: !self.class::REPORT_SRC_DEINDEX
|
|
115
|
+
)
|
|
116
|
+
log(:warn) { msg } if msg
|
|
117
|
+
|
|
118
|
+
msg = tracker.report_multi_pairs(
|
|
119
|
+
only_present: !self.class::REPORT_SRC_EMPTY,
|
|
120
|
+
only_indexed: !self.class::REPORT_SRC_DEINDEX,
|
|
121
|
+
dst_exclude_ximport: !self.class::REPORT_DST_XIMPORT
|
|
122
|
+
)
|
|
123
|
+
log(:warn) { msg } if msg
|
|
124
|
+
|
|
125
|
+
msg = tracker.report_dst_unpaired(
|
|
126
|
+
only_required: false,
|
|
127
|
+
exclude_ximport: !self.class::REPORT_DST_XIMPORT
|
|
128
|
+
)
|
|
129
|
+
log(:warn) { msg } if msg
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|