eco-helpers 3.2.16 → 3.2.17
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 +117 -0
- data/eco-helpers.gemspec +2 -2
- 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 +5 -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/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 +2 -0
- data/lib/eco/version.rb +1 -1
- metadata +34 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a51d3b328b3fda96aa2fa153f47481bad73417bc59b45000c7bea49d83d963f
|
|
4
|
+
data.tar.gz: 46a7268158b4d9d6695868ab2c89eb737764ee9896b53e956c2b08ffadb9c8a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ad1185638b499866d8052480b013cb46e2fd89d2cadf622bbe3e8392bdba50334019ca5ce08e969e8e53dc3f32d71eb7dfcf4246aec8524d9708862ab9ce5f2
|
|
7
|
+
data.tar.gz: ee15599fc00631f1e4f659df324f3b5a587d62b893f35ed436bd01b8c3d23c5ee1b8a72c55e615b890afce94a3bc80f51e4e2389449f89ba46a0b01031d93625
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.2.17] - 2026-07-04
|
|
6
|
+
|
|
7
|
+
Native GraphQL template build+maintenance + ooze-native migration (strangler-fig): pure,
|
|
8
|
+
non-breaking additions built ALONGSIDE the OozeRedirect shim. No `OozeSamples::*` name is
|
|
9
|
+
flipped/redirected and the shim is untouched. **Not yet published** — the template Deploy loop binds
|
|
10
|
+
to the gem's `Diff` module (gem v1.3.11), which is tagged but unpublished.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`-no-email` double-send guard typo.** `Location::Command::Service::TreeUpdate#email_digest` set
|
|
15
|
+
`{worfklow: {no_email: true}}` (misspelled key) after sending, so the "don't send the digest twice"
|
|
16
|
+
guard never took effect. Corrected to `workflow`. (Note: the primary `-no-email` check on the same
|
|
17
|
+
method already used the correct key, so a single `-no-email` run was already suppressed.)
|
|
18
|
+
- **Location command optimizations — dead fragment convention (live crash).**
|
|
19
|
+
`Helpers::Location::Command::EndPoints::Optimizations` referenced fragments via the removed
|
|
20
|
+
`___Const__Fragment` constant convention (`___Ecoportal__API__GraphQL__Fragment__LocationDraft` /
|
|
21
|
+
`…__LocationsError`), which the gem's registry-based fragments no longer expose as Ruby constants →
|
|
22
|
+
`uninitialized constant … Fragment::LocationDraft (NameError)` when building a locations-draft
|
|
23
|
+
`addCommands`/`create`/`publish` request. Crashed the live act-gov tagtree / org-structure sync.
|
|
24
|
+
Now `spread :LocationDraft` / `spread :LocationsError`, matching the gem.
|
|
25
|
+
|
|
26
|
+
### Dependencies
|
|
27
|
+
|
|
28
|
+
- **Raised floors to the fixed stack: `ecoportal-api-graphql >= 1.3.11`, `ecoportal-api-v2 >= 3.3.3`.**
|
|
29
|
+
Stops the stale gem 1.3.9 (missing the LocationStructure `updatedAt`/`createdAt` selection fixes from
|
|
30
|
+
1.3.10) and the buggy v2 3.3.2 (Ruby-3.x `DoubleModel` cascade `TypeError`) from resolving on
|
|
31
|
+
consumers — both crashed the live act-gov integration.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`Helpers::Pages::OozeHandlers`** — native re-expression of `OozeSamples::Helpers::OozeHandlers`.
|
|
36
|
+
`merge_values(origin, append, type:, delimiter:)` dispatches on the GraphQL DataField TYPE STRING
|
|
37
|
+
(`PlainText`/`RichText` → newline; `Select`/`CrossReference`/`People`/`Checklist`/`ActionsList`/
|
|
38
|
+
`File`/`ImageGallery`/`Geo`/`Law` → delimiter join; `Date`/`Number`/`Gauge` → keep origin) instead
|
|
39
|
+
of `Ecoportal::API::V2::Page::Component::*` classes. `merge_arrays`/`array_indexes` ported verbatim.
|
|
40
|
+
Included into `Samples::Pages::Register::Base`, which also gains `#merge_field_values(field, append)`
|
|
41
|
+
(reads the field's own `#type` + current `#value`).
|
|
42
|
+
- **`Samples::Pages::Register::TargetOozesUpdateCase`** — native counterpart of
|
|
43
|
+
`OozeSamples::TargetOozesUpdateCase`, subclassing the native `Register::Base`. Reads target page ids
|
|
44
|
+
from `options[:source][:file]` (first CSV column, header dropped), batches them, previews with
|
|
45
|
+
duplicate detection + proceed prompt, fetches each via `graphql.pages.get`, dedups by id, and hands
|
|
46
|
+
each page to `process_page`. `#each_page` (register-search) is disabled here to prevent an
|
|
47
|
+
accidental org scan.
|
|
48
|
+
- **`Compat::Parity`** A/B parity harness — `RunResult` (canonical KPI + normalised per-page update
|
|
49
|
+
payload snapshot), `Comparison` (KPI/page-set/payload diffs + `equivalent?` verdict + report), and
|
|
50
|
+
`Harness` (orchestrates a legacy vs native run and compares; `.record` captures a run's observables).
|
|
51
|
+
Comparison logic is pure/offline-runnable; live capture of the two runs needs a test org + creds.
|
|
52
|
+
- **`Helpers::Pages::TypedFieldsPairing`** — native re-expression of
|
|
53
|
+
`OozeSamples::HelpersMigration::TypedFieldsPairing`. Pairs data fields across two page models by
|
|
54
|
+
SAME type + SAME label, in two screenings (exact label, then mild: case-insensitive a-z with
|
|
55
|
+
bracketed content removed). Groups/dispatches by the GraphQL DataField TYPE STRING
|
|
56
|
+
(`PlainText`/`Select`/`Date`/... per `DataField::TYPE_MAP`) rather than v2 snake_case component
|
|
57
|
+
types; `EXCLUDED_TYPES` = `%w[TagField Chart FrequencyRateChart]` (never-pair). Tracks unpaired
|
|
58
|
+
src/dst fields + multi-candidate sources, with `resolve`/report helpers. Legacy class untouched.
|
|
59
|
+
This is the cross-template field-pairing (migration), DISTINCT from `OozeHandlers#merge_values`
|
|
60
|
+
(value-merge of one already-paired field).
|
|
61
|
+
- **`Helpers::Pages::Copying`** — native counterpart of
|
|
62
|
+
`OozeSamples::HelpersMigration::Copying`. `copy_generic_paired_fields(src, dst)` runs
|
|
63
|
+
`TypedFieldsPairing` and copies each pair's content; `copy_field_content(src, dst)` dispatches on
|
|
64
|
+
the GraphQL DataField type string (`PlainText`/`Number`/`Date`/`Gauge` → `value=`, `RichText` →
|
|
65
|
+
`content=`, `Select` → `select_option`, `CrossReference` → `page_ids=`; other types left untouched).
|
|
66
|
+
The v2 regex/JSON hooked-field mapping layer (`copy_hooked_fields`) is NOT ported (later increment).
|
|
67
|
+
- **`Samples::Pages::Register::MigrationCase`** — native counterpart of
|
|
68
|
+
`OozeSamples::RegisterMigrationCase`, subclassing the native `Register::Base` and mixing in
|
|
69
|
+
`Copying`. Iterates a source register, drafts a NEW entry of `TEMPLATE_ID` (via native `Creatable`),
|
|
70
|
+
pairs the source fields to the draft's fields and copies content across, runs the unpaired /
|
|
71
|
+
multi-pair reports, and hands the draft to `#custom_processing` for finalising. Per-page persistence
|
|
72
|
+
is the native create path (not the v2 batch queue). Native-only; no OozeSamples name flipped.
|
|
73
|
+
|
|
74
|
+
Template automatic build & maintenance (Phase 5): native, offline-safe orchestration on top of the
|
|
75
|
+
released gem's `Diff::Deploy` / `Diff::VersionDiff` / `Builder::Template`. Additive, no case flipped.
|
|
76
|
+
|
|
77
|
+
- **`Samples::Pages::Template::Deploy`** — deploy → verify → monitor loop for template (workflow)
|
|
78
|
+
changes, driven by a gem `Diff::Deploy` command batch. Dry-run is the DEFAULT; live apply is gated
|
|
79
|
+
behind an explicit `commit: true` + executor. All components are session-less and offline-runnable
|
|
80
|
+
(the apply / re-read seams are injected).
|
|
81
|
+
- `Deploy::Applier` — wraps a gem `Diff::Deploy` plan. Dry-run records the batch it WOULD send;
|
|
82
|
+
`commit: true` applies it via any `execute_workflow_commands`-shaped executor (gem `Builder::Page`
|
|
83
|
+
facade adapted by `TargetBoundExecutor`). Refuses to apply while the plan has `unsupported`
|
|
84
|
+
changes unless `allow_partial: true` — nothing is guessed.
|
|
85
|
+
- `Deploy::RecordingExecutor` — offline stand-in that records batches and (with a `mutator`) replays
|
|
86
|
+
them onto an in-memory doc, so the pre/post drift check is runnable with no API call.
|
|
87
|
+
- `Deploy::DriftReport` — compares the APPLIED delta (a pre/post self-version `Diff::VersionDiff` of
|
|
88
|
+
the re-read target) to the INTENDED delta (the source diff the batch came from) on an id-free shape
|
|
89
|
+
signature; reports `missing` (under-applied) and `unexpected` (side-effect) changes honestly.
|
|
90
|
+
- `Deploy::Verifier` — pluggable post-deploy verification. `QaVerifier` wires the `ecoportal-qa`
|
|
91
|
+
check framework through a single seam when the gem is on the load path (NOT a hard dependency);
|
|
92
|
+
otherwise `NullVerifier` keeps the loop runnable and is honest that qa was unavailable.
|
|
93
|
+
- `Deploy::SyncReadiness` — given a register subset + its active template doc, reports which entries
|
|
94
|
+
CAN sync (all REQUIRED fields present, non-empty, and correctly typed) vs not, with per-entry
|
|
95
|
+
reasons.
|
|
96
|
+
- `Deploy::Loop` — orchestrates apply → drift → verify → sync-readiness and yields an aggregate
|
|
97
|
+
`Result` (`ok?` gate + human `report`). The applied-diff builder defaults to the gem's
|
|
98
|
+
`Diff::VersionDiff` (resolved lazily) and is injectable for offline use.
|
|
99
|
+
- **`Samples::Pages::Template::CsvBuild`** — CSV → template BUILD pipeline reusing the existing
|
|
100
|
+
`Template::CommandEmitter` + the released gem `Builder::Template#create(commands:)`.
|
|
101
|
+
- `CsvBuild::FormatMap` — the SINGLE, isolated place the assumed columnar format lives (column names
|
|
102
|
+
+ option delimiters), mapping a CSV row to a format-agnostic `RowSpec`. ⚠ The exact ~300-template
|
|
103
|
+
CSV column format is due ~mid-July 2026 and is NOT yet confirmed — only this file (and its spec)
|
|
104
|
+
changes when the real format lands.
|
|
105
|
+
- `CsvBuild::Parser` — groups rows into a stage → section → field tree preserving first-seen order.
|
|
106
|
+
- `CsvBuild::Builder` — replays the tree onto `CommandEmitter` (same placeholder-id threading), emits
|
|
107
|
+
the ordered `WorkflowCommand` batch (select options → `addSelectFieldOption`), and stamps a hidden
|
|
108
|
+
per-section anchor field for the identity convention (hidden-field + description). Offline/dry-run
|
|
109
|
+
by default (`#commands`, `#preview`); `#create!(template_facade)` is the live path (needs creds).
|
|
110
|
+
- `Template::CommandEmitter#field` gains optional `description:`/`required:` pass-throughs (additive).
|
|
111
|
+
- **HONEST LIMITATIONS / deferred:**
|
|
112
|
+
- The released gem (1.3.9) does NOT yet ship `Diff::Deploy` / `Diff::VersionDiff` (unreleased on the
|
|
113
|
+
gem's main). The deploy loop is written against those APIs but touches them only via lazy/injected
|
|
114
|
+
seams, so it loads and its specs run offline today; a live drift check needs those classes released.
|
|
115
|
+
- The released gem's `addField` input drops any key outside
|
|
116
|
+
placeholderId/fieldType/label/stageId/sectionId/column, so the `description` identity token is NOT
|
|
117
|
+
yet persisted through addField — only the hidden anchor field's presence survives. Wiring is in
|
|
118
|
+
place so identity flows end-to-end once the gem's addField gains a description key.
|
|
119
|
+
- Live deploy/apply and live template creation both need credentials + a test org (the sandbox
|
|
120
|
+
cannot reach live). qa verification is stubbed behind `QaVerifier`'s single seam until qa is pinned.
|
|
121
|
+
|
|
5
122
|
## [3.2.16] - 2026-07-02
|
|
6
123
|
|
|
7
124
|
APIv2→GraphQL cutover — the OozeRedirect compat layer that runs the maintained ooze integrations
|
data/eco-helpers.gemspec
CHANGED
|
@@ -42,8 +42,8 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
|
|
43
43
|
spec.add_dependency 'dotenv', '~> 3'
|
|
44
44
|
spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.14'
|
|
45
|
-
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.
|
|
46
|
-
spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.
|
|
45
|
+
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.11'
|
|
46
|
+
spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.3'
|
|
47
47
|
spec.add_dependency 'ed25519', '~> 1.2'
|
|
48
48
|
spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
|
|
49
49
|
spec.add_dependency 'fuzzy_match', '>= 2.1.0', '< 2.2'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Compat
|
|
2
|
+
module Parity
|
|
3
|
+
# The result of comparing a legacy RunResult (A) against a native RunResult (B).
|
|
4
|
+
#
|
|
5
|
+
# Pure value object: given two RunResults it computes the KPI deltas, the set of
|
|
6
|
+
# page ids only one side touched, and the per-page payload diffs. `equivalent?`
|
|
7
|
+
# is the go/no-go signal the migration flips a case name on.
|
|
8
|
+
class Comparison
|
|
9
|
+
attr_reader :a, :b
|
|
10
|
+
|
|
11
|
+
def initialize(run_a, run_b)
|
|
12
|
+
@a = run_a
|
|
13
|
+
@b = run_b
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# True when the two runs are indistinguishable on the canonical KPI set and on
|
|
17
|
+
# every per-page update payload.
|
|
18
|
+
def equivalent?
|
|
19
|
+
kpi_diff.empty? && only_in_a.empty? && only_in_b.empty? && payload_diffs.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Canonical KPI names whose values differ, => [a_value, b_value].
|
|
23
|
+
def kpi_diff
|
|
24
|
+
keys = a.kpis.keys | b.kpis.keys
|
|
25
|
+
keys.each_with_object({}) do |key, out|
|
|
26
|
+
av = a.kpis[key].to_i
|
|
27
|
+
bv = b.kpis[key].to_i
|
|
28
|
+
out[key] = [av, bv] if av != bv
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Page ids only the legacy run acted on.
|
|
33
|
+
def only_in_a
|
|
34
|
+
a.page_ids - b.page_ids
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Page ids only the native run acted on.
|
|
38
|
+
def only_in_b
|
|
39
|
+
b.page_ids - a.page_ids
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Per-page payload diffs for the ids BOTH runs touched, => [a_payload, b_payload].
|
|
43
|
+
def payload_diffs
|
|
44
|
+
(a.page_ids & b.page_ids).each_with_object({}) do |id, out|
|
|
45
|
+
ap = a.updates[id]
|
|
46
|
+
bp = b.updates[id]
|
|
47
|
+
out[id] = [ap, bp] if ap != bp
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Human-readable report of the divergences (empty when equivalent).
|
|
52
|
+
def report # rubocop:disable Metrics/AbcSize
|
|
53
|
+
return "PARITY OK — #{a.label} ≡ #{b.label}" if equivalent?
|
|
54
|
+
|
|
55
|
+
lines = ["PARITY MISMATCH — #{a.label} vs #{b.label}:"]
|
|
56
|
+
kpi_diff.each do |key, (av, bv)|
|
|
57
|
+
lines << " * KPI #{key}: #{a.label}=#{av} #{b.label}=#{bv}"
|
|
58
|
+
end
|
|
59
|
+
lines << " * only in #{a.label}: #{only_in_a.join(', ')}" unless only_in_a.empty?
|
|
60
|
+
lines << " * only in #{b.label}: #{only_in_b.join(', ')}" unless only_in_b.empty?
|
|
61
|
+
payload_diffs.each do |id, (ap, bp)|
|
|
62
|
+
lines << " * payload #{id}:"
|
|
63
|
+
lines << " #{a.label}: #{ap.inspect}"
|
|
64
|
+
lines << " #{b.label}: #{bp.inspect}"
|
|
65
|
+
end
|
|
66
|
+
lines.join("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Compat
|
|
2
|
+
module Parity
|
|
3
|
+
# A/B parity harness for the ooze -> native GraphQL strangler-fig migration.
|
|
4
|
+
#
|
|
5
|
+
# Runs the SAME logical register-processing case through the two code paths —
|
|
6
|
+
# A: legacy (v2 / OozeRedirect shim), B: native GraphQL — and asserts that the
|
|
7
|
+
# observable outcome (KPI counters + per-page update payloads) is equivalent.
|
|
8
|
+
#
|
|
9
|
+
# The comparison logic (RunResult / Comparison) is PURE and offline-runnable.
|
|
10
|
+
# The harness itself is a thin orchestrator: you hand it two callables, each of
|
|
11
|
+
# which runs its side and returns a RunResult. In a live run each callable would
|
|
12
|
+
# execute the case in `-simulate` mode against the same test org, using a
|
|
13
|
+
# Recorder to capture what each side WOULD write (no data is mutated). Wiring the
|
|
14
|
+
# two live runs needs credentials + a test org; the structural machinery here is
|
|
15
|
+
# verified offline.
|
|
16
|
+
#
|
|
17
|
+
# == Usage (live)
|
|
18
|
+
#
|
|
19
|
+
# harness = Parity::Harness.new(
|
|
20
|
+
# legacy: -> { Parity::Harness.record('legacy') { |rec| run_legacy_case(rec) } },
|
|
21
|
+
# native: -> { Parity::Harness.record('native') { |rec| run_native_case(rec) } }
|
|
22
|
+
# )
|
|
23
|
+
# comparison = harness.run
|
|
24
|
+
# raise comparison.report unless comparison.equivalent?
|
|
25
|
+
#
|
|
26
|
+
# == Usage (offline / spec)
|
|
27
|
+
#
|
|
28
|
+
# a = Parity::RunResult.new(label: 'legacy', kpis: {...}, updates: {...})
|
|
29
|
+
# b = Parity::RunResult.new(label: 'native', kpis: {...}, updates: {...})
|
|
30
|
+
# Parity::Harness.new(legacy: -> { a }, native: -> { b }).run.equivalent?
|
|
31
|
+
class Harness
|
|
32
|
+
# @param legacy [#call] returns the legacy-path RunResult.
|
|
33
|
+
# @param native [#call] returns the native-path RunResult.
|
|
34
|
+
def initialize(legacy:, native:)
|
|
35
|
+
@legacy = legacy
|
|
36
|
+
@native = native
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Execute both sides and compare. Returns a Parity::Comparison.
|
|
40
|
+
def run
|
|
41
|
+
a = @legacy.call
|
|
42
|
+
b = @native.call
|
|
43
|
+
assert_run_result!(a, 'legacy')
|
|
44
|
+
assert_run_result!(b, 'native')
|
|
45
|
+
a.compare_to(b)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Capture the observable outcome of a case run into a RunResult.
|
|
49
|
+
#
|
|
50
|
+
# Yields a Recorder. The caller runs the case, feeding each page's would-be
|
|
51
|
+
# update payload to `recorder.record_update(page_id, payload)` (typically wired
|
|
52
|
+
# by stubbing the case's `update_page` to call `graphql.pages.get_body(page)`
|
|
53
|
+
# instead of persisting). After the run, pass the case's KPI hash to
|
|
54
|
+
# `recorder.kpis = case.kpis_hash` (or set counters individually).
|
|
55
|
+
#
|
|
56
|
+
# @param label [String] 'legacy' or 'native'.
|
|
57
|
+
# @yieldparam recorder [Recorder]
|
|
58
|
+
# @return [RunResult]
|
|
59
|
+
def self.record(label)
|
|
60
|
+
recorder = Recorder.new(label)
|
|
61
|
+
yield(recorder) if block_given?
|
|
62
|
+
recorder.to_run_result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def assert_run_result!(obj, side)
|
|
68
|
+
return if obj.is_a?(RunResult)
|
|
69
|
+
|
|
70
|
+
raise TypeError, "The #{side} callable must return a Parity::RunResult, got #{obj.class}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Collects a run's KPIs + per-page update payloads, then bakes a RunResult.
|
|
74
|
+
class Recorder
|
|
75
|
+
attr_accessor :kpis
|
|
76
|
+
|
|
77
|
+
def initialize(label)
|
|
78
|
+
@label = label
|
|
79
|
+
@kpis = {}
|
|
80
|
+
@updates = {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Record what a run WOULD send for `page_id`. Later ids for the same page
|
|
84
|
+
# overwrite (the last state wins — matching a real save of the final page).
|
|
85
|
+
def record_update(page_id, payload)
|
|
86
|
+
@updates[page_id.to_s] = payload
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Set a single canonical/native KPI counter by name.
|
|
91
|
+
def kpi(name, value)
|
|
92
|
+
@kpis[name.to_sym] = value
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_run_result
|
|
97
|
+
RunResult.new(label: @label, kpis: @kpis, updates: @updates)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Compat
|
|
2
|
+
module Parity
|
|
3
|
+
# A normalised, comparable snapshot of ONE use-case run.
|
|
4
|
+
#
|
|
5
|
+
# The A/B parity harness reduces a run — whether it went through the legacy
|
|
6
|
+
# (v2 / OozeRedirect) path or the native GraphQL path — to just its OBSERVABLE
|
|
7
|
+
# outcome:
|
|
8
|
+
# * `kpis` — the KPI counter hash (search/retrieved/updated/created/… )
|
|
9
|
+
# * `updates` — the per-page update payload the run WOULD send, keyed by page id
|
|
10
|
+
#
|
|
11
|
+
# Two runs are "equivalent" when, after normalisation, both are identical. This
|
|
12
|
+
# is the unit the strangler-fig migration is gated on: flip a case name only once
|
|
13
|
+
# its native RunResult matches the legacy RunResult over the same input.
|
|
14
|
+
#
|
|
15
|
+
# Everything here is PURE (no session, no network) so equivalence can be asserted
|
|
16
|
+
# offline in specs; the live capture step (feeding real runs in) is the only part
|
|
17
|
+
# that needs credentials.
|
|
18
|
+
class RunResult
|
|
19
|
+
attr_reader :label, :kpis, :updates
|
|
20
|
+
|
|
21
|
+
# @param label [String] human tag for this side ('legacy' / 'native').
|
|
22
|
+
# @param kpis [Hash] KPI counters. Keys are symbolised; values coerced to Integer.
|
|
23
|
+
# @param updates [Hash{String=>Object}] page_id => update payload (any Hash-ish).
|
|
24
|
+
def initialize(label:, kpis: {}, updates: {})
|
|
25
|
+
@label = label.to_s
|
|
26
|
+
@kpis = normalize_kpis(kpis)
|
|
27
|
+
@updates = normalize_updates(updates)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The set of page ids this run acted on.
|
|
31
|
+
def page_ids
|
|
32
|
+
updates.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Compare against another RunResult, returning a Comparison value object.
|
|
36
|
+
def compare_to(other)
|
|
37
|
+
Comparison.new(self, other)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Convenience: are the two runs equivalent?
|
|
41
|
+
def equivalent_to?(other)
|
|
42
|
+
compare_to(other).equivalent?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Merge/normalise into a plain hash (handy for golden-file capture).
|
|
46
|
+
def to_h
|
|
47
|
+
{ label: label, kpis: kpis, updates: updates }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# KPI names differ between the legacy and native cases (e.g. `updated_oozes`
|
|
51
|
+
# vs `updated_pages`). We compare on a CANONICAL subset of counters that both
|
|
52
|
+
# sides expose the same way, mapping each side's names onto the canon.
|
|
53
|
+
CANON_KPI_ALIASES = {
|
|
54
|
+
search: %i[total_search_oozes total_search_pages search],
|
|
55
|
+
duplicated: %i[dupped_search_oozes dupped_search_pages duplicated],
|
|
56
|
+
retrieved: %i[retrieved_oozes retrieved_pages retrieved],
|
|
57
|
+
updated: %i[updated_oozes updated_pages updated],
|
|
58
|
+
created: %i[created_oozes created_pages created],
|
|
59
|
+
skipped: %i[skipped_pages skipped],
|
|
60
|
+
failed: %i[failed_update_oozes failed_pages failed]
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def normalize_kpis(raw)
|
|
66
|
+
source = raw.each_with_object({}) do |(k, v), out|
|
|
67
|
+
out[k.to_sym] = v
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
CANON_KPI_ALIASES.each_with_object({}) do |(canon, aliases), out|
|
|
71
|
+
key = aliases.find {|a| source.key?(a)}
|
|
72
|
+
out[canon] = key ? source[key].to_i : 0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalise each update payload so a superficial key-order / string-vs-symbol
|
|
77
|
+
# difference between the two paths does not read as a real divergence.
|
|
78
|
+
def normalize_updates(raw)
|
|
79
|
+
raw.each_with_object({}) do |(id, payload), out|
|
|
80
|
+
out[id.to_s] = deep_normalize(payload)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deep_normalize(value)
|
|
85
|
+
case value
|
|
86
|
+
when Hash
|
|
87
|
+
value.map {|k, v| [k.to_s, deep_normalize(v)]}.sort_by(&:first).to_h
|
|
88
|
+
when Array
|
|
89
|
+
value.map {|v| deep_normalize(v)}
|
|
90
|
+
else
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
module Eco::API::UseCases::GraphQL
|
|
2
2
|
module Compat
|
|
3
|
+
module Parity
|
|
4
|
+
end
|
|
3
5
|
end
|
|
4
6
|
end
|
|
5
7
|
|
|
6
8
|
require_relative 'compat/ooze_redirect'
|
|
9
|
+
require_relative 'compat/parity/run_result'
|
|
10
|
+
require_relative 'compat/parity/comparison'
|
|
11
|
+
require_relative 'compat/parity/harness'
|
|
@@ -29,7 +29,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
|
29
29
|
messages
|
|
30
30
|
}
|
|
31
31
|
draft { # rubocop:disable Style/BlockDelimiters
|
|
32
|
-
|
|
32
|
+
spread :LocationDraft
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
end
|
|
@@ -44,7 +44,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
|
44
44
|
messages
|
|
45
45
|
}
|
|
46
46
|
draft { # rubocop:disable Style/BlockDelimiters
|
|
47
|
-
|
|
47
|
+
spread :LocationDraft
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
end
|
|
@@ -54,7 +54,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
|
54
54
|
clientMutationId
|
|
55
55
|
ok
|
|
56
56
|
error { # rubocop:disable Style/BlockDelimiters
|
|
57
|
-
|
|
57
|
+
spread :LocationsError
|
|
58
58
|
}
|
|
59
59
|
errors { # rubocop:disable Style/BlockDelimiters
|
|
60
60
|
details
|
|
@@ -69,7 +69,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
|
|
|
69
69
|
__typename
|
|
70
70
|
}
|
|
71
71
|
error { # rubocop:disable Style/BlockDelimiters
|
|
72
|
-
|
|
72
|
+
spread :LocationsError
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers
|
|
2
|
+
module Pages
|
|
3
|
+
# Native GraphQL re-expression of OozeSamples::HelpersMigration::Copying.
|
|
4
|
+
#
|
|
5
|
+
# Provides the "pair source fields to destination fields and copy their
|
|
6
|
+
# content across" flow used by the native migration case. Pairing is delegated
|
|
7
|
+
# to `TypedFieldsPairing`; the per-field content copy (`copy_field_content`)
|
|
8
|
+
# dispatches on the GraphQL DataField TYPE STRING (PlainText/Select/Date/...),
|
|
9
|
+
# mirroring how OozeHandlers#merge_values ports the v2 type-class dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Only the generic-typed-pairing flow is ported (the v2 regex/JSON hooked-field
|
|
12
|
+
# mapping — `copy_hooked_fields` / `with_src_dst_field_pair` — is a later
|
|
13
|
+
# increment and is intentionally omitted here).
|
|
14
|
+
module Copying
|
|
15
|
+
include Eco::Language::AuxiliarLogger
|
|
16
|
+
include Eco::API::UseCases::GraphQL::Helpers::Pages::Shortcuts
|
|
17
|
+
|
|
18
|
+
def session
|
|
19
|
+
defined?(super) ? super : @session
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Pair the same-type/same-label fields of `src` and `dst`, and copy the
|
|
23
|
+
# content of each pair from source to destination.
|
|
24
|
+
#
|
|
25
|
+
# @param src [Object] the source page model.
|
|
26
|
+
# @param dst [Object] the destination (draft) page model.
|
|
27
|
+
# @param src_excluded [Array] source fields to exclude from pairing.
|
|
28
|
+
# @param dst_excluded [Array] destination fields to exclude from pairing.
|
|
29
|
+
# @param exclude_ximport [Boolean] skip ximport-marked fields.
|
|
30
|
+
# @yield [src_fld, dst_fld, pairing] optional post-copy callback per pair.
|
|
31
|
+
# @return [TypedFieldsPairing] the tracker (unpaired / multi-pair inspection).
|
|
32
|
+
def copy_generic_paired_fields(src, dst, src_excluded: [], dst_excluded: [], exclude_ximport: false)
|
|
33
|
+
TypedFieldsPairing.new(
|
|
34
|
+
src,
|
|
35
|
+
dst,
|
|
36
|
+
src_excluded: src_excluded,
|
|
37
|
+
dst_excluded: dst_excluded,
|
|
38
|
+
exclude_ximport: exclude_ximport
|
|
39
|
+
).tap do |pairing|
|
|
40
|
+
pairing.each_pair do |src_fld, dst_fld|
|
|
41
|
+
copy_field_content(src_fld, dst_fld)
|
|
42
|
+
yield(src_fld, dst_fld, pairing) if block_given?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Copy the content of `src` into `dst`, dispatching on the GraphQL DataField
|
|
48
|
+
# TYPE STRING. `src` and `dst` are guaranteed to be the same type by the
|
|
49
|
+
# pairing. Types with no meaningful/copyable content (or whose native setter
|
|
50
|
+
# is not yet supported) are left untouched.
|
|
51
|
+
#
|
|
52
|
+
# @param src [#type] the source data field.
|
|
53
|
+
# @param dst [#type] the destination data field.
|
|
54
|
+
# @return [Object] result of the per-type copy (or a "won't copy" marker).
|
|
55
|
+
def copy_field_content(src, dst)
|
|
56
|
+
case src.type.to_s
|
|
57
|
+
when 'PlainText', 'Number', 'Date', 'Gauge'
|
|
58
|
+
dst.value = src.value
|
|
59
|
+
when 'RichText'
|
|
60
|
+
dst.content = src.content
|
|
61
|
+
when 'Select'
|
|
62
|
+
dst.select_option(*src.selected_options.map { |opt| opt['id'] })
|
|
63
|
+
when 'CrossReference'
|
|
64
|
+
dst.page_ids = src.page_ids
|
|
65
|
+
else
|
|
66
|
+
"won't copy"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers
|
|
2
|
+
module Pages
|
|
3
|
+
# Native GraphQL re-expression of OozeSamples::Helpers::Creatable.
|
|
4
|
+
#
|
|
5
|
+
# Provides the "create a new entry (page) from a template" flow used by
|
|
6
|
+
# register-processing use cases. Mirrors the v2 Creatable structure
|
|
7
|
+
# (creating_new_page -> drafting_entry -> create_entry -> create_ooze) but
|
|
8
|
+
# talks to the native GraphQL stack (`graphql.pages.get_new` / `graphql.pages.create`)
|
|
9
|
+
# and respects `simulate?` for dry-run feedback.
|
|
10
|
+
#
|
|
11
|
+
# Included via the base-type guard: it can only be mixed into the native
|
|
12
|
+
# register case (Samples::Pages::Register::Base), which supplies the
|
|
13
|
+
# `create_ooze` hook + KPI counters it drives.
|
|
14
|
+
module Creatable
|
|
15
|
+
module InstanceMethods
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Draft a page from `template_id`, let the caller fill it, then persist it.
|
|
19
|
+
# @param draft_reference [String] human-readable label for logs.
|
|
20
|
+
# @param template_id [String] the template to build the draft from.
|
|
21
|
+
# @yield [draft] the built (still unsaved) draft page to mutate.
|
|
22
|
+
# @return [String, nil] the created page id, or nil on dry-run / failure.
|
|
23
|
+
def creating_new_page(draft_reference = 'new entry', template_id:)
|
|
24
|
+
page_id = nil
|
|
25
|
+
drafting_entry(template_id) do |draft|
|
|
26
|
+
yield(draft) if block_given?
|
|
27
|
+
|
|
28
|
+
if (page_id = create_entry(draft, reference: draft_reference))
|
|
29
|
+
log(:info) { "Page '#{page_id}' created successfully -- #{draft_reference}" }
|
|
30
|
+
elsif simulate?
|
|
31
|
+
log(:info) { "Simulated launch for #{draft_reference}" }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
page_id
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Object] a draft page of `template_id` (still not saved).
|
|
38
|
+
def drafting_entry(template_id)
|
|
39
|
+
raise ArgumentError, 'Expecting block, but not given' unless block_given?
|
|
40
|
+
|
|
41
|
+
draft = graphql.pages.get_new(template_id)
|
|
42
|
+
yield(draft)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Does the actual creation of the entry (wrapped so a per-item error does
|
|
46
|
+
# not abort the whole run).
|
|
47
|
+
# @return [String, nil] the created page id, or nil.
|
|
48
|
+
def create_entry(draft, reference: "new entry from #{draft&.template_id}")
|
|
49
|
+
with_rescue(reference) do
|
|
50
|
+
if (result = create_ooze(draft, template_id: draft.template_id))
|
|
51
|
+
return result.page_id
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
def included(base)
|
|
59
|
+
super
|
|
60
|
+
|
|
61
|
+
validate_base_type!(base)
|
|
62
|
+
base.include(InstanceMethods)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_base_type!(base)
|
|
66
|
+
return super if defined?(super)
|
|
67
|
+
|
|
68
|
+
register_base = Eco::API::UseCases::GraphQL::Samples::Pages::Register::Base
|
|
69
|
+
msg = "#{self} can only be included in #{register_base}"
|
|
70
|
+
msg << "\nCan't be included in #{base}"
|
|
71
|
+
raise LoadError, msg unless base <= register_base
|
|
72
|
+
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|