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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2803cb1dc211ef8682ebcd33e4007f4e748b5eb42f56ecbaa931b5258064ead8
|
|
4
|
+
data.tar.gz: 7c0f5f66654761ce6ee5cb37ca4eb354054161c0fc3914c37da27f7f6fce4273
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|