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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -0
  3. data/lib/eco/api/usecases/default/pages.rb +30 -0
  4. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
  5. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
  6. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
  7. data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +223 -0
  8. data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
  9. data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
  10. data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
  11. data/lib/eco/api/usecases/graphql/compat.rb +11 -0
  12. data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
  13. data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
  14. data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
  15. data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
  16. data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
  17. data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
  18. data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
  19. data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
  20. data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
  21. data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
  22. data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
  23. data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
  24. data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
  25. data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
  26. data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
  27. data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
  28. data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
  29. data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
  30. data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
  31. data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
  32. data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
  33. data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
  34. data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
  35. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
  36. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
  37. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
  38. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
  39. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
  40. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
  41. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
  42. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
  43. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
  44. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
  45. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
  46. data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
  47. data/lib/eco/api/usecases/graphql/samples/pages.rb +9 -0
  48. data/lib/eco/api/usecases/graphql/samples.rb +1 -0
  49. data/lib/eco/api/usecases/graphql.rb +1 -0
  50. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
  51. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +13 -3
  52. data/lib/eco/version.rb +1 -1
  53. metadata +47 -15
  54. data/.gitignore +0 -23
  55. data/.idea/.gitignore +0 -10
  56. data/.markdownlint.json +0 -4
  57. data/.rspec +0 -3
  58. data/.rubocop.yml +0 -103
  59. data/.ruby-version +0 -1
  60. data/.yardopts +0 -10
  61. data/Gemfile +0 -8
  62. data/Rakefile +0 -38
  63. data/eco-helpers.gemspec +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d67a16095de2e32c2c627214b0254df6d2685e0591ab6295082736e52494c4d3
4
- data.tar.gz: 60835a688189d8feda9bdc6198bbdb0cfaa9e9f95e5c7521f36cedbec706c1b0
3
+ metadata.gz: 2803cb1dc211ef8682ebcd33e4007f4e748b5eb42f56ecbaa931b5258064ead8
4
+ data.tar.gz: 7c0f5f66654761ce6ee5cb37ca4eb354054161c0fc3914c37da27f7f6fce4273
5
5
  SHA512:
