ecoportal-api-graphql 2.2.1 → 3.0.0
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 +405 -9
- data/README.gem.md +53 -0
- data/lib/ecoportal/api/common/graphql/auth_service.rb +1 -1
- data/lib/ecoportal/api/common/graphql/client.rb +38 -0
- data/lib/ecoportal/api/common/graphql/http_client.rb +39 -6
- data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +159 -11
- data/lib/ecoportal/api/graphql/base/temp_image.rb +28 -0
- data/lib/ecoportal/api/graphql/base.rb +1 -0
- data/lib/ecoportal/api/graphql/builder/template.rb +9 -4
- data/lib/ecoportal/api/graphql/compat/filter_translator.rb +1 -1
- data/lib/ecoportal/api/graphql/file_upload/client.rb +140 -35
- data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +6 -1
- data/lib/ecoportal/api/graphql/input/page/update.rb +110 -7
- data/lib/ecoportal/api/graphql/input/search_conf.rb +1 -1
- data/lib/ecoportal/api/graphql/model/temp_image.rb +10 -0
- data/lib/ecoportal/api/graphql/model/template/binding.rb +60 -0
- data/lib/ecoportal/api/graphql/model/template/command_grouper.rb +107 -0
- data/lib/ecoportal/api/graphql/model/template/command_normalizer.rb +116 -0
- data/lib/ecoportal/api/graphql/model/template/command_synthesis.rb +262 -0
- data/lib/ecoportal/api/graphql/model/template/field.rb +68 -0
- data/lib/ecoportal/api/graphql/model/template/force.rb +65 -0
- data/lib/ecoportal/api/graphql/model/template/helper.rb +32 -0
- data/lib/ecoportal/api/graphql/model/template/instance.rb +202 -0
- data/lib/ecoportal/api/graphql/model/template/node.rb +78 -0
- data/lib/ecoportal/api/graphql/model/template/option.rb +49 -0
- data/lib/ecoportal/api/graphql/model/template/read.rb +164 -0
- data/lib/ecoportal/api/graphql/model/template/section.rb +82 -0
- data/lib/ecoportal/api/graphql/model/template/stage.rb +49 -0
- data/lib/ecoportal/api/graphql/model/template/staged_executor.rb +236 -0
- data/lib/ecoportal/api/graphql/model/template.rb +33 -0
- data/lib/ecoportal/api/graphql/model.rb +1 -0
- data/lib/ecoportal/api/graphql/mutation/file_container/upload.rb +11 -4
- data/lib/ecoportal/api/graphql/mutation/image/upload.rb +88 -0
- data/lib/ecoportal/api/graphql/mutation/image.rb +14 -0
- data/lib/ecoportal/api/graphql/mutation/template/create.rb +35 -3
- data/lib/ecoportal/api/graphql/mutation/template/update.rb +4 -2
- data/lib/ecoportal/api/graphql/mutation.rb +1 -0
- data/lib/ecoportal/api/graphql/payload/images_upload.rb +14 -0
- data/lib/ecoportal/api/graphql/payload.rb +1 -0
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- metadata +35 -2
- data/README.md +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90830a81a4ad762cf9a4a74e08d3898076e15f09ce677e78e7f0d5809c5517fb
|
|
4
|
+
data.tar.gz: 04c5d2c47000df72cbb2a3a705622ccc6c2a80340bae6745dc088b5bf5151d73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fee4494637c2fc0d591a0dccbbbb740cb3f99809be5205bf25be36f9d6d42ee3a956a06d2700f5baba2cb9bb8682abf4f7b31d792f84e84992b2c485ba605b1d
|
|
7
|
+
data.tar.gz: df0b9542b045c52224135f8dcaa50f9579ddcf9363e3ea7e774e1330a5b69227aac8134e66a1752586b7f4bba94ff63d46c7f7eefeec36df25bcba041d8a5c98
|
data/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,191 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [3.0.0] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
**Major, not minor:** `Base::Page::DataField::ImageGallery#file_container_ids`/
|
|
8
|
+
`#file_container_ids=` are REMOVED (see the first entry under `Changed`, below) -- a real
|
|
9
|
+
public-interface removal, not merely an addition, so this is a major version bump under
|
|
10
|
+
the versioning convention (minor = additive/breaking-but-additive, major = removal).
|
|
11
|
+
`lib/ecoportal/api/graphql/forces/` also left this gem for `oscar/eco-forces` (see the
|
|
12
|
+
`Changed` entry below) -- NOT breaking for any rubygems.org consumer (never packaged in a
|
|
13
|
+
released version). `lib/ecoportal/api/graphql/dsl/` left earlier, for `oscar/eco-dsl-dialects`
|
|
14
|
+
(FAAP-106 L2, `0bb6cd2`) -- also never released, and (owner ruling) not itself re-announced
|
|
15
|
+
here since there is nothing to announce; only this gem's own release guard and the fragments
|
|
16
|
+
whose described code STAYED are recorded below. **`lib/ecoportal/api/graphql/versioning/`
|
|
17
|
+
is likewise never packaged** (owner ARCHITECTURE ruling, PERKS, same day) -- unlike `dsl/`/
|
|
18
|
+
`forces/`, the CODE stays in this repo for now (extraction to its own private gem is a
|
|
19
|
+
follow-up card); every `Versioning::*`/`TemplateRegistry`/`DiffProfile`/`ProfileRunner`
|
|
20
|
+
entry this release would otherwise have listed is dropped from this changelog entirely
|
|
21
|
+
(none of it has ever shipped in a released version either) -- see the one `Removed` line
|
|
22
|
+
below.
|
|
6
23
|
|
|
7
|
-
###
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Added:** `WorkflowCommand` builder for `moveSectionToStage` (new in the 2026-09-29 live schema):
|
|
27
|
+
`build(:moveSectionToStage, source_stage_id:, target_stage_id:, section_id:, anchorId:, anchorPosition:)`,
|
|
28
|
+
moving an existing section to another stage. `anchorPosition` is validated like the other anchor commands.
|
|
29
|
+
|
|
30
|
+
- Test/tooling: DSL-08 emitter equivalence acceptance test
|
|
31
|
+
(`spec/ecoportal/api/graphql/emitters_equivalence_spec.rb`) and a new pure
|
|
32
|
+
`Model::Template::CommandNormalizer` helper (`lib/.../model/template/command_normalizer.rb`)
|
|
33
|
+
that repositions placeholder/id tokens to position and deep-sorts command keys so batches
|
|
34
|
+
from different emitters can be compared for structural equivalence. Measures (does not yet
|
|
35
|
+
fix) 8 real divergences between `Builder::TemplateBuilder`, `Diff::CommandSynthesizer`,
|
|
36
|
+
`Model::Template::CommandSynthesis`, the `template_field_config` DSL dialect, and the
|
|
37
|
+
a downstream script repo's writers against a shared one-stage/one-section/one-Select-field/
|
|
38
|
+
one-force/one-binding fixture -- see `TEMPLATE-MODEL-DESIGN.md` section 8 for the full
|
|
39
|
+
divergence table and fix plan. No production behaviour changed by this fragment.
|
|
40
|
+
|
|
41
|
+
- **`FileUpload::Client#upload_image`/`#upload_all_images`** — uploads a local file into the
|
|
42
|
+
Image Gallery upload pipeline (`uploadImage` → a `TempImage`, NOT `uploadFile` →
|
|
43
|
+
`FileContainer`) and returns the `TempImage` id (the `sourceId` an `ImageInput` write
|
|
44
|
+
needs). Shares its presign + S3-POST step with `#upload`/`#upload_all` (same collision-proof
|
|
45
|
+
key convention, same field order) — only the registration mutation differs. Same
|
|
46
|
+
`Error` hierarchy, same `:signature`/`:storage`/`:register` hooks, same bounded-concurrency
|
|
47
|
+
batch semantics as the existing File-container upload. New: `Mutation::Image::Upload`
|
|
48
|
+
(wraps `uploadImage(input: ImagesUploadInput!)`), `Payload::ImagesUpload`,
|
|
49
|
+
`Base::TempImage`/`Model::TempImage` (`id`, `s3Key`, `error`, `complete`).
|
|
50
|
+
- **`Base::Page::DataField::ImageGallery#add_source_images`** — merge-safe writer for Image
|
|
51
|
+
Gallery fields (mirrors `FileField#file_container_ids=`'s kept-vs-new split): keeps every
|
|
52
|
+
currently-loaded image (echoed back as `{id:}` — `ImageGalleryInput.images` is a FULL
|
|
53
|
+
REPLACE server-side, same trap as `FileField#items`) and adds new ones (`{sourceId:,
|
|
54
|
+
weight:, fileName:}`, `sourceId` being a `TempImage` id from `#upload_image`), at
|
|
55
|
+
sequential weights starting at `#next_weight` (one past the current maximum — there is no
|
|
56
|
+
server-side auto-increment for `weight`). Also adds `#images`/`#image_ids`/`#next_weight`
|
|
57
|
+
readers.
|
|
58
|
+
|
|
59
|
+
- `Model::Template.load(api, id:)` / `Model::Template::Instance` — the mutable, "three
|
|
60
|
+
lines" editable template model (the repo's internal docs): `template = Model::Template.load(api, id:); template.fields.each
|
|
61
|
+
{ |f| f.label = ... }; template.save!(api)`. Reconciles the gem's existing one-shot
|
|
62
|
+
`Builder::TemplateBuilder`/`Diff::CommandSynthesizer` command-emission layer with the
|
|
63
|
+
a downstream script repo's toolkit's proven staged/chunked write pattern
|
|
64
|
+
(`CommandBatchExecutor`, `TemplateFieldsRead`, `SectionWriter`/`FieldWriter`/
|
|
65
|
+
`SelectOptionWriter`/`ForceAndBindingWriter`), promoted here as a GENERIC gem primitive
|
|
66
|
+
rather than duplicated per org.
|
|
67
|
+
- `#as_commands` — diffs the in-memory tree against its load-time snapshot, ordered stage
|
|
68
|
+
-> section -> field -> option/config -> force -> helper -> binding, with a per-stage
|
|
69
|
+
`reorderSection` trailer.
|
|
70
|
+
- `#save!(api, simulate: true)` (default) returns the staged PLAN with no server call;
|
|
71
|
+
`simulate: false` executes it through `Builder::Template#update` (`updatePageTemplate` —
|
|
72
|
+
never `executeWorkflowCommands`, per `18_template_editor_save_path.md` section D.4's own
|
|
73
|
+
recommendation), chunked (`Model::Template::CommandGrouper`, a generic union-find grouping
|
|
74
|
+
that never splits a placeholder-minting command from a same-batch dependent), staged in
|
|
75
|
+
four phases (stage / section / field+option / force / helper+binding) with a live re-read
|
|
76
|
+
+ positional/count-guard/cross-check id resolution between phases — any number of
|
|
77
|
+
stages/sections/fields/forces, not one template's shape.
|
|
78
|
+
- `#verify(api)` re-reads and reports per changed property `verified`/`mismatch`/`missing`
|
|
79
|
+
against the model's OWN current (post-save) state.
|
|
80
|
+
- `Builder::Template#update` / `Mutation::Template::Update#query` gained an optional
|
|
81
|
+
`client_mutation_id:` (omitted when not given — the pre-existing call shape is byte-
|
|
82
|
+
identical for every other caller) for `StagedExecutor`'s own per-chunk tracing id.
|
|
83
|
+
|
|
84
|
+
- `rewrite_field_labels`/`rewrite_field_tooltips` results now carry a `selection_digest`
|
|
85
|
+
(a sha256 hex digest over the sorted touched field ids + their before-values -- never the
|
|
86
|
+
values themselves) -- `verify_field_labels`/`verify_field_tooltips` gained an OPTIONAL
|
|
87
|
+
`expected_selection_digest` argument: when given and it differs from a fresh digest of
|
|
88
|
+
the current selection, the verify call refuses with `selection_drift: true, verified:
|
|
89
|
+
false` and both digests (hex only) instead of the usual buckets. Closes "apply requires
|
|
90
|
+
a paired verify over the SAME selection" pinning a PREDICATE, not a fixed set -- the live
|
|
91
|
+
field set (or a field's own value) can change between a rewrite call and a later verify
|
|
92
|
+
call (a field added, a label hand-edited, a force re-installed) with nothing to detect
|
|
93
|
+
it (eco-forces consumer review finding F-04). Fully backwards compatible: omitting
|
|
94
|
+
`expected_selection_digest` verifies against the current selection exactly as before.
|
|
95
|
+
See the repo's internal docs, "Selection digest".
|
|
96
|
+
|
|
97
|
+
### Changed
|
|
8
98
|
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
99
|
+
- **BREAKING:** `Base::Page::DataField::ImageGallery#file_container_ids`/
|
|
100
|
+
`#file_container_ids=` are REMOVED — an Image Gallery image was never a `FileContainer`;
|
|
101
|
+
`ImageGalleryInput` has no `fileContainerIds` on the live schema (confirmed against the live
|
|
102
|
+
ecoPortal server source — `ImageGalleryInput` takes `images: [ImageInput]`, referencing a
|
|
103
|
+
NEW image by `sourceId`, a `TempImage` id from the separate `uploadImage` mutation, never a
|
|
104
|
+
`fileContainerId`). The old accessors read/wrote a doc key (`fileContainers`) the live API
|
|
105
|
+
never returns or accepts — any caller depending on them was already silently broken (see the
|
|
106
|
+
three characterization bugs closed below). Replaced by `#images`/`#image_ids`/
|
|
107
|
+
`#add_source_images` (see `Added`, above) — a caller previously doing
|
|
108
|
+
`field.file_container_ids = [...]` should now upload via
|
|
109
|
+
`FileUpload::Client#upload_image` and call `field.add_source_images([{source_id:,
|
|
110
|
+
file_name:}, ...])` instead.
|
|
111
|
+
|
|
112
|
+
- `imageGalleryField` (`Fragment::Pages::CommonPageUnion`) now selects `id`/`weight` on each
|
|
113
|
+
`images` item — both were previously omitted entirely, which is why no correct reader was
|
|
114
|
+
possible before this fix (see the `Fixed` entry below).
|
|
115
|
+
- `fileName`/`fileSize` on an Image Gallery image are confirmed NULLABLE server-side even on
|
|
116
|
+
a successful upload (a documented upstream platform bug, per the live schema's own field
|
|
117
|
+
comment) — any comparison built on either must treat a nil on either side as "not a
|
|
118
|
+
confident match", never a positive one. Documented on
|
|
119
|
+
`Base::Page::DataField::ImageGallery`'s own header.
|
|
120
|
+
|
|
121
|
+
- **Changed (behaviour):** `Base::Page::DataField::ImageGallery#as_input` now echoes a KEPT
|
|
122
|
+
(currently-existing, untouched) image with its FULL `ImageInput` field set — `id`,
|
|
123
|
+
`sourceId` (read back from `uploadId`), `weight`, `caption`, `fileName`,
|
|
124
|
+
`sensitiveContent`, `inaccurateDescription`, `inaccurateExtractedText` — instead of a bare
|
|
125
|
+
`{id:}`. `ImageGalleryInput.images` is a full replace server-side
|
|
126
|
+
(`assign_attributes_service.rb#set_image_gallery_attrs` → `format_removed_ids`): a bare
|
|
127
|
+
`{id:}` is read by the server as "clear every other attribute on this image", not "leave
|
|
128
|
+
it as it is" — the previous shape silently reset caption/moderation flags to their
|
|
129
|
+
defaults on every write that merely appended a new photo alongside existing ones.
|
|
130
|
+
`#add_source_images` now keeps each existing image's FULL doc entry (not collapsed to a
|
|
131
|
+
bare `{id:}`) so this echo has the data to build from. `#add_source_images`'s own
|
|
132
|
+
kept-vs-new merge semantics (weight = current max + 1, sequential) are unchanged.
|
|
133
|
+
|
|
134
|
+
- `lib/ecoportal/api/graphql/forces/` (dry-run docking, the L1 force-representation
|
|
135
|
+
reader/bridge/history slice, the conformance harness -- 14 files) is REMOVED from
|
|
136
|
+
this gem and moves to `oscar/eco-forces` (owner ruling R-2026-09-28-64 D-19:
|
|
137
|
+
eco-forces is the most sensitive IP in this stack, so the GraphQL client adapter
|
|
138
|
+
code that docks directly into it gets its own repo, same reasoning as the earlier
|
|
139
|
+
`dsl/` -> `eco-dsl-dialects` move). NOT BREAKING for rubygems.org consumers: this
|
|
140
|
+
tree was never published in any released version of this gem -- confirmed 0 files
|
|
141
|
+
under this path in every release through 2.2.0 (measured via `gem fetch` +
|
|
142
|
+
`Gem::Package#contents`, per the release-guard's own header). The only consumer this
|
|
143
|
+
could ever have reached is a repo-checkout-level `require
|
|
144
|
+
"ecoportal/api/graphql/forces"` (opt-in, never part of the main require chain, only
|
|
145
|
+
resolvable under the now-removed `Gemfile.forces`). `lib/ecoportal/api/graphql/
|
|
146
|
+
forces/CLAUDE.md` stays as a short pointer (same pattern as `dsl/CLAUDE.md`). The
|
|
147
|
+
release guard (`scripts/audit/dsl_release_guard.rb`) keeps its own
|
|
148
|
+
`lib/ecoportal/api/graphql/forces/` protected-prefix entry, now lifted by this MR's
|
|
149
|
+
own `docs/audits/rulings/forces-licence-ruling.md` -- independent of `dsl/`'s own
|
|
150
|
+
entry, which stays blocked until a separate ruling exists.
|
|
151
|
+
- `Gemfile.forces` removed entirely -- nothing else in this gem required eco-forces
|
|
152
|
+
(confirmed: a full-repo grep for `eco/forces`/`eco-forces`/`Eco::Forces` outside the
|
|
153
|
+
removed tree found only doc/tooling mentions unrelated to any require path). Its
|
|
154
|
+
`.rubocop.yml` exclude entry removed with it.
|
|
155
|
+
- `tools/forces-conformance/` and `tools/forces-l1/` removed -- both existed only to
|
|
156
|
+
drive the now-removed lib tree (their own `require_relative`s pointed directly into
|
|
157
|
+
it); both moved to `oscar/eco-forces` verbatim instead (same commit/MR as the tree
|
|
158
|
+
itself).
|
|
159
|
+
- The six `changelog.d/` fragments describing this tree's own prior history
|
|
160
|
+
(`added-forces-conformance-harness.md`, `added-forces-docking-dry-run.md`,
|
|
161
|
+
`added-forces-l1-dump-adapter.md`, `added-forces-l1-history.md`,
|
|
162
|
+
`added-forces-l1-reader-bridge.md`, `fixed-forces-parity-rerun-protocol-run.md`) are
|
|
163
|
+
DELETED from this gem's changelog.d and migrated, reworded only where a path/name
|
|
164
|
+
changed, into `oscar/eco-forces`'s own `CHANGELOG.md` `[Unreleased]` section -- this
|
|
165
|
+
gem never shipped a tagged release carrying any of them, so nothing is lost from
|
|
166
|
+
this gem's own history by moving them. `security-faap-906b-guard-forces.md` (the
|
|
167
|
+
guard itself, not the moved code) stays here, unchanged.
|
|
168
|
+
|
|
169
|
+
**Migration:** if you were requiring `ecoportal/api/graphql/forces` at the
|
|
170
|
+
repo-checkout level (never possible via the published gem, see above), add
|
|
171
|
+
`eco-forces` to your own Gemfile instead -- see that gem's README, "GraphQL client
|
|
172
|
+
adapters (moved from ecoportal-api-graphql 3.0.0)".
|
|
173
|
+
|
|
174
|
+
- **Changed (behaviour):** `Input::Page::Update.from_model` now defaults `client_mutation_id:`
|
|
175
|
+
to a fresh `SecureRandom.uuid` per call instead of an omitted empty string — every
|
|
176
|
+
`updatePage` mutation this input builds is now traceable by default (correlation only; the
|
|
177
|
+
server does NOT dedupe mutations on `clientMutationId`, so this buys no idempotency — a
|
|
178
|
+
retried call with the same id still re-applies the write). Pass an explicit
|
|
179
|
+
`client_mutation_id:` to correlate several calls under one value, or `''`/`nil` to omit the
|
|
180
|
+
key entirely (unchanged).
|
|
181
|
+
|
|
182
|
+
- **Changed** `scripts/release_smoke_check.rb` (release tooling, not packaged in the gem): the
|
|
183
|
+
packaging gate now honours per-gem exceptions declared in a `.release-smoke-allow` file at the
|
|
184
|
+
gem root — one EXACT packaged path per line, no globs — plus a `RELEASE_SMOKE_EXTRA_ALLOWED`
|
|
185
|
+
env override for one-off runs. Needed because the shared gate hardcoded `lib/**/*.rb` and
|
|
186
|
+
refused eco-helpers 3.3.2, whose three JSON data files are loaded at require time and are
|
|
187
|
+
allowlisted by its own gemspec and packaging spec. Exceptions are themselves gated: a declared
|
|
188
|
+
path missing from the package FAILS (stale declarations cannot rot silently), and a declared
|
|
189
|
+
`.json` file must parse. Proven red-then-green against the real eco-helpers-3.3.2.gem artifact.
|
|
12
190
|
|
|
13
191
|
### Fixed
|
|
14
192
|
|
|
@@ -18,11 +196,229 @@ All notable changes to this project will be documented in this file.
|
|
|
18
196
|
longer selects them. The model accessors are kept, deprecated, and now always return `nil`, so
|
|
19
197
|
callers do not break. Affects every earlier version, including 2.2.0.
|
|
20
198
|
|
|
21
|
-
|
|
199
|
+
- `AuthService::InstanceMethods#session_token_renewed` read the wrong hash key when no
|
|
200
|
+
explicit `refresh_token:` was given -- `body['resfresh_token']` (typo) instead of
|
|
201
|
+
`body['refresh_token']` -- so a session refresh attempted without an explicit token
|
|
202
|
+
always silently returned `nil` instead of renewing, even though the server's own
|
|
203
|
+
response body carried a real `refresh_token`. Fixed at
|
|
204
|
+
`lib/ecoportal/api/common/graphql/auth_service.rb`.
|
|
205
|
+
Zero specs existed for `AuthService` before this fix; `spec/ecoportal/api/common/
|
|
206
|
+
graphql/auth_service_spec.rb` now covers `#session_token` (happy path, the
|
|
207
|
+
auto-renew window, `auto_renew: false`, and a failing `session_token_data` call),
|
|
208
|
+
`#session_token_renewed` (explicit `refresh_token:`, reading it from the body, a
|
|
209
|
+
missing key, and a failing refresh call), and `#token_renew?`'s own boundary
|
|
210
|
+
(`TOKEN_AUTORENEW * 60` seconds, exactly at / one second inside / one second
|
|
211
|
+
outside) -- with a plain-Ruby fake HTTP client, no network, no new gem dependency.
|
|
212
|
+
|
|
213
|
+
- `Model::Template::CommandSynthesis` now emits `stageId` on `editFieldConfiguration` when
|
|
214
|
+
the owning template is phased (a new `Instance#phased?`, derived from the read's own
|
|
215
|
+
`page['__typename']`) -- `Field` always carries a `#stage` reference, but the command never
|
|
216
|
+
threaded it through, so editing a field via `Model::Template#save!` on a phased page would
|
|
217
|
+
omit a schema-required disambiguator and 500 server-side (found measuring DSL-08's emitter
|
|
218
|
+
equivalence acceptance test against the DSL dialect's own tested binding and the downstream
|
|
219
|
+
`FieldWriter`, both of which already got this right). `stageId` is still correctly OMITTED
|
|
220
|
+
(never sent as an explicit `null`) when the template is non-phased.
|
|
221
|
+
|
|
222
|
+
- `Ecoportal::API::Common::GraphQL::HttpClient#base_request` (the only place in this gem's
|
|
223
|
+
dependency chain that talks to the server via `http.rb`; every live GraphQL query goes
|
|
224
|
+
through it via `Logic::BaseQuery#graphql_query` -> `client.http_client.execute`, and the
|
|
225
|
+
OAuth token POST in `AuthService#auth_http_client` reuses the same method) called
|
|
226
|
+
`.timeout(read: READ_TIMEOUT, write: WRITE_TIMEOUT)` without a `connect:` value. http.rb's
|
|
227
|
+
`HTTP::Timeout::PerOperation` (`lib/http/timeout/per_operation.rb`) defaults any timeout key
|
|
228
|
+
NOT given to 0.25 seconds, so `@connect_timeout` stayed at that 0.25s library default even
|
|
229
|
+
though read/write were correctly set to 90s. `connect_ssl` (the TLS handshake) uses
|
|
230
|
+
`@connect_timeout` via `rescue_readable`/`rescue_writable`, whose error text is
|
|
231
|
+
unconditionally "Read timed out after #{@read_timeout} seconds" -- so a slow TLS handshake
|
|
232
|
+
against the EU instance surfaced as `HTTP::TimeoutError: Read timed out after 0.25 seconds`
|
|
233
|
+
even though the actual `@read_timeout` was 90 the whole time. Live-confirmed 2026-09-08 via
|
|
234
|
+
backtrace: `connect_ssl` -> `start_tls` (`http/connection.rb`) -> `Common::Client#post` ->
|
|
235
|
+
`AuthService#session_token_data` -> `Common::GraphQL::Client#initialize`. Added a
|
|
236
|
+
`CONNECT_TIMEOUT = 30` constant alongside `READ_TIMEOUT`/`WRITE_TIMEOUT` and pass
|
|
237
|
+
`connect: CONNECT_TIMEOUT` in the same `.timeout(...)` call. Read and write were never the
|
|
238
|
+
problem and are unchanged.
|
|
239
|
+
|
|
240
|
+
- `Ecoportal::API::Common::GraphQL::HttpClient#base_request` (the only place in this gem's
|
|
241
|
+
dependency chain that talks to the server via `http.rb`; every live GraphQL query goes
|
|
242
|
+
through it via `Logic::BaseQuery#graphql_query` -> `client.http_client.execute`, and the
|
|
243
|
+
OAuth token POST in `AuthService#auth_http_client` reuses the same method) built a plain,
|
|
244
|
+
non-persistent `HTTP` client. http.rb sends `Connection: close` on every request from a
|
|
245
|
+
non-persistent client (`http/client.rb`: `default_options.persistent? ? KEEP_ALIVE : CLOSE`).
|
|
246
|
+
Live-confirmed 2026-09-08: `eu.live.ecoportal.com` closes any HTTP/1.1 request carrying that
|
|
247
|
+
header without ever sending a response -- immediately, or after ~61s -- which surfaced in
|
|
248
|
+
Ruby as `OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`. The same Ruby stack
|
|
249
|
+
with a persistent (keep-alive) client works (`HTTP.persistent("https://eu.live.ecoportal.com")
|
|
250
|
+
.post(...)` -> 200 in 1.9s); Sydney (`live.ecoportal.com`) is fine either way. `base_request`
|
|
251
|
+
now returns a client made persistent to the configured host (`HTTP.persistent(base_url,
|
|
252
|
+
timeout: KEEP_ALIVE_TIMEOUT)`, new `KEEP_ALIVE_TIMEOUT = 5` constant) before chaining the
|
|
253
|
+
existing `.accept(:json).timeout(...)`. Confirmed against the installed http.rb 5.3.1 source
|
|
254
|
+
that this is safe: one `HttpClient` is bound to one host, so `HTTP::StateError` (raised only
|
|
255
|
+
on a cross-origin request against a persistent client, `client.rb#verify_connection!`) cannot
|
|
256
|
+
occur here; and an expired or dead persistent connection is transparently closed and
|
|
257
|
+
reconnected on the next request (`verify_connection!` checks `@connection.expired?` /
|
|
258
|
+
`keep_alive?` and calls `close`, then `perform` lazily re-opens via
|
|
259
|
+
`@connection ||= HTTP::Connection.new(...)`), with any resulting `HTTP::ConnectionError`
|
|
260
|
+
already covered by the inherited `Common::Client` retry pipeline. `execute` and the rest of
|
|
261
|
+
the retry/throttle pipeline are unchanged.
|
|
262
|
+
|
|
263
|
+
- **Fixed:** `imageGalleryField` (`Fragment::Pages::CommonPageUnion`) now also selects
|
|
264
|
+
`sensitiveContent`, `inaccurateDescription`, `inaccurateExtractedText` on each `images`
|
|
265
|
+
item, alongside the pre-existing `id`/`weight`/`downloadUrl`/`caption`/`fileName`/
|
|
266
|
+
`fileSize`/`uploadId` — every `ImageInput`-echoable attribute is now read in one query, so
|
|
267
|
+
`#kept_image_input`'s full-field echo (see the `Changed` entry above) never needs a
|
|
268
|
+
follow-up read. `fileName`/`fileSize` remain documented NULLABLE even on a successful
|
|
269
|
+
upload (upstream platform bug) — never treat a nil on either as a confident match.
|
|
270
|
+
|
|
271
|
+
- `Base::Page::DataField::ImageGallery`'s reader closes all three defects
|
|
272
|
+
`image_gallery_characterization_spec.rb` documented: BUG-3a (reader keyed on the wrong doc
|
|
273
|
+
key, `fileContainers` instead of `images` — see `Changed`, above, for the fragment fix this
|
|
274
|
+
needed first), BUG-3b (the fragment omitted `images[].id`, so no correct reader was
|
|
275
|
+
possible), and BUG-3c (a pure read injected a phantom `fileContainers` key into `doc`) — the
|
|
276
|
+
new reader is a plain `doc['images']` accessor (no `passarray`), so a pure read no longer
|
|
277
|
+
mutates `doc` at all, a stronger fix than the prior array-diff no-op (MR !106) gave the old
|
|
278
|
+
reader.
|
|
279
|
+
|
|
280
|
+
- `Common::GraphQL::Client#schema` no longer lets graphql-ruby's "Input Object types must
|
|
281
|
+
have arguments" warning leak out for the argument-less INPUT_OBJECT types the live
|
|
282
|
+
ecoPortal schema actually ships (confirmed today: `SupervisedPeopleFieldValuesGeneratorInput`,
|
|
283
|
+
`DiscreteMetadataFilterWidgetInput`, `DateFilterWidgetInput`, `PeopleFilterWidgetInput`,
|
|
284
|
+
`ByUserAnalysisInput`, `ByTypeAnalysisInput`, `ByRelativeStatusAnalysisInput`,
|
|
285
|
+
`TemplatesAnalysisInput`, `RestorePageOperationInput`, `UnarchiveOperationInput`,
|
|
286
|
+
`ArchiveOperationInput`, `DeleteFilesOperationInput` -- 12 types, 0 after the fix).
|
|
287
|
+
`GraphQL::Schema::Loader` (used by `GraphQL::Schema.from_introspection`, which
|
|
288
|
+
`Graphlient::Schema` / `GraphQL::Client.load_schema` call to build our client-side schema
|
|
289
|
+
from the server's raw introspection JSON) defines one `GraphQL::Schema::InputObject`
|
|
290
|
+
subclass per INPUT_OBJECT type but never calls `has_no_arguments(true)` on the ones whose
|
|
291
|
+
`inputFields` came back empty -- so graphql-ruby warns (and, in a future version, will
|
|
292
|
+
raise) the first time anything asks such a type for its arguments, which happens whenever
|
|
293
|
+
the loaded schema is re-serialized (`schema.to_json` / `to_definition`), as eco-helpers'
|
|
294
|
+
`-graphql-schema` use case does. The live server never warns because its own Ruby classes
|
|
295
|
+
are defined with real argument lists (or `has_no_arguments(true)` already set) -- only the
|
|
296
|
+
client-side schema rebuilt purely from introspection data loses that annotation, so the fix
|
|
297
|
+
belongs here rather than in graphlient/graphql-client/graphql-ruby or in eco-helpers.
|
|
298
|
+
`#schema` now walks the freshly loaded schema's types once and calls the library's own
|
|
299
|
+
`has_no_arguments(true)` on every INPUT_OBJECT with no arguments, leaving types that do have
|
|
300
|
+
arguments untouched. No pre-processing of the introspection JSON, no schema shape change.
|
|
301
|
+
|
|
302
|
+
- **Fixed:** `updatePage` on a PHASED page has been observed to 500 when `input.stageId` is
|
|
303
|
+
missing on an Image Gallery / File field write (live-diagnosed, page
|
|
304
|
+
a customer's phased page) — `Input::Page::Update.from_model` now DERIVES a default
|
|
305
|
+
`stageId` from the owning stage of the data fields being changed in the same call (walks
|
|
306
|
+
`model.stages`/`Stage#stage_field_ids`), for both `dataFields.updates`/`.additions` and
|
|
307
|
+
`.deletions`. A `BasicPage` (no `#stages` at all) is unaffected — `stageId` is omitted
|
|
308
|
+
entirely, never sent as an explicit `null`. Explicit `stage_id:` still overrides the
|
|
309
|
+
derivation. If the fields being updated in ONE call span more than one stage,
|
|
310
|
+
`from_model` now raises a clear `ArgumentError` instead of guessing which stage's id to
|
|
311
|
+
send — split such a change into one call per stage, in stage order, re-fetching the page
|
|
312
|
+
(fresh `patchVer`) between calls. Corrects a stale doc comment on `#require_stage!` that
|
|
313
|
+
claimed "a plain field update ... needs no stage" — true only for a `BasicPage`.
|
|
314
|
+
|
|
315
|
+
- `Ecoportal::API::GraphQL::DSL::TemplateFieldConfig::Generator.declare_rewrite!`'s
|
|
316
|
+
`rewrite_field_<x>s`/`verify_field_<x>s` set-shaped verb pair now declares `acts_on` as
|
|
317
|
+
the UNION of every element noun type it can reach (`template` AND `data_field` -- was
|
|
318
|
+
`template` only). A set-shaped verb reaches EVERY `DataField` matching its pattern, not
|
|
319
|
+
just the template it is invoked against; under-declaring `acts_on` would under-approximate
|
|
320
|
+
a future eco-dsl-core strict-mode required-class set (found by the eco-forces consumer
|
|
321
|
+
review of this convention, finding F-07 -- `eco-dsl-core` `docs/reviews/2026-09-22-set-
|
|
322
|
+
shaped-verb-forces-consumer-review.md`).
|
|
323
|
+
|
|
324
|
+
- `spec/ecoportal/api/graphql/diff/{version_diff_spec,command_synthesizer_spec,deploy_spec,
|
|
325
|
+
version_diff_typed_config_spec,version_diff_modalities_spec}.rb` each duplicated the same
|
|
326
|
+
`fixture(name)` helper calling `File.read(path)` with no `encoding:` -- under a non-UTF-8
|
|
327
|
+
ambient locale (`LANG=C LC_ALL=C`, no `RUBYOPT` encoding flags) this raised
|
|
328
|
+
`Encoding::InvalidByteSequenceError` inside `JSON.parse` on the first non-ASCII byte in a
|
|
329
|
+
fixture, before a single document was even parsed (35 spec failures reproduced this way).
|
|
330
|
+
JSON is always UTF-8 per its own spec (RFC 8259 SS8.1), never a guess. Extracted the
|
|
331
|
+
duplicated helper into a single shared `Support.template_fixture` (`spec/support/
|
|
332
|
+
template_fixtures.rb`, `encoding: 'UTF-8'`); the five spec files now delegate to it. Spec
|
|
333
|
+
code only -- no `lib/` change. Verified: `LANG=C LC_ALL=C bundle exec rspec` and the normal
|
|
334
|
+
`bundle exec rspec` both green (2069 examples, 0 failures, 45 pending either way).
|
|
335
|
+
|
|
336
|
+
- `Mutation::Template::Create` / `Builder::Template#create` now accept an optional
|
|
337
|
+
`register_id:`, sent as `CreatePageTemplateInput.registerId` (verified against the live SDL,
|
|
338
|
+
2026-09-04 — `registerId: ID` is an INPUT FIELD, because
|
|
339
|
+
`Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation` folds the backend's
|
|
340
|
+
`argument :register_id` into the generated input object). Without it a template create is
|
|
341
|
+
**unauthorizable**, not merely unbound: the backend resolves a page's registers by tag
|
|
342
|
+
(`NewEp::Pages::AssociatedRegisters` — org registers whose `filter_tags` are a subset of the
|
|
343
|
+
page's `combined_tags`) and `RegisterTemplatePermissionChecker` returns `false` outright when
|
|
344
|
+
that set is empty. A template created with no `registerId` carries no `base_tags`, so it
|
|
345
|
+
matches zero registers, so the check fails no matter what the account is granted — surfacing
|
|
346
|
+
as the misleading `You are not authorized to perform: edit_template_basic_settings`.
|
|
347
|
+
`Pages::Templates::CreateForm#bind_register!` is what closes the hole, setting
|
|
348
|
+
`page.base_tags = register.filter_tags` before the command batch runs.
|
|
349
|
+
A nil `register_id:` is OMITTED, never sent as an explicit null, so every existing call site
|
|
350
|
+
produces the byte-identical input it produced before.
|
|
351
|
+
|
|
352
|
+
- `tools/template_yaml/cli.rb --delta` now wires `reviewer: Review.method(:call)` into
|
|
353
|
+
`Delta.call`, so the printed JSON always carries a `review_findings` key (`{'before' => [...],
|
|
354
|
+
'after' => [...]}` of Review finding triples), matching the fixture ground truth the
|
|
355
|
+
ep-rovo-qa delta-verification agent reads. It also merges a top-level **`warnings`** key
|
|
356
|
+
(`Delta::Result#warnings`) into the printed JSON -- pairing-key collisions, duplicate force
|
|
357
|
+
names, rebuilt-field counts -- so the `colliding_keys` false-pairing trap reaches the consumer
|
|
358
|
+
instead of being silently dropped. `Delta::Result#payload` itself is unchanged (it stays
|
|
359
|
+
byte-for-byte comparable to the Python projector's JSON); `warnings` is added by the CLI only,
|
|
360
|
+
on top of the payload, for `--delta` output specifically.
|
|
361
|
+
- Documented, in `cli.rb`'s header and `id_guard.rb`'s header, that `IdGuard`'s `HEX24` check is
|
|
362
|
+
a plain regex (`/\A[0-9a-f]{24}\z/`) with no ObjectId timestamp/counter decoding and no
|
|
363
|
+
uniqueness check beyond it -- confirming `md5(readable-name)[:24]` as an accepted convention for
|
|
364
|
+
synthetic fixture ids (a hex-24 string of either provenance passes identically).
|
|
22
365
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
366
|
+
### Removed
|
|
367
|
+
|
|
368
|
+
- `versioning/` (git-core diff/versioning) is a perk -- not packaged in the public gem;
|
|
369
|
+
available from a private gem in a follow-up.
|
|
370
|
+
|
|
371
|
+
### Security
|
|
372
|
+
|
|
373
|
+
- **Security (release gate):** `scripts/release_smoke_check.rb` now also gates packaged CONTENT,
|
|
374
|
+
not only which files ship. Every packaged text file is scanned for object ids, the private
|
|
375
|
+
source-host name, internal documentation paths, internal repository names, and a local
|
|
376
|
+
(never-shipped) denylist of customer names; hits fail the release unless excused by an exact
|
|
377
|
+
line in `.release-content-allow`. The gem now ships a short public `README.gem.md` instead of
|
|
378
|
+
the repository's developer README, and code comments and this changelog were reworded to
|
|
379
|
+
remove internal identifiers.
|
|
380
|
+
|
|
381
|
+
- New `bundler-audit` CI job (MIDW-909): runs `bundle exec bundle-audit check
|
|
382
|
+
--update` on every merge-request and branch pipeline, `allow_failure: false`.
|
|
383
|
+
`bundler-audit` (`~> 0.9`) added to the `Gemfile` (repo-local CI tool, not a
|
|
384
|
+
gemspec dependency).
|
|
385
|
+
- Fixed the "Insecure Source URI" finding the job first reported: the optional
|
|
386
|
+
`anthropic` dependency (only needed for `SearchConf::AIGenerator` live tests)
|
|
387
|
+
no longer has its own `source 'http://rubygems.org' do ... end` block -- it is
|
|
388
|
+
folded into the file's top-level `https://rubygems.org` source (owner ruling
|
|
389
|
+
2026-09-29, option a). The block existed to bypass local SSL inspection on
|
|
390
|
+
some developer machines (e.g. Norton Antivirus); that workaround now belongs
|
|
391
|
+
on the machine (point `SSL_CERT_FILE` at the OS certificate store's CA
|
|
392
|
+
bundle), not in the dependency source URI. `bundle exec bundle-audit check
|
|
393
|
+
--update` now reports no vulnerabilities.
|
|
394
|
+
|
|
395
|
+
- Added a release guard, `scripts/audit/dsl_release_guard.rb`: no gem build that
|
|
396
|
+
packages a path under `lib/ecoportal/api/graphql/dsl/` may be published (CI job
|
|
397
|
+
`dsl-release-guard` on every pipeline; `rake release:build`, before the smoke check)
|
|
398
|
+
until `docs/audits/rulings/dsl-licence-ruling.md` carries a `RULED:` line. Stopgap
|
|
399
|
+
while the DSL licence question (FAAP-906) is open; does not change `LICENSE`, the
|
|
400
|
+
gemspec licence field, or `spec.files`.
|
|
401
|
+
|
|
402
|
+
- Extended the release guard, `scripts/audit/dsl_release_guard.rb` (FAAP-906): the
|
|
403
|
+
protected prefixes are now a list, each lifted independently by its own ruling
|
|
404
|
+
file. No gem build that packages a path under `lib/ecoportal/api/graphql/dsl/` OR
|
|
405
|
+
`lib/ecoportal/api/graphql/forces/` may be published (CI job `dsl-release-guard`;
|
|
406
|
+
`rake release:build`) until THAT prefix's own ruling file
|
|
407
|
+
(`docs/audits/rulings/dsl-licence-ruling.md` or
|
|
408
|
+
`docs/audits/rulings/forces-licence-ruling.md`) carries a `RULED:` line --
|
|
409
|
+
R-2026-09-28-64 (D-19): eco-forces is the most sensitive IP. Measured: the
|
|
410
|
+
published 2.2.0 gem ships zero files under either prefix. Does not change
|
|
411
|
+
`LICENSE`, the gemspec licence field, or `spec.files`.
|
|
412
|
+
|
|
413
|
+
- Extended the release guard, `scripts/audit/dsl_release_guard.rb`, with a THIRD
|
|
414
|
+
protected prefix, `lib/ecoportal/api/graphql/versioning/` (owner ARCHITECTURE
|
|
415
|
+
ruling 2026-09-29: dsl dialects, forces, and git-core diff/versioning are perks,
|
|
416
|
+
not core). No gem build that packages a path under this prefix may be published
|
|
417
|
+
until `docs/audits/rulings/versioning-licence-ruling.md` carries a `RULED:` line.
|
|
418
|
+
Unlike the dsl/forces rulings, this ruling file records that the prefix stays
|
|
419
|
+
blocked by design -- extraction to a private gem is a follow-up card, not
|
|
420
|
+
something this ruling completes. Measured: 0 packaged paths under all three
|
|
421
|
+
prefixes. Does not change `LICENSE`, the gemspec licence field, or `spec.files`.
|
|
26
422
|
|
|
27
423
|
## [2.2.0] - 2026-09-02
|
|
28
424
|
|
data/README.gem.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ecoportal-api-graphql
|
|
2
|
+
|
|
3
|
+
A Ruby client for the ecoPortal GraphQL API: typed models for pages, templates, data fields and
|
|
4
|
+
registers, query and mutation helpers, and the workflow command bus for granular template edits.
|
|
5
|
+
|
|
6
|
+
This is the README shipped inside the published gem. The source repository keeps its own
|
|
7
|
+
developer README.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'ecoportal-api-graphql', require: %w[ecoportal/api-graphql]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
$ bundle
|
|
17
|
+
# or: $ gem install ecoportal-api-graphql
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
| | version |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Ruby | `>= 3.2.2` |
|
|
25
|
+
| `ecoportal-api` | `~> 0.10, >= 0.10.17` |
|
|
26
|
+
| `ecoportal-api-v2` | `~> 3.3, >= 3.3.5` |
|
|
27
|
+
| `graphlient` | `>= 0.9.0, < 0.10` |
|
|
28
|
+
|
|
29
|
+
## Example: load and edit a template
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
api = Ecoportal::API::GraphQL.new(email: ..., pass: ..., org_id: ...)
|
|
33
|
+
template = Ecoportal::API::GraphQL::Model::Template.load(api, id: template_page_id)
|
|
34
|
+
template.fields.each { |f| f.label = "#{f.label} (reviewed)" }
|
|
35
|
+
template.save!(api, simulate: false) # simulate: true is the default -- inspect first
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`save!` simulates by default: it reports the commands it would send without writing anything.
|
|
39
|
+
Pass `simulate: false` to apply them.
|
|
40
|
+
|
|
41
|
+
## Optional extensions
|
|
42
|
+
|
|
43
|
+
Some capabilities are distributed separately as optional extensions and are not part of this
|
|
44
|
+
gem. The gem works fully without them; when an extension is installed, it is picked up
|
|
45
|
+
automatically.
|
|
46
|
+
|
|
47
|
+
## Changes
|
|
48
|
+
|
|
49
|
+
See `CHANGELOG.md`, shipped alongside this file.
|
|
50
|
+
|
|
51
|
+
## Licence
|
|
52
|
+
|
|
53
|
+
MIT -- see `LICENSE`.
|
|
@@ -25,7 +25,7 @@ module Ecoportal
|
|
|
25
25
|
def session_token_renewed(host: server, refresh_token: nil)
|
|
26
26
|
unless refresh_token
|
|
27
27
|
return unless (body = session_token_data(host: host))
|
|
28
|
-
return unless (refresh_token = body['
|
|
28
|
+
return unless (refresh_token = body['refresh_token'])
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
session_refresh_token_data(
|
|
@@ -56,12 +56,50 @@ module Ecoportal
|
|
|
56
56
|
@org_id || fetch_env_required('ORGANIZATION_ID')
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Graphlient/graphql-client build the client-side schema from the server's raw
|
|
60
|
+
# introspection JSON (GraphQL::Schema.from_introspection -> GraphQL::Schema::Loader).
|
|
61
|
+
# That loader defines one GraphQL::Schema::InputObject subclass per INPUT_OBJECT type,
|
|
62
|
+
# but it never calls `has_no_arguments(true)` on the ones whose `inputFields` came back
|
|
63
|
+
# empty. graphql-ruby then warns (and, in a future version, raises) the first time
|
|
64
|
+
# anything asks such a type for its arguments -- which happens whenever this schema is
|
|
65
|
+
# re-serialized (e.g. `schema.to_json`/`to_definition`, as the `-graphql-schema` use
|
|
66
|
+
# case in eco-helpers does). The live server never emits this warning because its own
|
|
67
|
+
# schema classes are defined directly in Ruby with real argument lists (or with
|
|
68
|
+
# `has_no_arguments(true)` already set) -- it is only the client-side schema, rebuilt
|
|
69
|
+
# purely from introspection data, that loses that annotation.
|
|
70
|
+
#
|
|
71
|
+
# Patching the built classes here (rather than pre-processing the introspection JSON)
|
|
72
|
+
# keeps the schema's shape untouched and uses graphql-ruby's own documented escape
|
|
73
|
+
# hatch for this exact situation.
|
|
74
|
+
def schema
|
|
75
|
+
mark_argumentless_input_objects!(super)
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
private
|
|
60
79
|
|
|
61
80
|
def url
|
|
62
81
|
base_url = Ecoportal::API::Common::GraphQL::HttpClient.base_url(host)
|
|
63
82
|
"#{base_url}/api/#{org_id}/#{ENDPOINT_PATH}"
|
|
64
83
|
end
|
|
84
|
+
|
|
85
|
+
def mark_argumentless_input_objects!(loaded_schema)
|
|
86
|
+
return loaded_schema if @argumentless_input_objects_marked
|
|
87
|
+
|
|
88
|
+
graphql_schema_types(loaded_schema).each_value do |type|
|
|
89
|
+
next unless type.respond_to?(:kind) && type.kind.input_object?
|
|
90
|
+
next unless type.respond_to?(:has_no_arguments) && type.respond_to?(:any_arguments?)
|
|
91
|
+
|
|
92
|
+
type.has_no_arguments(true) unless type.any_arguments?
|
|
93
|
+
end
|
|
94
|
+
@argumentless_input_objects_marked = true
|
|
95
|
+
|
|
96
|
+
loaded_schema
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def graphql_schema_types(loaded_schema)
|
|
100
|
+
target = loaded_schema.respond_to?(:graphql_schema) ? loaded_schema.graphql_schema : loaded_schema
|
|
101
|
+
target.types
|
|
102
|
+
end
|
|
65
103
|
end
|
|
66
104
|
end
|
|
67
105
|
end
|
|
@@ -96,9 +96,11 @@ module Ecoportal
|
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
ENDPOINT_PATH
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
ENDPOINT_PATH = 'external/graphql'.freeze
|
|
100
|
+
CONNECT_TIMEOUT = 30
|
|
101
|
+
READ_TIMEOUT = 90
|
|
102
|
+
WRITE_TIMEOUT = 90
|
|
103
|
+
KEEP_ALIVE_TIMEOUT = 5
|
|
102
104
|
|
|
103
105
|
attr_reader :host, :version
|
|
104
106
|
|
|
@@ -137,6 +139,36 @@ module Ecoportal
|
|
|
137
139
|
|
|
138
140
|
# Creates a HTTP object adding the `X-ApiKey` or `X-ECOPORTAL-API-KEY` param to the header, depending on the API version.
|
|
139
141
|
# @note It configures HTTP so it only allows body data in json format.
|
|
142
|
+
# @note This is the ONLY place in the gem's dependency chain that talks to the
|
|
143
|
+
# server via http.rb (`require 'http'`, pulled in by `Ecoportal::API::Common::Client`
|
|
144
|
+
# from the `ecoportal-api` gem) -- every live GraphQL query goes through `#execute`
|
|
145
|
+
# below, called from `Logic::BaseQuery#graphql_query` via `client.http_client.execute`,
|
|
146
|
+
# and `AuthService#auth_http_client` (`version: 'http'`) reuses this same
|
|
147
|
+
# `base_request` for the OAuth token POST. `Common::GraphQL::Client` (graphlient) is a
|
|
148
|
+
# separate, unrelated path: it never reaches http.rb at all.
|
|
149
|
+
#
|
|
150
|
+
# Without an explicit `connect:`, http.rb's `HTTP::Timeout::PerOperation` falls back
|
|
151
|
+
# to its own default of 0.25s for CONNECT even though read/write were already given
|
|
152
|
+
# explicitly (per_operation.rb: `options.fetch(:connect_timeout, CONNECT_TIMEOUT)`,
|
|
153
|
+
# `CONNECT_TIMEOUT = 0.25`). `connect_ssl` (`http/timeout/per_operation.rb`) uses
|
|
154
|
+
# that same `@connect_timeout` for the TLS handshake via `rescue_readable`/
|
|
155
|
+
# `rescue_writable` (`http/timeout/null.rb`) -- whose error text is unconditionally
|
|
156
|
+
# "Read timed out after #{@read_timeout} seconds", so a slow TLS handshake on the EU
|
|
157
|
+
# instance surfaced as `HTTP::TimeoutError: Read timed out after 0.25 seconds`
|
|
158
|
+
# even though `@read_timeout` itself was correctly 90 the whole time. Live-confirmed
|
|
159
|
+
# 2026-09-08 via backtrace: `connect_ssl` -> `start_tls` (`http/connection.rb`) ->
|
|
160
|
+
# `Common::Client#post` -> `AuthService#session_token_data` ->
|
|
161
|
+
# `Common::GraphQL::Client#initialize`. Read/write were never the problem; only
|
|
162
|
+
# connect was missing.
|
|
163
|
+
# @note The client is made PERSISTENT (`HTTP.persistent`) to `base_url`, not a
|
|
164
|
+
# plain one-shot client. Confirmed live 2026-09-08: eu.live.ecoportal.com
|
|
165
|
+
# closes an HTTP/1.1 request carrying `Connection: close` -- what a
|
|
166
|
+
# non-persistent http.rb client always sends -- without ever responding. A
|
|
167
|
+
# persistent client sends `Connection: keep-alive` and reuses the TCP/TLS
|
|
168
|
+
# connection instead, which the SAME Ruby stack proved fine against EU. This
|
|
169
|
+
# also matches Cloudflare keep-alive hygiene; Sydney is fine either way. One
|
|
170
|
+
# `HttpClient` is bound to one `host`, so `HTTP::StateError` (raised only on a
|
|
171
|
+
# cross-origin request against a persistent client) cannot occur here.
|
|
140
172
|
# @return [HTTP] HTTP object.
|
|
141
173
|
def base_request
|
|
142
174
|
@base_request ||=
|
|
@@ -151,9 +183,10 @@ module Ecoportal
|
|
|
151
183
|
HTTP.headers('Authorization' => "Bearer #{session_token(host: host)}")
|
|
152
184
|
end.then do |request|
|
|
153
185
|
request ||= HTTP
|
|
154
|
-
request.accept(:json).timeout(
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
request.persistent(base_url, timeout: KEEP_ALIVE_TIMEOUT).accept(:json).timeout(
|
|
187
|
+
connect: CONNECT_TIMEOUT,
|
|
188
|
+
read: READ_TIMEOUT,
|
|
189
|
+
write: WRITE_TIMEOUT
|
|
157
190
|
)
|
|
158
191
|
end
|
|
159
192
|
end
|