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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/lib/eco/api/usecases/CLAUDE.md +78 -0
  4. data/lib/eco/api/usecases/default/pages.rb +30 -0
  5. data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
  6. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
  7. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
  8. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
  9. data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
  10. data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
  11. data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
  12. data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
  13. data/lib/eco/api/usecases/graphql/compat.rb +11 -0
  14. data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
  15. data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
  16. data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
  17. data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
  18. data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
  19. data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
  20. data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
  21. data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
  22. data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
  23. data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
  24. data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
  25. data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
  26. data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
  27. data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
  28. data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
  29. data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
  30. data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
  31. data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
  32. data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
  33. data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
  34. data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
  35. data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
  36. data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
  37. data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
  38. data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
  39. data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
  40. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
  41. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
  42. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
  43. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
  44. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
  45. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
  46. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
  47. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
  48. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
  49. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
  50. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
  51. data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
  52. data/lib/eco/api/usecases/graphql/samples/pages.rb +9 -0
  53. data/lib/eco/api/usecases/graphql/samples.rb +1 -0
  54. data/lib/eco/api/usecases/graphql.rb +1 -0
  55. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
  56. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +13 -3
  57. data/lib/eco/version.rb +1 -1
  58. metadata +52 -15
  59. data/.gitignore +0 -23
  60. data/.idea/.gitignore +0 -10
  61. data/.markdownlint.json +0 -4
  62. data/.rspec +0 -3
  63. data/.rubocop.yml +0 -103
  64. data/.ruby-version +0 -1
  65. data/.yardopts +0 -10
  66. data/Gemfile +0 -8
  67. data/Rakefile +0 -38
  68. data/eco-helpers.gemspec +0 -63
@@ -0,0 +1,76 @@
1
+ # usecases/graphql/samples
2
+
3
+ Built-in GraphQL sample base classes shipped with the gem.
4
+ These are abstract/semi-abstract classes that org scripts inherit from.
5
+ Concrete, CLI-integrated cases go in `usecases/default/` instead.
6
+
7
+ ---
8
+
9
+ ## Hierarchy convention
10
+
11
+ Each domain follows:
12
+ ```
13
+ samples/<domain>.rb ← namespace loader (may also BE the base class)
14
+ samples/<domain>/
15
+ <level>/
16
+ dsl.rb ← DSL concern — include in base, available to subclasses
17
+ base.rb ← Base class (inherits from GraphQL::Base or parent level)
18
+ <functional_level>.rb ← Optional: opinionated subclass, inherit directly
19
+ <functional_level>/
20
+ dsl.rb ← Further DSL for that functional level
21
+ ```
22
+
23
+ DSLs are always **concerns (modules)** to include — never classes.
24
+ Bases are **classes** with the scaffolding logic.
25
+ Functional levels add opinionated defaults on top of base.
26
+
27
+ ---
28
+
29
+ ## Contents
30
+
31
+ | File | Class | Purpose |
32
+ |------|-------|---------|
33
+ | `samples/location.rb` | `Samples::Location` | Location structure management |
34
+ | `samples/location/command.rb` | `Samples::Location::Command` | Apply location tree commands |
35
+ | `samples/location/service.rb` | `Samples::Location::Service` | Tree diffing and conversion service |
36
+ | `samples/contractors.rb` | `Samples::Contractors` | Contractor entity base case |
37
+ | `samples/contractors/dsl.rb` | `Contractors::DSL` | Contractor helper mixin |
38
+ | `samples/pages.rb` | `Samples::Pages` (namespace) | Page processing cases loader |
39
+ | `samples/pages/page/dsl.rb` | `Pages::Page::DSL` | SearchConf helpers mixin |
40
+ | `samples/pages/page/base.rb` | `Pages::Page::Base` | Register-scoped page iteration |
41
+ | `samples/pages/org_page/dsl.rb` | `Pages::OrgPage::DSL` | Org-page DSL (extends Page::DSL) |
42
+ | `samples/pages/org_page/base.rb` | `Pages::OrgPage::Base` | Org-wide page iteration |
43
+
44
+ ---
45
+
46
+ ## Difference: samples vs org-specific cases vs default
47
+
48
+ | Layer | Location | Purpose |
49
+ |---|---|---|
50
+ | **samples** (here) | `eco-helpers/lib/.../graphql/samples/` | Abstract base classes — org scripts inherit |
51
+ | **default** | `eco-helpers/lib/.../usecases/default/` | Concrete CLI-integrated cases for ALL orgs |
52
+ | **org-specific** | `multi_org_api/{org}/config/graphql_cases/` | Org-specific implementations |
53
+
54
+ Org scripts inherit from `samples/`, optionally via `default/` as an intermediate layer.
55
+
56
+ ---
57
+
58
+ ## Adding a new built-in sample
59
+
60
+ 1. Create the case file in `samples/`:
61
+ ```ruby
62
+ # lib/eco/api/usecases/graphql/samples/my_domain.rb
63
+ class Eco::API::UseCases::GraphQL::Samples::MyDomain < Eco::API::UseCases::GraphQL::Base
64
+ name 'my-domain-case'
65
+ type :other
66
+
67
+ def process
68
+ # ...
69
+ end
70
+ end
71
+ ```
72
+
73
+ 2. Add `require_relative 'samples/my_domain'` to `samples.rb`.
74
+
75
+ If the case is page-centric, inherit from `PageCase` instead of `Base` to get
76
+ pagination, KPI tracking, and `update_page` for free.
@@ -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!({worfklow: {no_email: true}})
60
+ options.deep_merge!({workflow: {no_email: true}})
61
61
  end