6
- metadata.gz: 0c1ded6a88ad0c6394e96cb511fddb5c5ac29635307affc1577d5eeb210f01ad8dd78edf78e449b9bca765a8754aa31083abb72beb60746ed21741e523878e6c
7
- data.tar.gz: a18f9c81c2430ba8251bdfc34e6e4e1d3da0fd3cbe4647226942469d8da1f71e00aa7e21e3162d9d89d492f98e800642c0132edc30a305515df67764c397de91
6
+ metadata.gz: 10235574cf1a38826f4de995d715747b65fd657e0ef4e183b04644f8e26bebb2471c4d118fbbc00eac72e174d74c72d4685c9fb49a9a3667d8320c005ef7983a
7
+ data.tar.gz: e9f15c9f0e9c5243c835402fe64efd1384021773d3e0c0d65fbbfe17bd10128e944932e9a8ac21501cf31025040c9fcc0b2b26b8eb70844fd631f899008c822c
data/CHANGELOG.md CHANGED
@@ -2,6 +2,227 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.2.23] - 2026-09-30
6
+
7
+ Security republish of 3.2.22. **Backwards-compatible; no code change**: same dependencies, same
8
+ behaviour. Supersedes the published versions of this line, which are to be yanked.
9
+
10
+ ### Security
11
+
12
+ - Removed customer and internal identifiers from shipped code comments and this changelog (a
13
+ comment listing customer organisations and their case names; internal repository names and
14
+ internal documentation paths). Comments and changelog only. It also carries the full-path packaging allowlist from 3.3.2, so the five internal
15
+ `CLAUDE.md` files that 3.2.22 packaged under `lib/` no longer ship.
16
+ - Packaging: the three runtime JSON files are declared in `.release-smoke-allow`, and the release
17
+ tasks accept `release/*` maintenance branches.
18
+
19
+ ## [3.2.22] - 2026-08-14
20
+
21
+ ### Fixed
22
+
23
+ - Republish of `3.2.21`: its `lib/eco/version.rb` shipped with doubled carriage returns
24
+ (`
25
+ ` line endings), causing a `warning: encountered in middle of line` on every
26
+ load. Functionally identical otherwise; `3.2.21` will be yanked.
27
+
28
+ ## [3.2.21] - 2026-08-14
29
+
30
+ ### Fixed
31
+
32
+ - Packaging-only republish of `3.2.19` with an allowlisted gemspec (backported from `3.3.0`).
33
+ Versions `3.2.16`-`3.2.19` shipped internal repository content (the repo's internal docs tooling,
34
+ `.claude/settings.json`) to rubygems.org via the old denylist `spec.files`; `3.2.19` is
35
+ yanked and `3.2.16`/`3.2.18` are queued for deletion by RubyGems support. `3.2.21` is the
36
+ identical `lib/` code packaged clean, so constraints like `'~> 3.2.0', '>= 3.2.19'` keep
37
+ resolving on the 3.2 line (its graphql dependency stays `~> 1.3`, satisfied by the clean
38
+ `1.3.16`). The version number `3.2.20` is intentionally skipped: it exists as a
39
+ tagged-but-deliberately-unpublished version (see the 3.3.0-era changelog corrections).
40
+
41
+ ## [3.2.19] - 2026-07-16
42
+
43
+ A customer's `cans-upsert` reliability adoption + an ooze KPI counter fix. **Backwards-compatible.**
44
+ Cut from the `v3.2.18` tag (not `master`, which carries the native GraphQL activity/dashboard
45
+ readers depending on the unreleased gem `1.4.0`), so this ships needing only the published
46
+ `ecoportal-api-graphql 1.3.14`.
47
+
48
+ ### Changed
49
+
50
+ - **Floor `ecoportal-api-graphql` to `>= 1.3.14`** (was `>= 1.3.11`) — pulls the HttpClient
51
+ 429/1015 resilience fix, so bulk `cans-upsert` / register-update live runs are no longer
52
+ aborted by a single Cloudflare edge rate-limit.
53
+
54
+ ### Fixed
55
+
56
+ - **Ooze update KPI counters now count GraphQL updates.** `RegisterUpdateCase` tallied
57
+ `updated`/`failed` only when the result `is_a?(Ecoportal::API::Common::Response)`, but the
58
+ GraphQL compat layer returns an `Ecoportal::API::GraphQL::Compat::Response` — it duck-types
59
+ `success?`/`status` yet is not in that class hierarchy, so every GraphQL update was silently
60
+ uncounted (`Updated 0 (attempted: N)`, `Failed 0`) even when the write applied. The guard is
61
+ now a duck-type (`respond_to?(:success?)`); `false`/`nil` returns (dry-run / no-op) still skip.
62
+ **Note:** this fixes the *report* only — the updates themselves were already applying.
63
+
64
+ ## [3.2.18] - 2026-07-10
65
+
66
+ Version-identity + regression-guard release. **No behaviour change** vs the fixed `3.2.17` build —
67
+ this bump exists so the LocationDraft fix can be pinned/verified by version, and adds an offline
68
+ guard so the dead-fragment class of bug can't reach production again.
69
+
70
+ ### Why this bump
71
+
72
+ `3.2.17` shipped as **two different builds under one version**: the version was bumped to `3.2.17`
73
+ on 2026-07-04 (`243822b9`), then the live-crash fix `352a9657` (LocationDraft dead-fragment
74
+ convention) landed on 2026-07-05 — keeping the same `3.2.17` label. Because `3.2.17` is installed
75
+ from git/path (unpublished), a consumer bundled in that ~26h window reports `3.2.17` yet lacks the
76
+ fix. This crashed the **live customer** org-structure sync again on 2026-07-09 with the exact
77
+ `uninitialized constant …Fragment::LocationDraft (NameError)`. Bumping to `3.2.18` makes the fixed
78
+ build unambiguously identifiable: any consumer on `>= 3.2.18` provably has the fix.
79
+
80
+ ### Added
81
+
82
+ - **Regression-guard spec for the dead-fragment convention.**
83
+ `spec/…/helpers/location/command/end_points/optimizations_spec.rb` scans every file under
84
+ `usecases/graphql/` and fails if any references a fragment via the removed `___Const__Fragment` /
85
+ `::Fragment::<Name>` constant convention (the exact NameError that crashed a customer sync), and asserts
86
+ the three Location command payload procs still route fragments through the `spread :Name` registry
87
+ DSL. It is a SOURCE lint, not a full offline render: rendering needs the graphlient fork's
88
+ `to_query_string`/`spread` DSL, but eco-helpers' own bundle resolves stock graphlient `0.8.0` (the
89
+ fork is pinned only in the deployment/gem bundles). If eco-helpers later pins the fork in dev/test,
90
+ promote this to a real render guard like the gem's `tests/validate_queries.rb`.
91
+
92
+ ## [3.2.17] - 2026-07-04
93
+
94
+ Native GraphQL template build+maintenance + ooze-native migration (strangler-fig): pure,
95
+ non-breaking additions built ALONGSIDE the OozeRedirect shim. No `OozeSamples::*` name is
96
+ flipped/redirected and the shim is untouched. **Not yet published** — the template Deploy loop binds
97
+ to the gem's `Diff` module (gem v1.3.11), which is tagged but unpublished.
98
+
99
+ ### Fixed
100
+
101
+ - **`-no-email` double-send guard typo.** `Location::Command::Service::TreeUpdate#email_digest` set
102
+ `{worfklow: {no_email: true}}` (misspelled key) after sending, so the "don't send the digest twice"
103
+ guard never took effect. Corrected to `workflow`. (Note: the primary `-no-email` check on the same
104
+ method already used the correct key, so a single `-no-email` run was already suppressed.)
105
+ - **Location command optimizations — dead fragment convention (live crash).**
106
+ `Helpers::Location::Command::EndPoints::Optimizations` referenced fragments via the removed
107
+ `___Const__Fragment` constant convention (`___Ecoportal__API__GraphQL__Fragment__LocationDraft` /
108
+ `…__LocationsError`), which the gem's registry-based fragments no longer expose as Ruby constants →
109
+ `uninitialized constant … Fragment::LocationDraft (NameError)` when building a locations-draft
110
+ `addCommands`/`create`/`publish` request. Crashed a customer's live tagtree / org-structure sync.
111
+ Now `spread :LocationDraft` / `spread :LocationsError`, matching the gem.
112
+
113
+ ### Dependencies
114
+
115
+ - **Raised floors to the fixed stack: `ecoportal-api-graphql >= 1.3.11`, `ecoportal-api-v2 >= 3.3.3`.**
116
+ Stops the stale gem 1.3.9 (missing the LocationStructure `updatedAt`/`createdAt` selection fixes from
117
+ 1.3.10) and the buggy v2 3.3.2 (Ruby-3.x `DoubleModel` cascade `TypeError`) from resolving on
118
+ consumers — both crashed a customer's live integration.
119
+
120
+ ### Added
121
+
122
+ - **`Helpers::Pages::OozeHandlers`** — native re-expression of `OozeSamples::Helpers::OozeHandlers`.
123
+ `merge_values(origin, append, type:, delimiter:)` dispatches on the GraphQL DataField TYPE STRING
124
+ (`PlainText`/`RichText` → newline; `Select`/`CrossReference`/`People`/`Checklist`/`ActionsList`/
125
+ `File`/`ImageGallery`/`Geo`/`Law` → delimiter join; `Date`/`Number`/`Gauge` → keep origin) instead
126
+ of `Ecoportal::API::V2::Page::Component::*` classes. `merge_arrays`/`array_indexes` ported verbatim.
127
+ Included into `Samples::Pages::Register::Base`, which also gains `#merge_field_values(field, append)`
128
+ (reads the field's own `#type` + current `#value`).
129
+ - **`Samples::Pages::Register::TargetOozesUpdateCase`** — native counterpart of
130
+ `OozeSamples::TargetOozesUpdateCase`, subclassing the native `Register::Base`. Reads target page ids
131
+ from `options[:source][:file]` (first CSV column, header dropped), batches them, previews with
132
+ duplicate detection + proceed prompt, fetches each via `graphql.pages.get`, dedups by id, and hands
133
+ each page to `process_page`. `#each_page` (register-search) is disabled here to prevent an
134
+ accidental org scan.
135
+ - **`Compat::Parity`** A/B parity harness — `RunResult` (canonical KPI + normalised per-page update
136
+ payload snapshot), `Comparison` (KPI/page-set/payload diffs + `equivalent?` verdict + report), and
137
+ `Harness` (orchestrates a legacy vs native run and compares; `.record` captures a run's observables).
138
+ Comparison logic is pure/offline-runnable; live capture of the two runs needs a test org + creds.
139
+ - **`Helpers::Pages::TypedFieldsPairing`** — native re-expression of
140
+ `OozeSamples::HelpersMigration::TypedFieldsPairing`. Pairs data fields across two page models by
141
+ SAME type + SAME label, in two screenings (exact label, then mild: case-insensitive a-z with
142
+ bracketed content removed). Groups/dispatches by the GraphQL DataField TYPE STRING
143
+ (`PlainText`/`Select`/`Date`/... per `DataField::TYPE_MAP`) rather than v2 snake_case component
144
+ types; `EXCLUDED_TYPES` = `%w[TagField Chart FrequencyRateChart]` (never-pair). Tracks unpaired
145
+ src/dst fields + multi-candidate sources, with `resolve`/report helpers. Legacy class untouched.
146
+ This is the cross-template field-pairing (migration), DISTINCT from `OozeHandlers#merge_values`
147
+ (value-merge of one already-paired field).
148
+ - **`Helpers::Pages::Copying`** — native counterpart of
149
+ `OozeSamples::HelpersMigration::Copying`. `copy_generic_paired_fields(src, dst)` runs
150
+ `TypedFieldsPairing` and copies each pair's content; `copy_field_content(src, dst)` dispatches on
151
+ the GraphQL DataField type string (`PlainText`/`Number`/`Date`/`Gauge` → `value=`, `RichText` →
152
+ `content=`, `Select` → `select_option`, `CrossReference` → `page_ids=`; other types left untouched).
153
+ The v2 regex/JSON hooked-field mapping layer (`copy_hooked_fields`) is NOT ported (later increment).
154
+ - **`Samples::Pages::Register::MigrationCase`** — native counterpart of
155
+ `OozeSamples::RegisterMigrationCase`, subclassing the native `Register::Base` and mixing in
156
+ `Copying`. Iterates a source register, drafts a NEW entry of `TEMPLATE_ID` (via native `Creatable`),
157
+ pairs the source fields to the draft's fields and copies content across, runs the unpaired /
158
+ multi-pair reports, and hands the draft to `#custom_processing` for finalising. Per-page persistence
159
+ is the native create path (not the v2 batch queue). Native-only; no OozeSamples name flipped.
160
+
161
+ Template automatic build & maintenance (Phase 5): native, offline-safe orchestration on top of the
162
+ released gem's `Diff::Deploy` / `Diff::VersionDiff` / `Builder::Template`. Additive, no case flipped.
163
+
164
+ - **`Samples::Pages::Template::Deploy`** — deploy → verify → monitor loop for template (workflow)
165
+ changes, driven by a gem `Diff::Deploy` command batch. Dry-run is the DEFAULT; live apply is gated
166
+ behind an explicit `commit: true` + executor. All components are session-less and offline-runnable
167
+ (the apply / re-read seams are injected).
168
+ - `Deploy::Applier` — wraps a gem `Diff::Deploy` plan. Dry-run records the batch it WOULD send;
169
+ `commit: true` applies it via any `execute_workflow_commands`-shaped executor (gem `Builder::Page`
170
+ facade adapted by `TargetBoundExecutor`). Refuses to apply while the plan has `unsupported`
171
+ changes unless `allow_partial: true` — nothing is guessed.
172
+ - `Deploy::RecordingExecutor` — offline stand-in that records batches and (with a `mutator`) replays
173
+ them onto an in-memory doc, so the pre/post drift check is runnable with no API call.
174
+ - `Deploy::DriftReport` — compares the APPLIED delta (a pre/post self-version `Diff::VersionDiff` of
175
+ the re-read target) to the INTENDED delta (the source diff the batch came from) on an id-free shape
176
+ signature; reports `missing` (under-applied) and `unexpected` (side-effect) changes honestly.
177
+ - `Deploy::Verifier` — pluggable post-deploy verification. `QaVerifier` wires the `ecoportal-qa`
178
+ check framework through a single seam when the gem is on the load path (NOT a hard dependency);
179
+ otherwise `NullVerifier` keeps the loop runnable and is honest that qa was unavailable.
180
+ - `Deploy::SyncReadiness` — given a register subset + its active template doc, reports which entries
181
+ CAN sync (all REQUIRED fields present, non-empty, and correctly typed) vs not, with per-entry
182
+ reasons.
183
+ - `Deploy::Loop` — orchestrates apply → drift → verify → sync-readiness and yields an aggregate
184
+ `Result` (`ok?` gate + human `report`). The applied-diff builder defaults to the gem's
185
+ `Diff::VersionDiff` (resolved lazily) and is injectable for offline use.
186
+ - **`Samples::Pages::Template::CsvBuild`** — CSV → template BUILD pipeline reusing the existing
187
+ `Template::CommandEmitter` + the released gem `Builder::Template#create(commands:)`.
188
+ - `CsvBuild::FormatMap` — the SINGLE, isolated place the assumed columnar format lives (column names
189
+ + option delimiters), mapping a CSV row to a format-agnostic `RowSpec`. ⚠ The exact ~300-template
190
+ CSV column format is due ~mid-July 2026 and is NOT yet confirmed — only this file (and its spec)
191
+ changes when the real format lands.
192
+ - `CsvBuild::Parser` — groups rows into a stage → section → field tree preserving first-seen order.
193
+ - `CsvBuild::Builder` — replays the tree onto `CommandEmitter` (same placeholder-id threading), emits
194
+ the ordered `WorkflowCommand` batch (select options → `addSelectFieldOption`), and stamps a hidden
195
+ per-section anchor field for the identity convention (hidden-field + description). Offline/dry-run
196
+ by default (`#commands`, `#preview`); `#create!(template_facade)` is the live path (needs creds).
197
+ - `Template::CommandEmitter#field` gains optional `description:`/`required:` pass-throughs (additive).
198
+ - **HONEST LIMITATIONS / deferred:**
199
+ - The released gem (1.3.9) does NOT yet ship `Diff::Deploy` / `Diff::VersionDiff` (unreleased on the
200
+ gem's main). The deploy loop is written against those APIs but touches them only via lazy/injected
201
+ seams, so it loads and its specs run offline today; a live drift check needs those classes released.
202
+ - The released gem's `addField` input drops any key outside
203
+ placeholderId/fieldType/label/stageId/sectionId/column, so the `description` identity token is NOT
204
+ yet persisted through addField — only the hidden anchor field's presence survives. Wiring is in
205
+ place so identity flows end-to-end once the gem's addField gains a description key.
206
+ - Live deploy/apply and live template creation both need credentials + a test org (the sandbox
207
+ cannot reach live). qa verification is stubbed behind `QaVerifier`'s single seam until qa is pinned.
208
+
209
+ ## [3.2.16] - 2026-07-02
210
+
211
+ APIv2→GraphQL cutover — the OozeRedirect compat layer that runs the maintained ooze integrations
212
+ (toocs-coding, cans-upsert, supplier-documents) on GraphQL. Requires ecoportal-api-graphql >= 1.3.9.
213
+
214
+ ### Fixed / Changed
215
+
216
+ - **OozeRedirect slimmed onto the base OozeSamples loop** — removed the `with_each_entry` /
217
+ `update_ooze` / `process_ooze` overrides that duplicated and silently bypassed the base loop
218
+ (KPIs, dedup, queue, `dry_run_feedback`). Fetches go through `ooze → apiv2.pages.get` and saves
219
+ through `update_oozes → update_ooze → apiv2.pages.update`; a captured `submit!`/`sign_off!` rides
220
+ along on the single `updatePage` via `Input::Page::Update.from_model` (reads the `_compat_*` flags).
221
+ - **`dirty?`** also treats a pending `submit!`/`sign_off!` as dirty (so a submit-only page still saves).
222
+ - **`OozeBaseCase#dry_run_feedback`** prints an affirmative `[dry-run] would create/update <ref>` line.
223
+ - **`RegisterUpdateCase#enqueue`** accepts any duck-typed entry (`#dirty?` + `#as_update`), not just
224
+ v2 `Page`/`PageStage` — GraphQL compat pages queue correctly under OozeRedirect.
225
+
5
226
  ## [3.2.15] - 2026-05-xx
6
227
 
7
228
  ### Added
@@ -0,0 +1,30 @@
1
+ module Eco
2
+ module API
3
+ class UseCases
4
+ class Default
5
+ # Namespace for CLI-integrated page use cases.
6
+ # These are concrete, fully functional cases exposed to all org environments.
7
+ #
8
+ # == Convention
9
+ # Cases here should:
10
+ # - Inherit from Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
11
+ # (or OrgPage::Base for org-wide operations)
12
+ # - Be registered in the org's config/cli.rb
13
+ # - Be as org-agnostic as possible (accept register_id, filters via options)
14
+ #
15
+ # == Adding a new default page case
16
+ # 1. Create the file here: default/pages/my_case.rb
17
+ # 2. Inherit from the appropriate pages sample base class
18
+ # 3. Require it below
19
+ # 4. Wire CLI registration in usecases/default_cases/samples.rb or the
20
+ # org's config/cli.rb
21
+ #
22
+ # == Currently
23
+ # No default page cases yet — add them here as common patterns emerge
24
+ # across org implementations.
25
+ module Pages
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ module Eco::API::UseCases::GraphQL::Compat::OozeRedirect
2
+ # Array subclass that calls the field's setter when elements are appended,
3
+ # ensuring GraphQL dirty-tracking fires on `fld.people_ids << value`.
4
+ class DirtyArray < Array
5
+ def initialize(field, data)
6
+ @field = field
7
+ super(Array(data))
8
+ end
9
+
10
+ def <<(value)
11
+ super
12
+ @field.people_ids = to_a
13
+ self
14
+ end
15
+
16
+ def push(*values)
17
+ super
18
+ @field.people_ids = to_a
19
+ self
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,241 @@
1
+ module Eco::API::UseCases::GraphQL::Compat::OozeRedirect
2
+ # Applies one-time compatibility patches to v2 and GraphQL field classes.
3
+ # Idempotent — safe to call multiple times (first call does the work, rest no-op).
4
+ #
5
+ # After apply!:
6
+ # 1. V2 type classes recognise GraphQL instances in case/when dispatch
7
+ # 2. GraphQL field classes respond to the v2 field interface
8
+ module FieldPatches
9
+ @applied = false
10
+
11
+ V2_TO_GQL = {
12
+ 'PlainTextField' => 'PlainText',
13
+ 'NumberField' => 'Number',
14
+ 'SelectionField' => 'Select',
15
+ 'PeopleField' => 'People',
16
+ 'ReferenceField' => 'CrossReference'
17
+ }.freeze
18
+
19
+ def self.apply!
20
+ return if @applied
21
+
22
+ # eco-helpers lazy-requires the gem only when session.api(version: :graphql) is first
23
+ # used (runtime). apply! runs at OozeRedirect.included (boot) — BEFORE that — so the
24
+ # gem classes aren't loaded yet: every safe_const below would return nil, the patches
25
+ # would silently no-op, and @applied would latch with nothing patched. Force the gem
26
+ # load now so all the GraphQL consts resolve.
27
+ require 'ecoportal/api-graphql'
28
+
29
+ @applied = true
30
+ patch_v2_type_dispatch!
31
+ patch_graphql_base_field!
32
+ patch_graphql_people!
33
+ patch_graphql_cross_reference!
34
+ patch_graphql_select!
35
+ end
36
+
37
+ # --- 1. V2 type dispatch -------------------------------------------------
38
+
39
+ # Reopen each V2 field class so that `===` also returns true for the
40
+ # equivalent GraphQL field type. This makes existing `case/when` dispatch
41
+ # work transparently without touching the end scripts.
42
+ def self.patch_v2_type_dispatch!
43
+ V2_TO_GQL.each do |v2_type, gql_type|
44
+ v2_klass = safe_const("Ecoportal::API::V2::Page::Component::#{v2_type}")
45
+ gql_klass = safe_const("Ecoportal::API::GraphQL::Base::Page::DataField::#{gql_type}")
46
+ next unless v2_klass && gql_klass
47
+
48
+ gql_ref = gql_klass # capture for the closures
49
+ v2_ref = v2_klass
50
+
51
+ # case/when dispatch: `V2Class === graphql_instance`.
52
+ v2_klass.define_singleton_method(:===) do |obj|
53
+ super(obj) || obj.is_a?(gql_ref)
54
+ end
55
+
56
+ # is_a?/kind_of? guards: `graphql_instance.is_a?(V2Class)` — scripts use both the
57
+ # case/when form AND explicit is_a? guards (e.g. cans update_select). === alone
58
+ # doesn't cover is_a?, so make the GraphQL field recognise its v2 counterpart too.
59
+ gql_klass.prepend(Module.new do
60
+ define_method(:is_a?) { |klass| klass == v2_ref || super(klass) }
61
+ define_method(:kind_of?) { |klass| klass == v2_ref || super(klass) }
62
+ end)
63
+ end
64
+ end
65
+
66
+ # --- 2. GraphQL base field: submit! + ooze stub --------------------------
67
+
68
+ def self.patch_graphql_base_field!
69
+ base = safe_const('Ecoportal::API::GraphQL::Base::Page::DataField')
70
+ return unless base
71
+
72
+ # submit! — stores a pending submit flag consumed by the infrastructure
73
+ base.class_eval do
74
+ def _compat_submit_pending?
75
+ @_v2_compat_submit_pending
76
+ end
77
+ end
78
+
79
+ # BasePage submit! — called as target.submit!(force: true) in scripts
80
+ page_base = safe_const('Ecoportal::API::GraphQL::Interface::BasePage')
81
+ return unless page_base
82
+
83
+ page_base.class_eval do
84
+ # v2-compat stage SUBMIT. Scripts call `target.submit!` (optionally pinning a
85
+ # stage via `submit!(stage_id:)`). Captures the intent; OozeRedirect#process_ooze
86
+ # consumes it → `graphql.pages.update(submit: true)`. Submitting completes the
87
+ # stage's fill-in task (the server requires all visible, non-hidden required
88
+ # fields to be filled). If the stage also has a review task configured, it then
89
+ # awaits a SEPARATE `sign_off!`. submit! does NOT sign off.
90
+ #
91
+ # `submit!(force: true)` maps to completePageTask.forcedComplete: true — the
92
+ # backend then SKIPS the empty visible-required-field check. This is admin/
93
+ # superuser-only server-side; the integration's service user must hold that
94
+ # permission (it did under APIv2, so the same account carries it on GraphQL).
95
+ def submit!(force: false, stage_id: nil)
96
+ @_v2_compat_submit_pending = true
97
+ @_v2_compat_submit_stage_id = stage_id
98
+ @_v2_compat_submit_force = force ? true : false
99
+ end
100
+
101
+ # v2-compat stage SIGN-OFF. Scripts call `target.sign_off!` (optionally
102
+ # `sign_off!(stage_id:)`). Per the platform model the stage tasks are sequential
103
+ # (fill-in → review): signing off a stage that has NOT been submitted yet also
104
+ # submits it — a permitted user may submit + sign off in one go (also the page-
105
+ # creation path). process_ooze realises this as
106
+ # `updatePage(submit: true, completePageTask { signOff: true })`, which the
107
+ # backend treats as submit + inline review approval, advancing the stage.
108
+ def sign_off!(stage_id: nil)
109
+ @_v2_compat_sign_off_pending = true
110
+ @_v2_compat_sign_off_stage_id = stage_id
111
+ end
112
+
113
+ def _compat_submit?
114
+ @_v2_compat_submit_pending
115
+ end
116
+
117
+ def _compat_submit_stage_id
118
+ @_v2_compat_submit_stage_id
119
+ end
120
+
121
+ def _compat_submit_force?
122
+ @_v2_compat_submit_force ? true : false
123
+ end
124
+
125
+ def _compat_sign_off?
126
+ @_v2_compat_sign_off_pending
127
+ end
128
+
129
+ def _compat_sign_off_stage_id
130
+ @_v2_compat_sign_off_stage_id
131
+ end
132
+ end
133
+ end
134
+
135
+ # --- 3. People field -----------------------------------------------------
136
+
137
+ def self.patch_graphql_people!
138
+ klass = safe_const('Ecoportal::API::GraphQL::Base::Page::DataField::People')
139
+ return unless klass
140
+
141
+ klass.prepend(Module.new do
142
+ # Return a DirtyArray so fld.people_ids << value triggers dirty tracking
143
+ def people_ids
144
+ DirtyArray.new(self, Array(super))
145
+ end
146
+
147
+ # Stub: back-reference used in v2 warning messages
148
+ def ooze
149
+ Struct.new(:uid, :name, :id).new('(graphql)', label, id)
150
+ end
151
+ end)
152
+ end
153
+
154
+ # --- 4. CrossReference (v2 ReferenceField) field -------------------------
155
+
156
+ def self.patch_graphql_cross_reference!
157
+ klass = safe_const('Ecoportal::API::GraphQL::Base::Page::DataField::CrossReference')
158
+ return unless klass
159
+
160
+ klass.prepend(Module.new do
161
+ # v2 compat: fld.reference_ids → fld.page_ids
162
+ def reference_ids
163
+ page_ids || []
164
+ end
165
+
166
+ # v2 compat: fld.add(ep_id)
167
+ def add(ep_id)
168
+ return if ep_id.nil?
169
+
170
+ self.page_ids = (page_ids || []) + [ep_id] unless (page_ids || []).include?(ep_id)
171
+ end
172
+
173
+ # v2 compat: fld.clear
174
+ def clear
175
+ self.page_ids = []
176
+ end
177
+
178
+ def ooze
179
+ Struct.new(:uid, :name, :id).new('(graphql)', label, id)
180
+ end
181
+ end)
182
+ end
183
+
184
+ # --- 5. Select field -----------------------------------------------------
185
+
186
+ OptionStruct = Struct.new(:id, :name, :value, :selected)
187
+
188
+ def self.patch_graphql_select!
189
+ klass = safe_const('Ecoportal::API::GraphQL::Base::Page::DataField::Select')
190
+ return unless klass
191
+
192
+ klass.prepend(Module.new do
193
+ # v2 compat: fld.options → array of OptionStructs (respond to .name, .value)
194
+ def options
195
+ Array(doc['options'] || []).map do |opt|
196
+ FieldPatches::OptionStruct.new(opt['id'], opt['name'], opt['value'], opt['selected'])
197
+ end
198
+ end
199
+
200
+ # v2 compat: fld.select(value, by_name: false) or fld.select(name, by_name: true)
201
+ def select(val, by_name: false)
202
+ select_option(val.to_s)
203
+ self
204
+ end
205
+
206
+ # v2 compat: fld.deselect(value) — deselect one specific option
207
+ def deselect(val)
208
+ (doc['options'] || []).each do |opt|
209
+ target = val.to_s
210
+ if [opt['id'], opt['name'], opt['value']].map(&:to_s).include?(target)
211
+ opt['selected'] = false
212
+ end
213
+ end
214
+ # No explicit dirty call: mutating doc['options'] in place is picked up
215
+ # by DataField's LeafDiffService diff (same as native select_option /
216
+ # clear_selection). There is no mark_dirty! method on the field.
217
+ self
218
+ end
219
+
220
+ # v2 compat: fld.values — array of selected option values
221
+ def values
222
+ (doc['options'] || []).select { |opt| opt['selected'] }.map { |opt| opt['value'] }
223
+ end
224
+
225
+ def ooze
226
+ Struct.new(:uid, :name, :id).new('(graphql)', label, id)
227
+ end
228
+ end)
229
+ end
230
+
231
+ def self.safe_const(path)
232
+ path.split('::').reduce(Object) { |mod, c| mod.const_get(c) }
233
+ rescue NameError
234
+ nil
235
+ end
236
+
237
+ private_class_method :patch_v2_type_dispatch!, :patch_graphql_base_field!,
238
+ :patch_graphql_people!, :patch_graphql_cross_reference!,
239
+ :patch_graphql_select!, :safe_const
240
+ end
241
+ end
@@ -0,0 +1,73 @@
1
+ module Eco::API::UseCases::GraphQL::Compat::OozeRedirect
2
+ # Extends OozeRedirect with Force / binding support via the GraphQL
3
+ # executeWorkflowCommands mutation.
4
+ #
5
+ # Automatically included by OozeRedirect when the ForceFields fragment is
6
+ # available in the loaded gem version. No additional include is needed in
7
+ # end scripts — the same single include covers forces too:
8
+ #
9
+ # include Eco::API::UseCases::GraphQL::Compat::OozeRedirect
10
+ #
11
+ # ## What ForceCompat adds
12
+ #
13
+ # 1. Override `with_each_entry` to fetch pages via Query::PageWithForces,
14
+ # which includes forces in the GraphQL response.
15
+ # 2. Override `process_ooze` to call save_force_changes! after the script
16
+ # runs, executing any accumulated force commands via executeWorkflowCommands.
17
+ #
18
+ # ## Accumulated commands
19
+ #
20
+ # Scripts interact with forces exactly as in v2:
21
+ # force.custom_script = new_script # queues editForce
22
+ # force.bindings.add(field, name: 'n') # queues addBinding
23
+ # force.bindings.delete!(binding) # queues removeBinding
24
+ #
25
+ # After process_ooze returns, ForceCompat collects all pending commands from
26
+ # all forces on the page and submits them in one executeWorkflowCommands call.
27
+ module ForceCompat
28
+ module Infrastructure
29
+ # Fetch pages with forces included in the GraphQL response.
30
+ def with_each_entry
31
+ warn_once(:force_fetch, 'ForceCompat: fetching pages with forces (Query::PageWithForces)')
32
+ target_ids.each do |page_id|
33
+ @target = Ecoportal::API::GraphQL::Query::PageWithForces.new(graphql.client).query(id: page_id)
34
+ if @target
35
+ yield
36
+ else
37
+ log(:warn) { "[ForceCompat] Could not fetch page #{page_id}" }
38
+ end
39
+ end
40
+ end
41
+
42
+ # After the script's process_ooze runs, submit all accumulated force commands.
43
+ def process_ooze
44
+ super
45
+ save_force_changes!
46
+ end
47
+
48
+ private
49
+
50
+ def save_force_changes!
51
+ return unless target.respond_to?(:forces)
52
+
53
+ force_col = target.forces
54
+ return unless force_col.dirty?
55
+
56
+ commands = force_col.pending_commands
57
+ return if commands.empty?
58
+
59
+ warn_once(:force_save, "ForceCompat: executing #{commands.size} force command(s) via executeWorkflowCommands")
60
+ return if simulate?
61
+
62
+ response = graphql.page.execute_force_commands(
63
+ id: target.id,
64
+ patch_ver: target.patchVer.to_i,
65
+ commands: commands
66
+ )
67
+ if response.respond_to?(:error?) && response.error?
68
+ log(:error) { "[ForceCompat] executeWorkflowCommands failed for page #{target.id}: #{response.body}" }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end