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,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!({
|
|
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,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,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
|