62
62
  end
63
63
  end
@@ -0,0 +1,59 @@
1
+ # samples/pages
2
+
3
+ Base classes for GraphQL-native page processing use cases.
4
+
5
+ ---
6
+
7
+ ## Structure
8
+
9
+ ```
10
+ pages/
11
+ page/
12
+ dsl.rb ← Page::DSL — SearchConf helpers mixin (sc, in_register, state_is, ...)
13
+ base.rb ← Page::Base — register-scoped page iteration + KPI scaffolding
14
+ org_page/
15
+ dsl.rb ← OrgPage::DSL — extends Page::DSL (org-wide helpers)
16
+ base.rb ← OrgPage::Base — org-wide iteration (no default register scope)
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Inheritance
22
+
23
+ ```
24
+ GraphQL::Base
25
+ ↓
26
+ Samples::Pages::Page::Base (register-scoped, inherits Page::DSL)
27
+ ↓
28
+ Samples::Pages::OrgPage::Base (org-wide, overrides search_conf)
29
+ ↓
30
+ your org subclass
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Page::Base — register-scoped scripts
36
+
37
+ Override `process_page(page)` and optionally `search_conf`.
38
+ Set `register_id` and `batch_size` on the class.
39
+
40
+ ## OrgPage::Base — org-wide scripts
41
+
42
+ Same as Page::Base but `search_conf` starts empty.
43
+ Add your own filters via `super.filter(state_is(:active))` etc.
44
+
45
+ ---
46
+
47
+ ## Adding a functional level
48
+
49
+ If a common pattern emerges (e.g., "stage-submit scripts"), add:
50
+
51
+ ```
52
+ pages/page/
53
+ stage_submitter.rb ← Page::StageSubmitter < Page::Base
54
+ stage_submitter/
55
+ dsl.rb ← StageSubmitter::DSL
56
+ ```
57
+
58
+ Keep base.rb for the pure iteration scaffolding; put opinionated defaults in
59
+ the functional level so scripts can choose their entry point.
@@ -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,7 @@
1
+ module Eco::API::UseCases::GraphQL::Samples::Pages
2
+ module OrgPage
3
+ end
4
+ end
5
+
6
+ require_relative 'org_page/dsl'
7
+ require_relative 'org_page/base'
@@ -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
@@ -0,0 +1,7 @@
1
+ module Eco::API::UseCases::GraphQL::Samples::Pages
2
+ module Page
3
+ end
4
+ end
5
+
6
+ require_relative 'page/dsl'
7
+ require_relative 'page/base'
@@ -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