ecoportal-api-graphql 1.3.14 → 1.3.15
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/.ai-assistance/projects/TODO.md +170 -118
- data/.ai-assistance/skills/ai-instructions/SKILL.md +48 -48
- data/.ai-assistance/skills/code-specs/SKILL.md +69 -69
- data/.ai-assistance/skills/gemini-assist/SKILL.md +63 -63
- data/.ai-assistance/skills/project-cycle/SKILL.md +177 -177
- data/.ai-assistance/skills/refactor/SKILL.md +62 -62
- data/.ai-assistance/skills/spec-generation/SKILL.md +72 -72
- data/.gitlab-ci.yml +64 -12
- data/CHANGELOG.md +60 -0
- data/changelog.d/fixed-mr51-payload-item-mismatches.md +17 -0
- data/lib/ecoportal/api/common/graphql/http_client.rb +6 -0
- data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +17 -17
- data/lib/ecoportal/api/graphql/base/page/task.rb +24 -0
- data/lib/ecoportal/api/graphql/base/page.rb +17 -16
- data/lib/ecoportal/api/graphql/base/template.rb +35 -0
- data/lib/ecoportal/api/graphql/base.rb +2 -0
- data/lib/ecoportal/api/graphql/compat/response.rb +35 -35
- data/lib/ecoportal/api/graphql/file_upload/client.rb +21 -10
- data/lib/ecoportal/api/graphql/fragment/field_configuration.rb +38 -0
- data/lib/ecoportal/api/graphql/fragment/force.rb +14 -10
- data/lib/ecoportal/api/graphql/fragment/page_task.rb +39 -0
- data/lib/ecoportal/api/graphql/fragment/template.rb +51 -0
- data/lib/ecoportal/api/graphql/fragment.rb +3 -0
- data/lib/ecoportal/api/graphql/model/page/task.rb +16 -0
- data/lib/ecoportal/api/graphql/model/page.rb +16 -15
- data/lib/ecoportal/api/graphql/model/template.rb +15 -0
- data/lib/ecoportal/api/graphql/model.rb +2 -0
- data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +45 -45
- data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/page/approve_review_task.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/page/batch_update_review_task.rb +38 -40
- data/lib/ecoportal/api/graphql/mutation/page/create_draft.rb +38 -40
- data/lib/ecoportal/api/graphql/mutation/page/delete_draft.rb +38 -40
- data/lib/ecoportal/api/graphql/mutation/page/execute_force_commands.rb +7 -7
- data/lib/ecoportal/api/graphql/mutation/page/execute_workflow_commands.rb +8 -7
- data/lib/ecoportal/api/graphql/mutation/page/publish_draft.rb +38 -40
- data/lib/ecoportal/api/graphql/mutation/page/reject_review_task.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/page/restart_review_task.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/page/undo_review_task.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/preset_view/destroy.rb +34 -35
- data/lib/ecoportal/api/graphql/mutation/register/destroy.rb +35 -35
- data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +36 -36
- data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +40 -40
- data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +13 -13
- data/lib/ecoportal/api/graphql/mutation/template/create_related_page.rb +46 -46
- data/lib/ecoportal/api/graphql/mutation/template/destroy_related_page.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/template/update_information.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation.rb +20 -20
- data/lib/ecoportal/api/graphql/payload/ok_payload.rb +36 -21
- data/lib/ecoportal/api/graphql/payload/page/draft.rb +26 -13
- data/lib/ecoportal/api/graphql/payload/page/review_task.rb +13 -13
- data/lib/ecoportal/api/graphql/payload/page/review_task_batch.rb +23 -0
- data/lib/ecoportal/api/graphql/payload/page.rb +20 -19
- data/lib/ecoportal/api/graphql/payload/preset_view.rb +15 -11
- data/lib/ecoportal/api/graphql/payload/register.rb +15 -11
- data/lib/ecoportal/api/graphql/payload/template/create_related_page.rb +1 -1
- data/lib/ecoportal/api/graphql/payload/template/destroy_related_page.rb +1 -1
- data/lib/ecoportal/api/graphql/payload/template/update_information.rb +1 -1
- data/lib/ecoportal/api/graphql/query/page_with_forces.rb +9 -3
- data/lib/ecoportal/api/graphql/query/pages_workflow_commands.rb +9 -3
- data/lib/ecoportal/api/graphql/query/register_preset_views.rb +78 -78
- data/lib/ecoportal/api/graphql.rb +9 -5
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- data/tests/dump_schema.rb +88 -0
- data/tests/validate_queries.rb +34 -12
- metadata +11 -1
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
# SKILL: Spec Generation
|
|
2
|
-
|
|
3
|
-
**Purpose:** Add RSpec specs that guard backwards compatibility and document expected behaviour, placed in the correct spec folder mirroring the lib/ structure.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
- A change could break downstream consumers (primarily `eco-helpers`).
|
|
10
|
-
- A public method or interface is being added, changed, or removed.
|
|
11
|
-
- A bug is fixed — add a regression spec.
|
|
12
|
-
- A new class is added that other code will depend on.
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Placement Rules
|
|
17
|
-
|
|
18
|
-
Spec files **mirror `lib/` structure** under `spec/`:
|
|
19
|
-
|
|
20
|
-
| Code file | Spec file |
|
|
21
|
-
|-----------|----------|
|
|
22
|
-
| `lib/ecoportal/api/graphql/model/action.rb` | `spec/ecoportal/api/graphql/model/action_spec.rb` |
|
|
23
|
-
| `lib/ecoportal/api/graphql/input/location_structure/draft.rb` | `spec/ecoportal/api/graphql/input/location_structure/draft_spec.rb` |
|
|
24
|
-
|
|
25
|
-
Create the folder path if it doesn't exist.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Spec Requirements
|
|
30
|
-
|
|
31
|
-
Each spec file for a changed area must have:
|
|
32
|
-
|
|
33
|
-
1. A description of the class/module under test.
|
|
34
|
-
2. At least one spec covering the public interface being changed.
|
|
35
|
-
3. For backwards-compat specs: a comment `# backwards-compat` so they're easily searched.
|
|
36
|
-
|
|
37
|
-
**Minimum spec template:**
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# frozen_string_literal: true
|
|
41
|
-
|
|
42
|
-
RSpec.describe Ecoportal::API::Graphql::<ClassName> do
|
|
43
|
-
# backwards-compat
|
|
44
|
-
describe '#<method>' do
|
|
45
|
-
subject { described_class.new(<args>) }
|
|
46
|
-
|
|
47
|
-
it '<expected behaviour>' do
|
|
48
|
-
expect(subject.<method>).to <matcher>
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Linked to Code Specs for AI
|
|
57
|
-
|
|
58
|
-
Whenever a spec file is created or updated for an area, check the corresponding `.ai-assistance/code/<area>.md` doc:
|
|
59
|
-
- Does it mention this class/method?
|
|
60
|
-
- Does it reflect the current expected behaviour?
|
|
61
|
-
|
|
62
|
-
If not — update it. This redundancy is intentional: it saves AI agents from having to parse spec files to understand expected behaviour.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Gemini Assist Integration
|
|
67
|
-
|
|
68
|
-
When generating specs for a large area, you may use `gemini-assist` to:
|
|
69
|
-
- Get a first-pass draft of spec cases given the source file.
|
|
70
|
-
- Check for edge cases you may have missed.
|
|
71
|
-
|
|
72
|
-
Always verify the generated specs run and match actual behaviour before committing.
|
|
1
|
+
# SKILL: Spec Generation
|
|
2
|
+
|
|
3
|
+
**Purpose:** Add RSpec specs that guard backwards compatibility and document expected behaviour, placed in the correct spec folder mirroring the lib/ structure.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
- A change could break downstream consumers (primarily `eco-helpers`).
|
|
10
|
+
- A public method or interface is being added, changed, or removed.
|
|
11
|
+
- A bug is fixed — add a regression spec.
|
|
12
|
+
- A new class is added that other code will depend on.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Placement Rules
|
|
17
|
+
|
|
18
|
+
Spec files **mirror `lib/` structure** under `spec/`:
|
|
19
|
+
|
|
20
|
+
| Code file | Spec file |
|
|
21
|
+
|-----------|----------|
|
|
22
|
+
| `lib/ecoportal/api/graphql/model/action.rb` | `spec/ecoportal/api/graphql/model/action_spec.rb` |
|
|
23
|
+
| `lib/ecoportal/api/graphql/input/location_structure/draft.rb` | `spec/ecoportal/api/graphql/input/location_structure/draft_spec.rb` |
|
|
24
|
+
|
|
25
|
+
Create the folder path if it doesn't exist.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Spec Requirements
|
|
30
|
+
|
|
31
|
+
Each spec file for a changed area must have:
|
|
32
|
+
|
|
33
|
+
1. A description of the class/module under test.
|
|
34
|
+
2. At least one spec covering the public interface being changed.
|
|
35
|
+
3. For backwards-compat specs: a comment `# backwards-compat` so they're easily searched.
|
|
36
|
+
|
|
37
|
+
**Minimum spec template:**
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# frozen_string_literal: true
|
|
41
|
+
|
|
42
|
+
RSpec.describe Ecoportal::API::Graphql::<ClassName> do
|
|
43
|
+
# backwards-compat
|
|
44
|
+
describe '#<method>' do
|
|
45
|
+
subject { described_class.new(<args>) }
|
|
46
|
+
|
|
47
|
+
it '<expected behaviour>' do
|
|
48
|
+
expect(subject.<method>).to <matcher>
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Linked to Code Specs for AI
|
|
57
|
+
|
|
58
|
+
Whenever a spec file is created or updated for an area, check the corresponding `.ai-assistance/code/<area>.md` doc:
|
|
59
|
+
- Does it mention this class/method?
|
|
60
|
+
- Does it reflect the current expected behaviour?
|
|
61
|
+
|
|
62
|
+
If not — update it. This redundancy is intentional: it saves AI agents from having to parse spec files to understand expected behaviour.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Gemini Assist Integration
|
|
67
|
+
|
|
68
|
+
When generating specs for a large area, you may use `gemini-assist` to:
|
|
69
|
+
- Get a first-pass draft of spec cases given the source file.
|
|
70
|
+
- Check for edge cases you may have missed.
|
|
71
|
+
|
|
72
|
+
Always verify the generated specs run and match actual behaviour before committing.
|
data/.gitlab-ci.yml
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# CI pipeline for ecoportal-api-graphql
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# (allow_failure: true) — it reports pass/fail status without gating merges.
|
|
5
|
-
# Rationale (2026-07-10): add visibility now; tighten to a blocking gate later
|
|
6
|
-
# once the suite is reliably green on a clean runner.
|
|
3
|
+
# Two jobs, deliberately different in strictness:
|
|
7
4
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# rspec — NON-BLOCKING (allow_failure: true): reports pass/fail without gating merges.
|
|
6
|
+
# Rationale (2026-07-10): visibility now; tighten to a gate later once the
|
|
7
|
+
# suite is reliably green on a clean runner.
|
|
8
|
+
# validate-queries — BLOCKING: renders EVERY query + mutation in the gem against a FRESHLY
|
|
9
|
+
# introspected schema and fails only on deterministic, non-negotiable
|
|
10
|
+
# breakage (see below). This is the gate the 2026-07-03 v1.3.10 prod hotfix
|
|
11
|
+
# (LocationStructure `updatedAt`) called for — those queries had no spec and
|
|
12
|
+
# first broke in a client runner.
|
|
13
|
+
#
|
|
14
|
+
# Live/AI specs (tagged :live) are excluded from rspec: they need ANTHROPIC_API_KEY + network.
|
|
15
|
+
# They already self-skip when the key is absent, but ~live keeps the runner off that path entirely.
|
|
11
16
|
#
|
|
12
17
|
# NOTE: graphlient resolves from a git fork (see Gemfile); ecoportal-api / -v2 come
|
|
13
18
|
# from rubygems. If a required floor (e.g. ecoportal-api-v2 >= 3.3.3) is not yet
|
|
14
|
-
# published, `bundle install` may fail on the runner
|
|
15
|
-
#
|
|
19
|
+
# published, `bundle install` may fail on the runner. For `rspec` that surfaces as a
|
|
20
|
+
# warning; for `validate-queries` it fails the pipeline — the fix is to publish the
|
|
21
|
+
# floor gem, not to relax the gate.
|
|
16
22
|
|
|
17
23
|
workflow:
|
|
18
24
|
rules:
|
|
@@ -27,10 +33,8 @@ workflow:
|
|
|
27
33
|
stages:
|
|
28
34
|
- test
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
stage: test
|
|
36
|
+
.ruby:
|
|
32
37
|
image: ruby:3.2.2
|
|
33
|
-
allow_failure: true # non-blocking: surface failures, do not gate merges
|
|
34
38
|
variables:
|
|
35
39
|
BUNDLE_PATH: vendor/bundle # cache installed gems between pipelines
|
|
36
40
|
cache:
|
|
@@ -41,5 +45,53 @@ rspec:
|
|
|
41
45
|
- ruby -v
|
|
42
46
|
- gem install bundler
|
|
43
47
|
- bundle install --jobs "$(nproc)" --retry 3
|
|
48
|
+
|
|
49
|
+
rspec:
|
|
50
|
+
extends: .ruby
|
|
51
|
+
stage: test
|
|
52
|
+
allow_failure: true # non-blocking: surface failures, do not gate merges
|
|
44
53
|
script:
|
|
45
54
|
- bundle exec rspec --tag ~live --format progress
|
|
55
|
+
|
|
56
|
+
# Offline validation of every gem query + mutation against the LIVE schema.
|
|
57
|
+
#
|
|
58
|
+
# Fails the pipeline on exactly two deterministic classes (see tests/validate_queries.rb):
|
|
59
|
+
# 1. FRAGMENT / RENDER FAILURES — the operation cannot even be rendered (dead fragment
|
|
60
|
+
# reference / unresolved constant). This is what broke the live LocationDraft, Action and
|
|
61
|
+
# ContractorEntity paths.
|
|
62
|
+
# 2. STRUCTURAL BUGS — "field must have selections" (object field selected bare) and
|
|
63
|
+
# "selections can't be made on scalars" (scalar over-selected).
|
|
64
|
+
# Everything else (unknown fields etc.) is reported but does NOT fail: it is usually schema
|
|
65
|
+
# staleness or union handling, and needs a human read.
|
|
66
|
+
#
|
|
67
|
+
# The schema is introspected AT PIPELINE TIME on purpose — a committed 6.3MB dump goes stale,
|
|
68
|
+
# and a stale schema both invents noise and hides real findings.
|
|
69
|
+
#
|
|
70
|
+
# Required CI/CD variables (Settings → CI/CD → Variables; mask the password):
|
|
71
|
+
# EP_CI_SERVER host of a NON-PRODUCTION environment (e.g. an internal/UAT host)
|
|
72
|
+
# EP_CI_ORG_ID organisation id on that host
|
|
73
|
+
# EP_CI_USER_EMAIL service account with READ access only — introspection needs no write scope
|
|
74
|
+
# EP_CI_USER_PASS its password (masked)
|
|
75
|
+
# Until all four are set the job does not run, so the gate is inert rather than red. Use a
|
|
76
|
+
# service account on a non-production org: the job only ever issues an introspection query,
|
|
77
|
+
# never touches records.
|
|
78
|
+
validate-queries:
|
|
79
|
+
extends: .ruby
|
|
80
|
+
stage: test
|
|
81
|
+
rules:
|
|
82
|
+
- if: '$EP_CI_SERVER && $EP_CI_ORG_ID && $EP_CI_USER_EMAIL && $EP_CI_USER_PASS'
|
|
83
|
+
variables:
|
|
84
|
+
BUNDLE_PATH: vendor/bundle
|
|
85
|
+
# The gem reads its connection from these names (Common::GraphQL::AuthService).
|
|
86
|
+
SERVER: $EP_CI_SERVER
|
|
87
|
+
ORGANIZATION_ID: $EP_CI_ORG_ID
|
|
88
|
+
USER_EMAIL: $EP_CI_USER_EMAIL
|
|
89
|
+
USER_PASS: $EP_CI_USER_PASS
|
|
90
|
+
script:
|
|
91
|
+
- bundle exec ruby tests/dump_schema.rb ci_schema.json
|
|
92
|
+
- bundle exec ruby tests/validate_queries.rb ci_schema.json
|
|
93
|
+
artifacts:
|
|
94
|
+
when: on_failure # keep the exact schema the failure was diagnosed against
|
|
95
|
+
expire_in: 1 week
|
|
96
|
+
paths:
|
|
97
|
+
- ci_schema.json
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.15] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
**Backwards-compatible.** Every change here fixes a query or mutation document that the live
|
|
8
|
+
schema rejected — i.e. code paths that could not run at all — plus the CI gate that found them.
|
|
9
|
+
Cherry-picked onto the 1.3.x line from `main`, where they landed alongside **breaking** changes
|
|
10
|
+
that are deliberately NOT in this release (see "Deferred to 1.4.0" below).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`FileUpload::Client` authenticated the v2 REST endpoints wrongly.** The ported S3
|
|
15
|
+
register/poll steps sent `Authorization: Bearer <oauth_token>`; those endpoints authenticate via
|
|
16
|
+
`X-ECOPORTAL-API-KEY`, so step 3 404'd and `#upload` never completed. `Ecoportal::API::GraphQL.new`
|
|
17
|
+
accepts an additive `api_key:` (independent of the GraphQL endpoint's own email/pass OAuth),
|
|
18
|
+
`HttpClient#api_key` is public, and the client raises a clear error when it is missing.
|
|
19
|
+
⚠ **Not yet live-verified** — verify against a sandbox org before relying on it.
|
|
20
|
+
- **Three invalid union selections.** `Query::RegisterPresetViews` (`fieldConfigurations`),
|
|
21
|
+
`Query::PageWithForces` (`forces`) and `Query::PagesWorkflowCommands` (`workflow`) selected
|
|
22
|
+
fields directly on a union, which is invalid GraphQL ("Selections can't be made directly on
|
|
23
|
+
unions") — all three queries failed outright. They now select through the members' shared
|
|
24
|
+
interface (`FieldConfigurationInterface` / `BasePageInterface`). Response shapes are unchanged.
|
|
25
|
+
- **`fragment ForceFields` asked for a non-existent `id` on bindings.** `Force.bindings` is
|
|
26
|
+
`[BindingInterface]`, whose only fields are `name` / `referenceId` (+ per-member `reference`),
|
|
27
|
+
so every query spreading the fragment was invalid. Now selects on the interface directly.
|
|
28
|
+
NOTE the read/write asymmetry this exposes: `WorkflowEditBindingInput` / `removeBinding` require
|
|
29
|
+
a binding `id` that no read exposes, so `BindingCollection#delete!(binding)` has no source for it.
|
|
30
|
+
- **7 invalid `PageFields` spreads.** The four review-task mutations return `item: TaskInterface`
|
|
31
|
+
(not a page — the page is reachable via `subject`), `Template::{Create,Destroy}RelatedPage`
|
|
32
|
+
return `RelatedPages`, and `Template::UpdateInformation` returns the narrow `Template` type. New
|
|
33
|
+
`:PageTaskFields` / `:TemplateRelatedPages` / `:TemplateInformation` fragments and read-only
|
|
34
|
+
`Base/Model::Page::Task` + `Base/Model::Template::{RelatedPages,Information}` models; the payload
|
|
35
|
+
`item_class`es pointed at `Model::PageUnion`, which never matched the schema. The template
|
|
36
|
+
mutations that genuinely return a page (`Create`/`Publish`/`Unpublish`/`Update`) are unchanged.
|
|
37
|
+
- **6 payloads selected an `item` their type does not have.** `Page::CreateDraft` → `{ id }`,
|
|
38
|
+
`Page::{DeleteDraft,PublishDraft}` → `{ ok }`, `Page::BatchUpdateReviewTask` → `{ count }` (and
|
|
39
|
+
its own `Payload::Page::ReviewTaskBatch`), `Register::Destroy` → `{ ok }`, `PresetView::Destroy`
|
|
40
|
+
→ `{ clientMutationId errors }`. `Payload::Page::Draft` gained `id`/`ok`, `Payload::Register`
|
|
41
|
+
gained `ok`.
|
|
42
|
+
- **`executeWorkflowCommands` was rendered with arguments it never accepted** — the schema takes a
|
|
43
|
+
single `input: ExecuteWorkflowCommandsInput!`, so the **page-level command bus could not run**.
|
|
44
|
+
Both classes' `#query` already built the right `input` hash; only the documents were wrong.
|
|
45
|
+
- **`Payload::OkPayload` now models `errors`** — it selected them but computed `success?` purely
|
|
46
|
+
from `ok`, silently ignoring a populated errors object.
|
|
47
|
+
|
|
48
|
+
### Added (tooling — no runtime effect)
|
|
49
|
+
|
|
50
|
+
- **`validate-queries` CI job** (`.gitlab-ci.yml`): renders every query + mutation against a
|
|
51
|
+
freshly introspected schema and fails on render failures plus the always-invalid selection
|
|
52
|
+
classes ("must have selections", "can't be made on scalars", "can't be made directly on unions",
|
|
53
|
+
"can't be spread inside"). This gate is what found everything above.
|
|
54
|
+
- **`tests/dump_schema.rb`**: fresh introspection through the gem's own `HttpClient`. Requires
|
|
55
|
+
`SERVER` explicitly (the gem's default host is production). Schema dumps are never committed.
|
|
56
|
+
|
|
57
|
+
### Deferred to 1.4.0 (breaking — intentionally absent here)
|
|
58
|
+
|
|
59
|
+
- The AI-summary rewire (`generateAiSummary` → `generatePageSummary`,
|
|
60
|
+
`submitAiSummaryFeedback` → `submitPageAiSummaryFeedback`, changed `#query` keywords,
|
|
61
|
+
`Base::AiSummaryVersion` field removals) and the removal of the `SmartFill` mutations
|
|
62
|
+
(`smartFillGenerate` / `smartFillSubmitFeedback` do not exist in the schema).
|
|
63
|
+
- The 2026-07-13 `WorkflowCommandInput` schema sync and the polymorphic read-model layer.
|
|
64
|
+
|
|
5
65
|
## [1.3.14] - 2026-07-16
|
|
6
66
|
|
|
7
67
|
Re-issue of the **1.3.13 HttpClient resilience fix** under a fresh version. The `1.3.13`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
- **Six mutations selected an `item` their payload type does not have** — invalid documents, so
|
|
2
|
+
none could run. Found by the first live run of the `validate-queries` CI gate (2026-07-30);
|
|
3
|
+
every replacement selection below is the schema's own shape, verified against a fresh prod
|
|
4
|
+
introspection:
|
|
5
|
+
- `Page::CreateDraft` → `CreatePageDraftPayload { id }` (the new draft's page id)
|
|
6
|
+
- `Page::DeleteDraft` / `Page::PublishDraft` → `{ ok }`
|
|
7
|
+
- `Page::BatchUpdateReviewTask` → `{ count }` (tasks updated). It also shared
|
|
8
|
+
`Payload::Page::ReviewTask`, whose type has an `item`; it now has its own
|
|
9
|
+
`Payload::Page::ReviewTaskBatch`.
|
|
10
|
+
- `Register::Destroy` → `DestroyRegisterPayload { ok }`
|
|
11
|
+
- `PresetView::Destroy` → `DestroyPresetViewPayload { clientMutationId errors }` — no payload
|
|
12
|
+
data at all, not even `ok`.
|
|
13
|
+
`Payload::Page::Draft` gained `id`/`ok` and `Payload::Register` gained `ok`; the create/update
|
|
14
|
+
variants of Register/PresetView still return `item` and are untouched. ★ Note `Logic::Payload`'s
|
|
15
|
+
inherited `embeds_one :item` materialises a HOLLOW item (all fields nil, and it writes
|
|
16
|
+
`'item' => {}` into the doc) when the response has no `item` — it does not return nil, so read
|
|
17
|
+
the scalars. 21 specs.
|
|
@@ -21,6 +21,12 @@ module Ecoportal
|
|
|
21
21
|
|
|
22
22
|
attr_reader :host, :version
|
|
23
23
|
|
|
24
|
+
# Publicly exposes the configured api_key (inherited as `private` from
|
|
25
|
+
# `Common::Client`) — `FileUpload::Client` needs it directly to authenticate
|
|
26
|
+
# against the v2 REST endpoints, independent of this client's own `@version`
|
|
27
|
+
# (which stays `'graphql'` and therefore always builds a Bearer `key_token`).
|
|
28
|
+
public :api_key
|
|
29
|
+
|
|
24
30
|
include Ecoportal::API::Common::GraphQL::AuthService
|
|
25
31
|
|
|
26
32
|
def initialize(
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Base
|
|
5
|
-
# Represents one generation version of an AiSummary field.
|
|
6
|
-
# Returned by generateAiSummary — tracks the prompt, output, token usage, and status.
|
|
7
|
-
class AiSummaryVersion < Logic::BaseModel
|
|
8
|
-
passkey :id
|
|
9
|
-
passthrough :aiSummary, :status, :prompt
|
|
10
|
-
passthrough :inputTokens, :outputTokens, :totalTokens
|
|
11
|
-
passthrough :resourceType # "field" | "stage" | "page"
|
|
12
|
-
passarray :ratings # [[user_id, score], ...]
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
# Represents one generation version of an AiSummary field.
|
|
6
|
+
# Returned by generateAiSummary — tracks the prompt, output, token usage, and status.
|
|
7
|
+
class AiSummaryVersion < Logic::BaseModel
|
|
8
|
+
passkey :id
|
|
9
|
+
passthrough :aiSummary, :status, :prompt
|
|
10
|
+
passthrough :inputTokens, :outputTokens, :totalTokens
|
|
11
|
+
passthrough :resourceType # "field" | "stage" | "page"
|
|
12
|
+
passarray :ratings # [[user_id, score], ...]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
module Page
|
|
6
|
+
# A page TASK — the `item` of the review-task mutations (`TaskInterface`:
|
|
7
|
+
# CompletePage / InComment / ReviewPage). Read-only.
|
|
8
|
+
#
|
|
9
|
+
# This is NOT a page: the page it belongs to is `subject` (a PageUnion). The
|
|
10
|
+
# review-task payloads used to type `item` as `Model::PageUnion`, which never
|
|
11
|
+
# matched the schema (see `Fragment :PageTaskFields`).
|
|
12
|
+
class Task < Logic::BaseModel
|
|
13
|
+
read_only!
|
|
14
|
+
|
|
15
|
+
passkey :id
|
|
16
|
+
passthrough :__typename, :stageId, :relativeStatus, :taskNotes
|
|
17
|
+
passboolean :complete, :overdue, :escalated
|
|
18
|
+
passdate :createdAt, :completedAt, :due
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Base
|
|
5
|
-
module Page
|
|
6
|
-
end
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
require_relative 'page/basic'
|
|
13
|
-
require_relative 'page/data_field'
|
|
14
|
-
require_relative 'page/section'
|
|
15
|
-
require_relative 'page/section_collection'
|
|
16
|
-
require_relative 'page/phased'
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
module Page
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative 'page/basic'
|
|
13
|
+
require_relative 'page/data_field'
|
|
14
|
+
require_relative 'page/section'
|
|
15
|
+
require_relative 'page/section_collection'
|
|
16
|
+
require_relative 'page/phased'
|
|
17
|
+
require_relative 'page/task'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
# Namespace for the narrow template-side types.
|
|
6
|
+
#
|
|
7
|
+
# ★ Note the platform asymmetry: in most of this gem a "template" IS a page (a page with
|
|
8
|
+
# `template: true`), which is why `Query::Templates` and most `Payload::Template::*`
|
|
9
|
+
# resolve to `Model::PageUnion`. The two types here are the exceptions — distinct schema
|
|
10
|
+
# types returned by specific template mutations.
|
|
11
|
+
module Template
|
|
12
|
+
# `item` of `updateTemplateInformation` (schema type `Template`). Read-only.
|
|
13
|
+
class Information < Logic::BaseModel
|
|
14
|
+
read_only!
|
|
15
|
+
|
|
16
|
+
passkey :id
|
|
17
|
+
passthrough :description, :aiDescription, :templateType, :templateContainerUid
|
|
18
|
+
passboolean :active, :public, :creatorEnabled, :creatorEditable
|
|
19
|
+
passarray :folders
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# `item` of `createTemplateRelatedPage` / `destroyTemplateRelatedPage`
|
|
23
|
+
# (schema type `RelatedPages`) — a related-pages CONFIG entry, not a page. Read-only.
|
|
24
|
+
class RelatedPages < Logic::BaseModel
|
|
25
|
+
read_only!
|
|
26
|
+
|
|
27
|
+
passkey :id
|
|
28
|
+
passthrough :title, :registerId
|
|
29
|
+
passboolean :hasInaccessibleVariables
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Compat
|
|
5
|
-
# Wraps a GraphQL mutation payload to expose the v2-compatible response interface.
|
|
6
|
-
# Scripts check .success? and .status after update/create operations.
|
|
7
|
-
class Response
|
|
8
|
-
def initialize(payload)
|
|
9
|
-
@payload = payload
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def success?
|
|
13
|
-
@payload.success?
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def error?
|
|
17
|
-
@payload.error?
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def status
|
|
21
|
-
@payload.error? ? :error : :ok
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def body
|
|
25
|
-
@payload.error_doc
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def item
|
|
29
|
-
@payload.item
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Compat
|
|
5
|
+
# Wraps a GraphQL mutation payload to expose the v2-compatible response interface.
|
|
6
|
+
# Scripts check .success? and .status after update/create operations.
|
|
7
|
+
class Response
|
|
8
|
+
def initialize(payload)
|
|
9
|
+
@payload = payload
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def success?
|
|
13
|
+
@payload.success?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def error?
|
|
17
|
+
@payload.error?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def status
|
|
21
|
+
@payload.error? ? :error : :ok
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def body
|
|
25
|
+
@payload.error_doc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def item
|
|
29
|
+
@payload.item
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -22,7 +22,12 @@ module Ecoportal
|
|
|
22
22
|
# page.components.get_by_name('Attachment').file_container_ids = [container_id]
|
|
23
23
|
# api.pages.update(page)
|
|
24
24
|
#
|
|
25
|
-
#
|
|
25
|
+
# Step 1 (GraphQL) authenticates via the gem's email/pass OAuth bearer token.
|
|
26
|
+
# Steps 3-4 (REST v2 `/s3/files`) require the gem to ALSO have been initialised
|
|
27
|
+
# with `api_key:` — `Ecoportal::API::GraphQL.new(email:, pass:, api_key:)` — since
|
|
28
|
+
# those endpoints authenticate via `X-ECOPORTAL-API-KEY`, not the GraphQL bearer
|
|
29
|
+
# token (mirrors `Ecoportal::API::Common::Content::Client#base_request` in
|
|
30
|
+
# ecoportal-api-v2).
|
|
26
31
|
class Client
|
|
27
32
|
POLL_INTERVAL = 2 # seconds between poll attempts
|
|
28
33
|
POLL_MAX_WAIT = 120 # seconds before giving up
|
|
@@ -111,10 +116,16 @@ module Ecoportal
|
|
|
111
116
|
end
|
|
112
117
|
end
|
|
113
118
|
|
|
114
|
-
# --- REST helpers (v2 endpoints,
|
|
119
|
+
# --- REST helpers (v2 endpoints, X-ECOPORTAL-API-KEY auth) ---
|
|
115
120
|
|
|
116
|
-
def
|
|
117
|
-
@
|
|
121
|
+
def api_key
|
|
122
|
+
@api_key ||= @http_client.api_key.tap do |key|
|
|
123
|
+
if key.nil? || key.to_s.strip.empty?
|
|
124
|
+
raise 'File upload requires an api_key: Ecoportal::API::GraphQL.new(email:, pass:, api_key:). ' \
|
|
125
|
+
'The v2 REST endpoints used to register/poll an upload authenticate via ' \
|
|
126
|
+
'X-ECOPORTAL-API-KEY, not the GraphQL OAuth bearer token.'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
118
129
|
end
|
|
119
130
|
|
|
120
131
|
def upload_base_url
|
|
@@ -124,18 +135,18 @@ module Ecoportal
|
|
|
124
135
|
def upload_get(path)
|
|
125
136
|
uri = URI("#{upload_base_url}#{path}")
|
|
126
137
|
request = Net::HTTP::Get.new(uri)
|
|
127
|
-
request['
|
|
128
|
-
request['Accept']
|
|
138
|
+
request['X-ECOPORTAL-API-KEY'] = api_key
|
|
139
|
+
request['Accept'] = 'application/json'
|
|
129
140
|
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
|
|
130
141
|
end
|
|
131
142
|
|
|
132
143
|
def upload_post(path, body)
|
|
133
144
|
uri = URI("#{upload_base_url}#{path}")
|
|
134
145
|
request = Net::HTTP::Post.new(uri)
|
|
135
|
-
request['
|
|
136
|
-
request['Content-Type']
|
|
137
|
-
request['Accept']
|
|
138
|
-
request.body
|
|
146
|
+
request['X-ECOPORTAL-API-KEY'] = api_key
|
|
147
|
+
request['Content-Type'] = 'application/json'
|
|
148
|
+
request['Accept'] = 'application/json'
|
|
149
|
+
request.body = JSON.generate(body)
|
|
139
150
|
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
|
|
140
151
|
end
|
|
141
152
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
class Fragment
|
|
5
|
+
# `RegisterPresetView#fieldConfigurations` returns a FieldConfigurationUnion.
|
|
6
|
+
# Selections can't be made directly on a union (GraphQL error: "Selections can't
|
|
7
|
+
# be made directly on unions"); the common fields live on FieldConfigurationInterface,
|
|
8
|
+
# which every union member implements, so they're pulled via an inline fragment on
|
|
9
|
+
# the interface rather than repeated per concrete type. Verified against the real
|
|
10
|
+
# frontend fragment (RegisterDataGrid/api/fieldConfigurationFragments/
|
|
11
|
+
# fieldConfigurationFields.graphql) — kept to the interface surface + the same three
|
|
12
|
+
# concrete-type extras it selects; other concrete types fall back to interface-only
|
|
13
|
+
# fields (matches the frontend, which doesn't cover every member either).
|
|
14
|
+
fragment :FieldConfiguration, <<~GRAPHQL
|
|
15
|
+
fragment FieldConfiguration on FieldConfigurationUnion {
|
|
16
|
+
__typename
|
|
17
|
+
... on FieldConfigurationInterface {
|
|
18
|
+
id
|
|
19
|
+
key
|
|
20
|
+
name
|
|
21
|
+
dataFieldLabel
|
|
22
|
+
weight
|
|
23
|
+
}
|
|
24
|
+
... on DateFieldConfiguration {
|
|
25
|
+
showTime
|
|
26
|
+
}
|
|
27
|
+
... on DisplayLocationsFieldConfiguration {
|
|
28
|
+
mode
|
|
29
|
+
}
|
|
30
|
+
... on PageNameFieldConfiguration {
|
|
31
|
+
displayUid
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
GRAPHQL
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|