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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/projects/TODO.md +170 -118
  3. data/.ai-assistance/skills/ai-instructions/SKILL.md +48 -48
  4. data/.ai-assistance/skills/code-specs/SKILL.md +69 -69
  5. data/.ai-assistance/skills/gemini-assist/SKILL.md +63 -63
  6. data/.ai-assistance/skills/project-cycle/SKILL.md +177 -177
  7. data/.ai-assistance/skills/refactor/SKILL.md +62 -62
  8. data/.ai-assistance/skills/spec-generation/SKILL.md +72 -72
  9. data/.gitlab-ci.yml +64 -12
  10. data/CHANGELOG.md +60 -0
  11. data/changelog.d/fixed-mr51-payload-item-mismatches.md +17 -0
  12. data/lib/ecoportal/api/common/graphql/http_client.rb +6 -0
  13. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +17 -17
  14. data/lib/ecoportal/api/graphql/base/page/task.rb +24 -0
  15. data/lib/ecoportal/api/graphql/base/page.rb +17 -16
  16. data/lib/ecoportal/api/graphql/base/template.rb +35 -0
  17. data/lib/ecoportal/api/graphql/base.rb +2 -0
  18. data/lib/ecoportal/api/graphql/compat/response.rb +35 -35
  19. data/lib/ecoportal/api/graphql/file_upload/client.rb +21 -10
  20. data/lib/ecoportal/api/graphql/fragment/field_configuration.rb +38 -0
  21. data/lib/ecoportal/api/graphql/fragment/force.rb +14 -10
  22. data/lib/ecoportal/api/graphql/fragment/page_task.rb +39 -0
  23. data/lib/ecoportal/api/graphql/fragment/template.rb +51 -0
  24. data/lib/ecoportal/api/graphql/fragment.rb +3 -0
  25. data/lib/ecoportal/api/graphql/model/page/task.rb +16 -0
  26. data/lib/ecoportal/api/graphql/model/page.rb +16 -15
  27. data/lib/ecoportal/api/graphql/model/template.rb +15 -0
  28. data/lib/ecoportal/api/graphql/model.rb +2 -0
  29. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +45 -45
  30. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +40 -40
  31. data/lib/ecoportal/api/graphql/mutation/page/approve_review_task.rb +40 -40
  32. data/lib/ecoportal/api/graphql/mutation/page/batch_update_review_task.rb +38 -40
  33. data/lib/ecoportal/api/graphql/mutation/page/create_draft.rb +38 -40
  34. data/lib/ecoportal/api/graphql/mutation/page/delete_draft.rb +38 -40
  35. data/lib/ecoportal/api/graphql/mutation/page/execute_force_commands.rb +7 -7
  36. data/lib/ecoportal/api/graphql/mutation/page/execute_workflow_commands.rb +8 -7
  37. data/lib/ecoportal/api/graphql/mutation/page/publish_draft.rb +38 -40
  38. data/lib/ecoportal/api/graphql/mutation/page/reject_review_task.rb +40 -40
  39. data/lib/ecoportal/api/graphql/mutation/page/restart_review_task.rb +40 -40
  40. data/lib/ecoportal/api/graphql/mutation/page/undo_review_task.rb +40 -40
  41. data/lib/ecoportal/api/graphql/mutation/preset_view/destroy.rb +34 -35
  42. data/lib/ecoportal/api/graphql/mutation/register/destroy.rb +35 -35
  43. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +36 -36
  44. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +40 -40
  45. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +13 -13
  46. data/lib/ecoportal/api/graphql/mutation/template/create_related_page.rb +46 -46
  47. data/lib/ecoportal/api/graphql/mutation/template/destroy_related_page.rb +1 -1
  48. data/lib/ecoportal/api/graphql/mutation/template/update_information.rb +1 -1
  49. data/lib/ecoportal/api/graphql/mutation.rb +20 -20
  50. data/lib/ecoportal/api/graphql/payload/ok_payload.rb +36 -21
  51. data/lib/ecoportal/api/graphql/payload/page/draft.rb +26 -13
  52. data/lib/ecoportal/api/graphql/payload/page/review_task.rb +13 -13
  53. data/lib/ecoportal/api/graphql/payload/page/review_task_batch.rb +23 -0
  54. data/lib/ecoportal/api/graphql/payload/page.rb +20 -19
  55. data/lib/ecoportal/api/graphql/payload/preset_view.rb +15 -11
  56. data/lib/ecoportal/api/graphql/payload/register.rb +15 -11
  57. data/lib/ecoportal/api/graphql/payload/template/create_related_page.rb +1 -1
  58. data/lib/ecoportal/api/graphql/payload/template/destroy_related_page.rb +1 -1
  59. data/lib/ecoportal/api/graphql/payload/template/update_information.rb +1 -1
  60. data/lib/ecoportal/api/graphql/query/page_with_forces.rb +9 -3
  61. data/lib/ecoportal/api/graphql/query/pages_workflow_commands.rb +9 -3
  62. data/lib/ecoportal/api/graphql/query/register_preset_views.rb +78 -78
  63. data/lib/ecoportal/api/graphql.rb +9 -5
  64. data/lib/ecoportal/api/graphql_version.rb +1 -1
  65. data/tests/dump_schema.rb +88 -0
  66. data/tests/validate_queries.rb +34 -12
  67. 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
- # First automated check on merge requests. Runs the RSpec suite NON-BLOCKING
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
- # Live/AI specs (tagged :live) are excluded: they need ANTHROPIC_API_KEY + network.
9
- # They already self-skip when the key is absent, but ~live keeps the runner off
10
- # that path entirely.
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 because this job is
15
- # allow_failure, that surfaces as a warning rather than blocking the MR.
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
- rspec:
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
@@ -33,3 +33,5 @@ require_relative 'base/ai_summary_version'
33
33
  require_relative 'base/force'
34
34
  require_relative 'base/pages_workflow'
35
35
  require_relative 'base/kickstand'
36
+
37
+ require_relative 'base/template'
@@ -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
- # Requires the gem to have been initialised with email/pass (OAuth Bearer token).
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, Bearer token auth) ---
119
+ # --- REST helpers (v2 endpoints, X-ECOPORTAL-API-KEY auth) ---
115
120
 
116
- def bearer_token
117
- @bearer_token ||= @http_client.send(:key_token)
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['Authorization'] = "Bearer #{bearer_token}"
128
- request['Accept'] = 'application/json'
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['Authorization'] = "Bearer #{bearer_token}"
136
- request['Content-Type'] = 'application/json'
137
- request['Accept'] = 'application/json'
138
- request.body = JSON.generate(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