eco-helpers 3.2.14 → 3.2.23
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 +221 -0
- data/lib/eco/api/usecases/default/pages.rb +30 -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 +223 -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/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/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 +47 -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,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 the ecoportal-api-graphql repo's internal docs.
|
|
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,41 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Base class for **org-wide** page processing use cases.
|
|
3
|
+
#
|
|
4
|
+
# Identical to Page::Base but with no default register scope — search_conf
|
|
5
|
+
# starts empty (all org pages). Subclass adds filters via super.
|
|
6
|
+
#
|
|
7
|
+
# == When to use which
|
|
8
|
+
#
|
|
9
|
+
# Page::Base — one register, most scripts ("go through all pages in Reg X")
|
|
10
|
+
# OrgPage::Base — entire org (cross-register audits, archive sweeps, bulk ops)
|
|
11
|
+
#
|
|
12
|
+
# == Subclass interface
|
|
13
|
+
#
|
|
14
|
+
# class Custom::UseCase::ArchiveStaleDrafts < Eco::API::UseCases::GraphQL::Samples::Pages::OrgPage::Base
|
|
15
|
+
# name 'archive-stale-drafts'
|
|
16
|
+
# batch_size 100
|
|
17
|
+
#
|
|
18
|
+
# def search_conf
|
|
19
|
+
# super.filter(state_is(:draft))
|
|
20
|
+
# .filter(updated_since('2025-01-01'))
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def process_page(page)
|
|
24
|
+
# return skip('already archived') if page.archived?
|
|
25
|
+
# log(:info) { "Archiving: #{page.name}" }
|
|
26
|
+
# graphql.page.archive(input: { id: page.id }) unless simulate?
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
class OrgPage::Base < Page::Base
|
|
30
|
+
name 'graphql-org-page-base'
|
|
31
|
+
type :other
|
|
32
|
+
|
|
33
|
+
require_relative 'dsl'
|
|
34
|
+
include Eco::API::UseCases::GraphQL::Samples::Pages::OrgPage::DSL
|
|
35
|
+
|
|
36
|
+
# No default register scope. Subclass adds filters via super.
|
|
37
|
+
def search_conf
|
|
38
|
+
sc.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::OrgPage
|
|
2
|
+
# DSL concern for org-wide page use cases.
|
|
3
|
+
# Includes Page::DSL — all SearchConf helpers are available.
|
|
4
|
+
# Add org-page-specific helpers here as the pattern grows.
|
|
5
|
+
module DSL
|
|
6
|
+
include Eco::API::UseCases::GraphQL::Samples::Pages::Page::DSL
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Base class for **register-scoped** page processing use cases.
|
|
3
|
+
#
|
|
4
|
+
# Provides the iteration + KPI scaffolding mirroring RegisterUpdateCase
|
|
5
|
+
# from OozeSamples, but working natively with the GraphQL stack.
|
|
6
|
+
#
|
|
7
|
+
# == Subclass interface
|
|
8
|
+
#
|
|
9
|
+
# class Custom::UseCase::UpdateRiskRating < Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
|
|
10
|
+
# name 'update-risk-rating'
|
|
11
|
+
# register_id 'REG_ABC123'
|
|
12
|
+
# batch_size 50
|
|
13
|
+
#
|
|
14
|
+
# # Add filters on top of the default register scope.
|
|
15
|
+
# def search_conf
|
|
16
|
+
# super.filter(state_is(:active))
|
|
17
|
+
# .filter(updated_since(options.dig(:filters, :from).to_s))
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Transform or act on each page; call update_page(page) to persist.
|
|
21
|
+
# def process_page(page)
|
|
22
|
+
# field = page.components.get_by_name('Risk Level')
|
|
23
|
+
# return skip('no risk field') unless field
|
|
24
|
+
# page.name = "[HIGH] #{page.name}" if field.value == 'High'
|
|
25
|
+
# update_page(page)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# == What the base provides
|
|
30
|
+
# * process_page(page) — override point (required)
|
|
31
|
+
# * search_conf — override point (optional; default scopes to register_id)
|
|
32
|
+
# * update_page(page) — from_model + simulate? guard + error tracking
|
|
33
|
+
# * skip(reason) — record a non-error skip
|
|
34
|
+
# * each_page — cursor-paginated org search
|
|
35
|
+
# * KPI tracking — total / processed / updated / skipped / failed
|
|
36
|
+
class Page::Base < Eco::API::UseCases::GraphQL::Base
|
|
37
|
+
name 'graphql-page-base'
|
|
38
|
+
type :other
|
|
39
|
+
|
|
40
|
+
require_relative 'dsl'
|
|
41
|
+
include Eco::API::UseCases::GraphQL::Samples::Pages::Page::DSL
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def batch_size(size = nil)
|
|
45
|
+
@batch_size ||= 50
|
|
46
|
+
return @batch_size unless size
|
|
47
|
+
@batch_size = size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def register_id(id = nil)
|
|
51
|
+
@register_id = id if id
|
|
52
|
+
@register_id
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
attr_reader :total_pages, :processed_pages, :updated_pages
|
|
57
|
+
attr_reader :failed_pages, :skipped_pages
|
|
58
|
+
|
|
59
|
+
# Entry point — called by GraphQL::Base#main.
|
|
60
|
+
def process
|
|
61
|
+
init_kpis
|
|
62
|
+
each_page do |page|
|
|
63
|
+
process_page(page)
|
|
64
|
+
@processed_pages += 1
|
|
65
|
+
end
|
|
66
|
+
log_kpis
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# == Subclass override points ==============================================
|
|
70
|
+
|
|
71
|
+
# Transform or act on one page. Call update_page(page) to persist changes.
|
|
72
|
+
def process_page(_page)
|
|
73
|
+
raise NotImplementedError, "Implement #process_page in #{self.class}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Return a SearchConf for the page search. Override to add filters.
|
|
77
|
+
# Default: scoped to register_id when set, otherwise all pages.
|
|
78
|
+
def search_conf
|
|
79
|
+
conf = sc.new
|
|
80
|
+
reg = self.class.register_id
|
|
81
|
+
conf = conf.filter(in_register(reg)) if reg
|
|
82
|
+
conf
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
protected
|
|
86
|
+
|
|
87
|
+
# Record a skip (not an error — the page was intentionally not processed).
|
|
88
|
+
def skip(reason = nil)
|
|
89
|
+
@skipped_pages += 1
|
|
90
|
+
log(:debug) { "Skipped#{reason ? ": #{reason}" : ''}" }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Persist a page. Wraps Input::Page::Update.from_model, respects simulate?,
|
|
94
|
+
# and tracks KPIs. Returns nil on simulate or no-change; Response otherwise.
|
|
95
|
+
def update_page(page, **kargs)
|
|
96
|
+
if simulate?
|
|
97
|
+
@skipped_pages += 1
|
|
98
|
+
log(:debug) { "Simulate — would update page #{page.id}" }
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
response = graphql.pages.update(page, **kargs)
|
|
102
|
+
if response.nil?
|
|
103
|
+
@skipped_pages += 1
|
|
104
|
+
elsif response.success?
|
|
105
|
+
@updated_pages += 1
|
|
106
|
+
else
|
|
107
|
+
@failed_pages += 1
|
|
108
|
+
log(:error) { "Failed to update page #{page.id}: #{response.body}" }
|
|
109
|
+
end
|
|
110
|
+
response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Cursor-paginated iteration over pages matching search_conf.
|
|
114
|
+
# Uses org search (currentOrganization.pages) — returns full PageUnion
|
|
115
|
+
# with patchVer and field IDs (required for update workflows).
|
|
116
|
+
def each_page(conf: search_conf, first: self.class.batch_size, &block)
|
|
117
|
+
graphql.pages.each(search_conf: conf.to_h, first: first) do |page|
|
|
118
|
+
@total_pages += 1
|
|
119
|
+
yield page
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def init_kpis
|
|
126
|
+
@total_pages = 0
|
|
127
|
+
@processed_pages = 0
|
|
128
|
+
@updated_pages = 0
|
|
129
|
+
@failed_pages = 0
|
|
130
|
+
@skipped_pages = 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_kpis
|
|
134
|
+
log(:info) { kpis_message }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def kpis_message
|
|
138
|
+
[
|
|
139
|
+
'Run end:',
|
|
140
|
+
" * Pages found: #{total_pages}",
|
|
141
|
+
" * Pages processed: #{processed_pages}",
|
|
142
|
+
" * Updated: #{updated_pages}",
|
|
143
|
+
" * Skipped: #{skipped_pages}",
|
|
144
|
+
" * Failed: #{failed_pages}"
|
|
145
|
+
].join("\n")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Page
|
|
2
|
+
# DSL concern — included in Page::Base and available to all Page subclasses.
|
|
3
|
+
# Provides the SearchConf accessor and common filter shorthand methods.
|
|
4
|
+
#
|
|
5
|
+
# Include this in any class that needs SearchConf DSL access without
|
|
6
|
+
# inheriting the full Page::Base iteration scaffolding.
|
|
7
|
+
module DSL
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Lazy accessor for the SearchConf builder class.
|
|
11
|
+
# Resolved at runtime so the ecoportal-api-graphql gem is guaranteed to be loaded.
|
|
12
|
+
# Subclasses can also define a local constant for brevity:
|
|
13
|
+
# SearchConf = Ecoportal::API::GraphQL::Input::SearchConf
|
|
14
|
+
def sc
|
|
15
|
+
Ecoportal::API::GraphQL::Input::SearchConf
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convenience: build a register-scoped filter object.
|
|
19
|
+
def in_register(*ids)
|
|
20
|
+
sc.in_register(*ids)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Convenience: build a state equality filter.
|
|
24
|
+
def state_is(value)
|
|
25
|
+
sc[:state].is(value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convenience: build an externalId equality filter.
|
|
29
|
+
def external_id_eq(value)
|
|
30
|
+
sc[:external_id].eq(value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convenience: build an updated_at since filter.
|
|
34
|
+
def updated_since(iso8601, time_zone: 'UTC')
|
|
35
|
+
sc[:updated_at].since(iso8601, time_zone: time_zone)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|