ecoportal-api-graphql 1.3.9 → 1.3.10
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 +21 -0
- data/CHANGELOG.md +29 -0
- data/docs/worklog.md +129 -0
- data/lib/ecoportal/api/graphql/fragment/location_draft.rb +53 -53
- data/lib/ecoportal/api/graphql/query/location_structure/draft.rb +62 -62
- data/lib/ecoportal/api/graphql/query/location_structure.rb +61 -61
- data/lib/ecoportal/api/graphql.rb +138 -138
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- data/tests/validate_queries.rb +105 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 180d0e0ea70c7fb3462ae6bdec6ef78d1480c97be0861bec6e3e4bbb4abba751
|
|
4
|
+
data.tar.gz: e2733075993edee570ef5762fe06cdaac035609e313f583b43753a4da21f0fd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e05ae227d2cc97f91c250b74c6e831cbec0f9c9f8822d0bfdebacccfbe519cf229a48f2b9222623faf1c1ff2622ac2e7c1538e0036fa4494cb7a3e9082ba6f51
|
|
7
|
+
data.tar.gz: ed6a82e38512fee2375a0bd880c1c721cee1d28597055cbeda9117a454d59f7d0a6b8bfdb1cb43fbe66fa18860a7508f426fb982ac0035d01b26140e789b1ecd
|
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Pending
|
|
4
4
|
|
|
5
|
+
- [ ] **⏰ CI: query-validation step (DUE by 2026-07-17 — within 2 weeks of 2026-07-03)** — wire
|
|
6
|
+
`tests/validate_queries.rb` into CI so the whole class of GraphQL selection bugs fails *before*
|
|
7
|
+
release, not in a client runner. **Use a FRESHLY-introspected schema** (do NOT commit the 6.3MB
|
|
8
|
+
dump — it goes stale); fail the build only on the two structural classes ("field must have
|
|
9
|
+
selections" / "selections can't be made on scalars"). Motivated by the 2026-07-03 v1.3.10 hotfix
|
|
10
|
+
(LocationStructure `updatedAt`/`createdAt`) — those queries had no spec and first broke in prod.
|
|
11
|
+
Also: fix the 3 flagged union findings — **TRIAGED 2026-07-03: all THREE are REAL bugs** (verified
|
|
12
|
+
by rendering the assembled docs + re-validating fixes; not harness/dump artifacts). Each selects
|
|
13
|
+
fields directly on a union (invalid GraphQL) — wrap in the members' shared interface:
|
|
14
|
+
- `Query::PageWithForces` (page_with_forces.rb:33-35): bare `forces` on `PageUnion` →
|
|
15
|
+
`... on BasePageInterface { forces { ...ForceFields } }`. ALSO `fragment ForceFields`
|
|
16
|
+
(fragment/force.rb:15,20) selects `id` on `DataFieldBinding`/`SectionBinding` which have NO `id`
|
|
17
|
+
(only name/reference/referenceId) → drop `id` (or use `referenceId`).
|
|
18
|
+
- `Query::PagesWorkflowCommands` (pages_workflow_commands.rb:33-37): bare `workflow` on `PageUnion`
|
|
19
|
+
→ `... on BasePageInterface { workflow { ... } }`. (The "unused variable" errors were an
|
|
20
|
+
artifact — a validator cascade from the union error; no action on the vars.)
|
|
21
|
+
- `Query::RegisterPresetViews` (register_preset_views.rb:71): bare `{id key name weight}` on
|
|
22
|
+
`FieldConfigurationUnion` → `... on FieldConfigurationInterface { id key name weight }`.
|
|
23
|
+
None of these 3 queries have specs — add render/validation specs alongside the fixes. These are
|
|
24
|
+
latent (not the users-sync incident); PageWithForces is WIP. Ship as a follow-up patch (1.3.11).
|
|
25
|
+
|
|
5
26
|
- [x] **Builder::Register** — `createRegister`, `updateRegister`, `destroyRegister` mutations.
|
|
6
27
|
`Base::Register`, `Model::Register`, `Input::Register::Create/Update`, `Payload::Register`.
|
|
7
28
|
`Builder::Register` with `create`, `update`, `destroy`, and `preset_view` sub-builder.
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.3.10] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
Hotfix (backwards-compatible). Resolves a live failure on the Turners & Growers users sync.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **LocationStructure query — `updatedAt` bare selection** → `Field must have selections (field
|
|
12
|
+
'updatedAt' returns DateTime but has no selections)`. `LocationStructureType.updatedAt` returns the
|
|
13
|
+
`DateTime` object (`{ dateTime, timeZone }`), so it must be selected with sub-fields. Fixed in
|
|
14
|
+
`Query::LocationStructure` and `Query::LocationStructure::Draft` (now `updatedAt { dateTime timeZone }`).
|
|
15
|
+
The plural `Query::LocationStructures` does not select it and was unaffected.
|
|
16
|
+
- **Root cause:** the bare selection has existed since 2023 (v0.3.x) but was **never exercised** —
|
|
17
|
+
no spec covered these queries, and the tagtree/users-sync path only began fetching the location
|
|
18
|
+
structure via GraphQL (rather than APIv2) as of the cutover, so it first ran in production.
|
|
19
|
+
- **Regression guard added:** `spec/.../query/location_structure_spec.rb` renders the query blocks
|
|
20
|
+
offline and asserts `updatedAt` carries `{ dateTime timeZone }` — this would have failed in CI.
|
|
21
|
+
- **LocationDraft fragment — `createdAt` over-selection** (found by the full audit below): selected
|
|
22
|
+
`createdAt { dateTime }`, but `Draft.createdAt` is an `ISO8601DateTime` **scalar** → would throw
|
|
23
|
+
`Selections can't be made on scalars` whenever the location-structure **draft** query runs. Now bare
|
|
24
|
+
`createdAt`. Guarded by the same spec.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`tests/validate_queries.rb`** — offline audit: renders every gem query (auth stubbed, no network),
|
|
29
|
+
assembles its fragments, and validates the full document against a schema introspection JSON. Flags
|
|
30
|
+
the whole bug class — object fields selected bare ("must have selections") and scalars over-selected
|
|
31
|
+
("selections can't be made on scalars"). A full run confirmed these two location fixes are the ONLY
|
|
32
|
+
such issues across all 17 query classes (contractors, actions, pages, register, templates: clean).
|
|
33
|
+
|
|
5
34
|
## [1.3.9] - 2026-07-02
|
|
6
35
|
|
|
7
36
|
APIv2→GraphQL cutover hardening (backwards-compatible). Live-validated on the maintained ooze
|
data/docs/worklog.md
CHANGED
|
@@ -8,6 +8,135 @@ This file tracks cross-project session state and the overall repo status.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## ▶ SESSION 2026-07-03 (cont.) — Template self-version diff + genome finding + domain capture
|
|
12
|
+
|
|
13
|
+
**Built `Ecoportal::API::GraphQL::Diff::VersionDiff`** (gem branch `feature/template-version-diff`,
|
|
14
|
+
commit `19c1dea`, 8 specs, rubocop clean): two SAME-object snapshots (ids retained) → exact structural
|
|
15
|
+
changelog (added/removed/changed/moved for stages/sections/fields[label,type,section-move]/options
|
|
16
|
+
[label,weight]). `Diff::Change` is command-ready. This is a **shared gem capability** (admin/
|
|
17
|
+
troubleshooting/integration/QA), not QA-only — per Oscar's clarification that these tools are the
|
|
18
|
+
epicenter of admin+troubleshooting automation.
|
|
19
|
+
|
|
20
|
+
**KEY FINDING — corrected a wrong doc claim:** `genomeSignature: String` **IS exposed** in the live
|
|
21
|
+
GraphQL schema on `DataFieldsInterface` + every data-field type (verified in the schema dump). The old
|
|
22
|
+
`graphql_domain_knowledge.md` claim "NOT exploitable" was wrong — corrected. Real gap: our gem doesn't
|
|
23
|
+
REQUEST it (add to the data-field fragment). Genome stays a strong-but-fallible pairing signal.
|
|
24
|
+
|
|
25
|
+
**Comprehensive harvestable knowledge doc:** `.ai-assistance/code/template_diff_pairing_domain.md` —
|
|
26
|
+
captures Oscar's full brain-dump (UAT→PROD regression modes, no-stable-identity Mongo obstacle,
|
|
27
|
+
self-version vs cross-object diff, genome verified facts + 6 failure modes, data-in-fields principle,
|
|
28
|
+
equivalence-matching reframe + learning ledger, diff MODALITIES, layering, pipeline, Product context)
|
|
29
|
+
so it never needs re-explaining and ep-ai-standards can harvest it. New project `template-diff-deploy`
|
|
30
|
+
(INTENT+TODO). Confirmed with Oscar: equivalence reframe, self-version-first, ledger as first-class.
|
|
31
|
+
|
|
32
|
+
**NEXT (template-diff-deploy):** add `genomeSignature` to fragment; `Change`→WorkflowCommands; pairing
|
|
33
|
+
engine + ledger (eco-helpers, reuse `TypedFieldsPairing`); diff modalities; deploy+verify+monitor. Also
|
|
34
|
+
tighten ecoportal-qa to use the gem's page-model (drop its own TemplateModel).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ▶ SESSION 2026-07-03 — NEW tool: ecoportal-qa (QA-as-CI/CD, Phases 1–2 built, Phase 3 scoped)
|
|
39
|
+
|
|
40
|
+
**Phase 3 SCOPED (docs only, branch `feature/qa-services-delivery-project`):** Jira adapter design in
|
|
41
|
+
`qa-services-delivery/PHASE3-SCOPE.md` — the Jira channel is *just another consumer of the canonical
|
|
42
|
+
Result* (no runner/DSL changes); `Jira::{Client,RestClient,McpClient,DryRunClient,Publisher}`;
|
|
43
|
+
human-gated (report-only default, explicit `--transition`, never auto); redact-before-post + idempotent
|
|
44
|
+
comments + audit. Decision (pending Oscar): interface + **REST-first**, MCP later (CI endgame likely
|
|
45
|
+
lacks MCP). Sub-phase **3a (DryRunClient + Publisher) is unblocked** and can start before creds; 3b/3c
|
|
46
|
+
need Jira project + token. Also added **`ROADMAP.md`** — north-star narrative (one Result → many
|
|
47
|
+
channels; P1–P5 capability ladder; how each rung changes the human's day) written to make the
|
|
48
|
+
vision→execution path legible for Oscar. Build blocked on 4 open Qs (Jira project/statuses, REST-vs-MCP,
|
|
49
|
+
verdict field, where check-sets live).
|
|
50
|
+
|
|
51
|
+
**Phase 2 done (branch `feature/phase-2`, commit `22cc865`, 31 specs green, rubocop clean):**
|
|
52
|
+
CSV + HTML reporters (JSON/Text refactored to render from the canonical hash); `Redactor` PII scrub
|
|
53
|
+
(preserves verdict/counts); `exe/ecoportal-qa` CLI (`run --source fixture:PATH|graphql:ID
|
|
54
|
+
--format text|json|csv|html [--redact]`, exit code = verdict for CI); richer checks — negative
|
|
55
|
+
assertions (stage/section/field absent), `label_pattern` regex, `field_count` (exact/`{min,max}`),
|
|
56
|
+
`fields_in_order`/`sections_in_order`, select `option_weights`, top-level force presence/binding
|
|
57
|
+
(skip when not captured). Synthetic `sample_template_page.json` fixture exercises them offline.
|
|
58
|
+
**Only remaining Phase-2 item: live capture of `6a3fa5b8…622b` — BLOCKED on sandbox creds.**
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
New standalone repo **`ecoportal-qa`** (`C:\ruby_scripts\git\ecoportal-qa`, git-init'd, commit
|
|
63
|
+
`1a62031`, v0.0.1) for the **Services Delivery QA team** who review **org config/template changes**
|
|
64
|
+
against Jira tickets. **QA-as-CI/CD:** QA authors declarative check-sets (YAML DSL) → Ruby compiler →
|
|
65
|
+
runnable checks → read-only runner → canonical Result JSON → reporting/Jira/monitoring.
|
|
66
|
+
|
|
67
|
+
- Phase 1 built + green: `DSL::CheckSet`, `Compiler`, `TemplateModel`, `Source::{Fixture,Graphql}`
|
|
68
|
+
(GraphqlSource lazy/read-only), `Runner`, `Result` (verdict + honest `:skip`), `Reporters::{Json,Text}`.
|
|
69
|
+
Proven vs the real `toocs_coding_page.json` snapshot (7 pass / 2 fail / 3 skip). 11 specs, rubocop clean.
|
|
70
|
+
Core has NO runtime gem deps (doc-based) → offline-runnable.
|
|
71
|
+
- **ep-ai-standards question resolved:** standards live in ep-ai-standards; implementations live with
|
|
72
|
+
their deps. `ecoportal-qa` is its own repo but ships a `.ai-assistance/` conformance stub
|
|
73
|
+
(`conformance.json`) so ep-ai-standards discovers/governs it without owning the code.
|
|
74
|
+
- Project docs on gem branch **`feature/qa-services-delivery-project`** (UNMERGED):
|
|
75
|
+
`.ai-assistance/projects/qa-services-delivery/{INTENT,DECISIONS,TODO}.md`.
|
|
76
|
+
- Drivers: safety/privacy/simplicity/delegation-to-QA/monitoring/reporting/governance, Ruby, human-in-loop.
|
|
77
|
+
- Roadmap: P2 CSV/HTML reporters + more checks + capture `6a3fa5b8…622b`; P3 Jira adapter (human-gated,
|
|
78
|
+
MCP vs REST TBD); P4 CI-as-code "ticket as pipeline"; P5 opt-in governed auto-approve.
|
|
79
|
+
|
|
80
|
+
**Merge note:** `ecoportal-qa` is a fresh standalone repo (no default-branch merge needed — it IS the
|
|
81
|
+
main line there). Only the gem's `feature/qa-services-delivery-project` docs branch awaits merge to main.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ▶ SESSION 2026-07-02 — Template pipeline + ooze-native foundations (4 branches, unmerged)
|
|
86
|
+
|
|
87
|
+
Post-cutover, picked up **two** parallel threads at Oscar's direction: (3) ooze → native GraphQL
|
|
88
|
+
migration — **native classes ONLY, no delegation flip yet** — and (4) the template-editing (CSV)
|
|
89
|
+
pipeline. Grounded both against live code via two parallel Explore surveys before writing. All work
|
|
90
|
+
is on feature branches, **green + rubocop-clean, unmerged**, pending Oscar's review + the merge plan
|
|
91
|
+
below. Nothing touches `main`/`master` yet; all additions are pure/non-breaking (no case flipped, no
|
|
92
|
+
shim removed, no existing API changed).
|
|
93
|
+
|
|
94
|
+
**Key findings (corrected the design docs):**
|
|
95
|
+
- A **template IS a page** — `Query::Templates` returns PageUnion; structure reads via the page
|
|
96
|
+
model. `Builder::Template` (create/update with `commands:`) + `executeWorkflowCommands` already
|
|
97
|
+
exist. So build-from-scratch is unblocked; only workflow-config *diff* waits on the partial
|
|
98
|
+
`PagesWorkflow.stages` read model.
|
|
99
|
+
- **`placeholderId` is the id-threading primitive** — commands reference not-yet-created nodes
|
|
100
|
+
intra-batch by client-chosen placeholder; simpler than the location engine's `newId` remap.
|
|
101
|
+
- **`build_commands` is non-idempotent** for key-renaming commands (addSelectFieldOption:
|
|
102
|
+
data_field_id→dataFieldId). Emitter output is FINAL built form, straight to the mutation.
|
|
103
|
+
- Ooze: first-wave cutover cases (RegisterUpdateCase/TargetOozesUpdateCase) are **force-free**
|
|
104
|
+
(migratable now); forces live only in downstream training scripts. Native `Pages::Page::Base`
|
|
105
|
+
already gives the loop skeleton to build on.
|
|
106
|
+
|
|
107
|
+
**Branches (all green, rubocop clean, UNMERGED):**
|
|
108
|
+
| Repo | Branch | Contents | Base |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| gem | `feature/template-editing-pipeline` | `tests/dump_template_model.rb`, WorkflowCommand serialization spec (10 ex), `template-maintenance/PHASE0-FINDINGS.md` | main @03799f5 |
|
|
111
|
+
| gem | `feature/ooze-native-phase0` | `ooze-graphql-native-migration/INVENTORY.md` + TODO scope note | main @03799f5 |
|
|
112
|
+
| eco-helpers | `feature/ooze-graphql-native-migration` | `graphql/helpers/pages/{shortcuts,filters,rescuable}` + specs (37 ex) | master @5b9d7c07 |
|
|
113
|
+
| eco-helpers | `feature/template-editing-pipeline` | `graphql/samples/pages/template/{command_emitter,base}` + spec (9 ex) | master @5b9d7c07 |
|
|
114
|
+
|
|
115
|
+
**STRATEGIC MERGE PLAN (for Oscar to action):**
|
|
116
|
+
1. **Gem first** (no cross-repo dep): merge `feature/ooze-native-phase0` (docs-only) then
|
|
117
|
+
`feature/template-editing-pipeline` into `main`. Independent; either order. No release bump
|
|
118
|
+
needed (tool + specs + docs; patch-level if tagging).
|
|
119
|
+
2. **eco-helpers next** (depends only on the *released* gem v1.3.9, which ships the WorkflowCommand
|
|
120
|
+
contract — no gem changes are required by either eco-helpers branch): merge
|
|
121
|
+
`feature/ooze-graphql-native-migration` (pure helper additions) then
|
|
122
|
+
`feature/template-editing-pipeline` (samples additions) into `master`. Independent of each other.
|
|
123
|
+
3. **Order rationale:** gem branches carry no runtime coupling to eco-helpers; eco-helpers branches
|
|
124
|
+
only reference the *already-released* gem constant `Input::WorkflowCommand` at runtime. So there
|
|
125
|
+
is **no hard merge-order dependency** — the split is by repo, and everything is additive.
|
|
126
|
+
4. **Do NOT** merge as a case flip / shim removal — those stay `[ ]` in the ooze TODO until parity
|
|
127
|
+
is proven (unchanged by this session).
|
|
128
|
+
|
|
129
|
+
**★ NEXT SESSION:**
|
|
130
|
+
- Template Phase 2/3: extend the emitter (editStage/editFieldConfiguration/gauge stops), then the
|
|
131
|
+
**sandbox live characterization** — run `dump_template_model.rb` on `6a3fa5b8…622b`, replay via
|
|
132
|
+
Template::Base, assert structure-equivalence. Needs a sandbox org + creds (Open Q #1).
|
|
133
|
+
- Ooze Phase 1/2: re-express `OozeHandlers#merge_values` + native `Creatable` against GraphQL
|
|
134
|
+
data-field types; then native `Register::Base` on top of `Pages::Page::Base`; A/B parity harness.
|
|
135
|
+
- Still open from prior: FARMERS/Travis support (F1 simulate-noop, F2 stale-patchVer) — locate
|
|
136
|
+
`FARMERS_GRAPHQL_HANDOVER.md` (api-deprecation branch, likely training repo).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
11
140
|
## ▶ DEEP REVIEW (2026-06-30 late) — pre-live audit of TOOCS + CANS; 3 more fixes
|
|
12
141
|
|
|
13
142
|
Exhaustive audit (4 parallel sub-investigations) of both prod dry-runs before going live.
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
class Fragment
|
|
5
|
-
fragment :LocationDraft, <<~GRAPHQL
|
|
6
|
-
fragment LocationDraft on Draft {
|
|
7
|
-
id
|
|
8
|
-
name
|
|
9
|
-
notes
|
|
10
|
-
committed
|
|
11
|
-
createdAt
|
|
12
|
-
ok
|
|
13
|
-
conflictingIds
|
|
14
|
-
results {
|
|
15
|
-
ok
|
|
16
|
-
command {
|
|
17
|
-
id
|
|
18
|
-
state
|
|
19
|
-
__typename
|
|
20
|
-
... on LocationInsertCommand {
|
|
21
|
-
nodeId name parentId insertBefore classificationIds
|
|
22
|
-
}
|
|
23
|
-
... on LocationUpdateCommand {
|
|
24
|
-
nodeId newId
|
|
25
|
-
newName: name
|
|
26
|
-
newClassificationIds: classificationIds
|
|
27
|
-
}
|
|
28
|
-
... on LocationArchiveCommand { nodeId }
|
|
29
|
-
... on LocationUnarchiveCommand { nodeId }
|
|
30
|
-
... on LocationMoveCommand { nodeId parentId insertBefore }
|
|
31
|
-
... on LocationReorderCommand { parentId orderedIds }
|
|
32
|
-
}
|
|
33
|
-
error {
|
|
34
|
-
message
|
|
35
|
-
conflictingIds
|
|
36
|
-
validationErrors {
|
|
37
|
-
property
|
|
38
|
-
error
|
|
39
|
-
message
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
commands {
|
|
44
|
-
id
|
|
45
|
-
state
|
|
46
|
-
__typename
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
GRAPHQL
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
class Fragment
|
|
5
|
+
fragment :LocationDraft, <<~GRAPHQL
|
|
6
|
+
fragment LocationDraft on Draft {
|
|
7
|
+
id
|
|
8
|
+
name
|
|
9
|
+
notes
|
|
10
|
+
committed
|
|
11
|
+
createdAt
|
|
12
|
+
ok
|
|
13
|
+
conflictingIds
|
|
14
|
+
results {
|
|
15
|
+
ok
|
|
16
|
+
command {
|
|
17
|
+
id
|
|
18
|
+
state
|
|
19
|
+
__typename
|
|
20
|
+
... on LocationInsertCommand {
|
|
21
|
+
nodeId name parentId insertBefore classificationIds
|
|
22
|
+
}
|
|
23
|
+
... on LocationUpdateCommand {
|
|
24
|
+
nodeId newId
|
|
25
|
+
newName: name
|
|
26
|
+
newClassificationIds: classificationIds
|
|
27
|
+
}
|
|
28
|
+
... on LocationArchiveCommand { nodeId }
|
|
29
|
+
... on LocationUnarchiveCommand { nodeId }
|
|
30
|
+
... on LocationMoveCommand { nodeId parentId insertBefore }
|
|
31
|
+
... on LocationReorderCommand { parentId orderedIds }
|
|
32
|
+
}
|
|
33
|
+
error {
|
|
34
|
+
message
|
|
35
|
+
conflictingIds
|
|
36
|
+
validationErrors {
|
|
37
|
+
property
|
|
38
|
+
error
|
|
39
|
+
message
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
commands {
|
|
44
|
+
id
|
|
45
|
+
state
|
|
46
|
+
__typename
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
GRAPHQL
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Query
|
|
5
|
-
class LocationStructure
|
|
6
|
-
class Draft < Logic::Query
|
|
7
|
-
accepted_params :id
|
|
8
|
-
accepted_params :structureId
|
|
9
|
-
accepted_params :includeArchivedNodes, default: true
|
|
10
|
-
|
|
11
|
-
field_name :draft
|
|
12
|
-
|
|
13
|
-
class_resolver :item_class, Model::LocationStructure::Draft
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def basic_block(&block)
|
|
18
|
-
final_block = block || default_query_block
|
|
19
|
-
proc {
|
|
20
|
-
query(
|
|
21
|
-
id: :id!,
|
|
22
|
-
structureId: :id!,
|
|
23
|
-
includeArchivedNodes: :boolean
|
|
24
|
-
) {
|
|
25
|
-
currentOrganization {
|
|
26
|
-
locations {
|
|
27
|
-
structure(
|
|
28
|
-
id: :structureId
|
|
29
|
-
) {
|
|
30
|
-
id
|
|
31
|
-
name
|
|
32
|
-
archived
|
|
33
|
-
updatedAt
|
|
34
|
-
draft(
|
|
35
|
-
id: :id,
|
|
36
|
-
&final_block
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def default_query_block
|
|
46
|
-
proc {
|
|
47
|
-
spread :LocationDraft
|
|
48
|
-
structure {
|
|
49
|
-
nodes(
|
|
50
|
-
includeArchived: :includeArchivedNodes
|
|
51
|
-
) {
|
|
52
|
-
spread :LocationNode
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end # Query
|
|
60
|
-
end # GraphQL
|
|
61
|
-
end # API
|
|
62
|
-
end # Ecoportal
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class LocationStructure
|
|
6
|
+
class Draft < Logic::Query
|
|
7
|
+
accepted_params :id
|
|
8
|
+
accepted_params :structureId
|
|
9
|
+
accepted_params :includeArchivedNodes, default: true
|
|
10
|
+
|
|
11
|
+
field_name :draft
|
|
12
|
+
|
|
13
|
+
class_resolver :item_class, Model::LocationStructure::Draft
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def basic_block(&block)
|
|
18
|
+
final_block = block || default_query_block
|
|
19
|
+
proc {
|
|
20
|
+
query(
|
|
21
|
+
id: :id!,
|
|
22
|
+
structureId: :id!,
|
|
23
|
+
includeArchivedNodes: :boolean
|
|
24
|
+
) {
|
|
25
|
+
currentOrganization {
|
|
26
|
+
locations {
|
|
27
|
+
structure(
|
|
28
|
+
id: :structureId
|
|
29
|
+
) {
|
|
30
|
+
id
|
|
31
|
+
name
|
|
32
|
+
archived
|
|
33
|
+
updatedAt { dateTime timeZone }
|
|
34
|
+
draft(
|
|
35
|
+
id: :id,
|
|
36
|
+
&final_block
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def default_query_block
|
|
46
|
+
proc {
|
|
47
|
+
spread :LocationDraft
|
|
48
|
+
structure {
|
|
49
|
+
nodes(
|
|
50
|
+
includeArchived: :includeArchivedNodes
|
|
51
|
+
) {
|
|
52
|
+
spread :LocationNode
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end # Query
|
|
60
|
+
end # GraphQL
|
|
61
|
+
end # API
|
|
62
|
+
end # Ecoportal
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Query
|
|
5
|
-
class LocationStructure < Logic::Query
|
|
6
|
-
accepted_params :id
|
|
7
|
-
accepted_params :includeArchivedNodes, default: true
|
|
8
|
-
|
|
9
|
-
field_name :structure
|
|
10
|
-
|
|
11
|
-
class_resolver :item_class, Model::LocationStructure
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def basic_block(&block)
|
|
16
|
-
final_block = block || default_query_block
|
|
17
|
-
proc {
|
|
18
|
-
query(
|
|
19
|
-
id: :id!,
|
|
20
|
-
includeArchivedNodes: :boolean
|
|
21
|
-
) {
|
|
22
|
-
currentOrganization {
|
|
23
|
-
locations {
|
|
24
|
-
structure(id: :id, &final_block)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# At the moment it always retrieves archived nodes!!
|
|
32
|
-
# @note this is on purpose, as via API
|
|
33
|
-
# there isn't much sense in not including archived nodes.
|
|
34
|
-
def default_query_block
|
|
35
|
-
proc {
|
|
36
|
-
id
|
|
37
|
-
name
|
|
38
|
-
archived
|
|
39
|
-
weight
|
|
40
|
-
updatedAt
|
|
41
|
-
visitorManagementEnabled
|
|
42
|
-
nodes(
|
|
43
|
-
includeArchived: :includeArchivedNodes
|
|
44
|
-
) {
|
|
45
|
-
spread :LocationNode
|
|
46
|
-
}
|
|
47
|
-
drafts {
|
|
48
|
-
id
|
|
49
|
-
createdAt
|
|
50
|
-
name
|
|
51
|
-
notes
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
require_relative 'location_structure/draft'
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class LocationStructure < Logic::Query
|
|
6
|
+
accepted_params :id
|
|
7
|
+
accepted_params :includeArchivedNodes, default: true
|
|
8
|
+
|
|
9
|
+
field_name :structure
|
|
10
|
+
|
|
11
|
+
class_resolver :item_class, Model::LocationStructure
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def basic_block(&block)
|
|
16
|
+
final_block = block || default_query_block
|
|
17
|
+
proc {
|
|
18
|
+
query(
|
|
19
|
+
id: :id!,
|
|
20
|
+
includeArchivedNodes: :boolean
|
|
21
|
+
) {
|
|
22
|
+
currentOrganization {
|
|
23
|
+
locations {
|
|
24
|
+
structure(id: :id, &final_block)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# At the moment it always retrieves archived nodes!!
|
|
32
|
+
# @note this is on purpose, as via API
|
|
33
|
+
# there isn't much sense in not including archived nodes.
|
|
34
|
+
def default_query_block
|
|
35
|
+
proc {
|
|
36
|
+
id
|
|
37
|
+
name
|
|
38
|
+
archived
|
|
39
|
+
weight
|
|
40
|
+
updatedAt { dateTime timeZone }
|
|
41
|
+
visitorManagementEnabled
|
|
42
|
+
nodes(
|
|
43
|
+
includeArchived: :includeArchivedNodes
|
|
44
|
+
) {
|
|
45
|
+
spread :LocationNode
|
|
46
|
+
}
|
|
47
|
+
drafts {
|
|
48
|
+
id
|
|
49
|
+
createdAt
|
|
50
|
+
name
|
|
51
|
+
notes
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
require_relative 'location_structure/draft'
|
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
# @attr_reader client [Common::GraphQL::Client] a client object that holds the configuration of the api connection.
|
|
4
|
-
# @attr_reader logger [Logger] the logger.
|
|
5
|
-
class GraphQL
|
|
6
|
-
include Ecoportal::API::Common::GraphQL::ClassHelpers
|
|
7
|
-
|
|
8
|
-
class_resolver :client_class, Ecoportal::API::Common::GraphQL::Client
|
|
9
|
-
|
|
10
|
-
attr_reader :client, :fragments
|
|
11
|
-
|
|
12
|
-
# Creates a `GraphQL` object to interact with the ecoPortal `GraphQL API`.
|
|
13
|
-
# @param org_id [String] the id of the target organization.
|
|
14
|
-
# It defaults to the environmental variable `ORGANIZATION_ID`, if defined
|
|
15
|
-
# @param logger [Logger] an object with `Logger` interface to generate logs.
|
|
16
|
-
def initialize(email: nil, pass: nil, org_id: nil, host: "live.ecoportal.com")
|
|
17
|
-
kargs = {
|
|
18
|
-
email: email,
|
|
19
|
-
pass: pass,
|
|
20
|
-
host: host,
|
|
21
|
-
org_id: org_id,
|
|
22
|
-
no_schema: true
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@client = client_class.new(**kargs)
|
|
26
|
-
@client.http_client = Ecoportal::API::Common::GraphQL::HttpClient.new(
|
|
27
|
-
email: email,
|
|
28
|
-
pass: pass,
|
|
29
|
-
org_id: org_id,
|
|
30
|
-
host: host
|
|
31
|
-
)
|
|
32
|
-
@fragments = Ecoportal::API::GraphQL::Fragment.new(client)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def currentOrganizationClass
|
|
36
|
-
API::GraphQL::Model::Organization.tap do |org_class|
|
|
37
|
-
org_class.client = client
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def currentOrganization
|
|
42
|
-
currentOrganizationClass
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def createContractorEntity(input:, &block)
|
|
46
|
-
createContractorEntityMutation.query(input: input, &block)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def contractorEntity
|
|
50
|
-
Ecoportal::API::GraphQL::Builder::ContractorEntity.new(client)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Gives a builder to use different options to modify a reporting structure
|
|
54
|
-
def locationStructure
|
|
55
|
-
Ecoportal::API::GraphQL::Builder::LocationStructure.new(client)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Gives a builder to use different options to play with action
|
|
59
|
-
def action
|
|
60
|
-
Ecoportal::API::GraphQL::Builder::Action.new(client)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Gives a builder to use different options to work with pages
|
|
64
|
-
def page
|
|
65
|
-
Ecoportal::API::GraphQL::Builder::Page.new(client)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# v2-compatible pages API — exposes get/get_new/create/update/get_body
|
|
69
|
-
# matching the interface eco-helpers scripts use against ecoportal-api-v2.
|
|
70
|
-
def pages
|
|
71
|
-
Compat::Pages.new(client)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# v2-compatible registers API — exposes search with cursor pagination.
|
|
75
|
-
def registers
|
|
76
|
-
Compat::Registers.new(client)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Mutation builder for register CRUD + preset view management.
|
|
80
|
-
# Distinct from #registers (compat search layer).
|
|
81
|
-
#
|
|
82
|
-
# Usage:
|
|
83
|
-
# api.register.create(input: { name: 'Safety', moduleType: 'form', filterTags: [] })
|
|
84
|
-
# api.register.preset_view.list('REG_ID')
|
|
85
|
-
# api.register.preset_view.create(input: { registerId: 'R', name: 'Active', fieldConfigs: [] })
|
|
86
|
-
def register
|
|
87
|
-
Ecoportal::API::GraphQL::Builder::Register.new(client)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Kickstand workflow engine controls — start/stop/fail individual workflows
|
|
91
|
-
# or bulk-update a set. Internal back-end automation; not for customer scripts.
|
|
92
|
-
def kickstand
|
|
93
|
-
Builder::Kickstand.new(client)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Template management — create/update/publish/unpublish templates and manage
|
|
97
|
-
# template information and related pages.
|
|
98
|
-
def template
|
|
99
|
-
Builder::Template.new(client)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# File upload orchestrator — get S3 credentials, upload to S3, register, poll.
|
|
103
|
-
# Returns a fileContainerId string usable in FileField/ImageGallery mutations.
|
|
104
|
-
#
|
|
105
|
-
# Usage:
|
|
106
|
-
# id = api.file_upload.upload('/path/to/report.pdf')
|
|
107
|
-
# page.components.get_by_name('Report').file_container_ids = [id]
|
|
108
|
-
# api.pages.update(page)
|
|
109
|
-
def file_upload
|
|
110
|
-
Ecoportal::API::GraphQL::FileUpload::Client.new(self)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
private
|
|
114
|
-
|
|
115
|
-
def createContractorEntityMutation
|
|
116
|
-
Ecoportal::API::GraphQL::Mutation::ContractorEntity::Create.new(client)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
require_relative 'graphql/helpers'
|
|
123
|
-
require_relative 'graphql/concerns'
|
|
124
|
-
require_relative 'graphql/logic/base_model'
|
|
125
|
-
require_relative 'graphql/error'
|
|
126
|
-
require_relative 'graphql/interface'
|
|
127
|
-
require_relative 'graphql/base'
|
|
128
|
-
require_relative 'graphql/model'
|
|
129
|
-
require_relative 'graphql/logic'
|
|
130
|
-
require_relative 'graphql/connection'
|
|
131
|
-
require_relative 'graphql/payload'
|
|
132
|
-
require_relative 'graphql/input'
|
|
133
|
-
require_relative 'graphql/fragment'
|
|
134
|
-
require_relative 'graphql/query'
|
|
135
|
-
require_relative 'graphql/mutation'
|
|
136
|
-
require_relative 'graphql/builder'
|
|
137
|
-
require_relative 'graphql/compat'
|
|
138
|
-
require_relative 'graphql/file_upload'
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
# @attr_reader client [Common::GraphQL::Client] a client object that holds the configuration of the api connection.
|
|
4
|
+
# @attr_reader logger [Logger] the logger.
|
|
5
|
+
class GraphQL
|
|
6
|
+
include Ecoportal::API::Common::GraphQL::ClassHelpers
|
|
7
|
+
|
|
8
|
+
class_resolver :client_class, Ecoportal::API::Common::GraphQL::Client
|
|
9
|
+
|
|
10
|
+
attr_reader :client, :fragments
|
|
11
|
+
|
|
12
|
+
# Creates a `GraphQL` object to interact with the ecoPortal `GraphQL API`.
|
|
13
|
+
# @param org_id [String] the id of the target organization.
|
|
14
|
+
# It defaults to the environmental variable `ORGANIZATION_ID`, if defined
|
|
15
|
+
# @param logger [Logger] an object with `Logger` interface to generate logs.
|
|
16
|
+
def initialize(email: nil, pass: nil, org_id: nil, host: "live.ecoportal.com")
|
|
17
|
+
kargs = {
|
|
18
|
+
email: email,
|
|
19
|
+
pass: pass,
|
|
20
|
+
host: host,
|
|
21
|
+
org_id: org_id,
|
|
22
|
+
no_schema: true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@client = client_class.new(**kargs)
|
|
26
|
+
@client.http_client = Ecoportal::API::Common::GraphQL::HttpClient.new(
|
|
27
|
+
email: email,
|
|
28
|
+
pass: pass,
|
|
29
|
+
org_id: org_id,
|
|
30
|
+
host: host
|
|
31
|
+
)
|
|
32
|
+
@fragments = Ecoportal::API::GraphQL::Fragment.new(client)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def currentOrganizationClass
|
|
36
|
+
API::GraphQL::Model::Organization.tap do |org_class|
|
|
37
|
+
org_class.client = client
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def currentOrganization
|
|
42
|
+
currentOrganizationClass
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def createContractorEntity(input:, &block)
|
|
46
|
+
createContractorEntityMutation.query(input: input, &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def contractorEntity
|
|
50
|
+
Ecoportal::API::GraphQL::Builder::ContractorEntity.new(client)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Gives a builder to use different options to modify a reporting structure
|
|
54
|
+
def locationStructure
|
|
55
|
+
Ecoportal::API::GraphQL::Builder::LocationStructure.new(client)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Gives a builder to use different options to play with action
|
|
59
|
+
def action
|
|
60
|
+
Ecoportal::API::GraphQL::Builder::Action.new(client)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Gives a builder to use different options to work with pages
|
|
64
|
+
def page
|
|
65
|
+
Ecoportal::API::GraphQL::Builder::Page.new(client)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# v2-compatible pages API — exposes get/get_new/create/update/get_body
|
|
69
|
+
# matching the interface eco-helpers scripts use against ecoportal-api-v2.
|
|
70
|
+
def pages
|
|
71
|
+
Compat::Pages.new(client)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# v2-compatible registers API — exposes search with cursor pagination.
|
|
75
|
+
def registers
|
|
76
|
+
Compat::Registers.new(client)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Mutation builder for register CRUD + preset view management.
|
|
80
|
+
# Distinct from #registers (compat search layer).
|
|
81
|
+
#
|
|
82
|
+
# Usage:
|
|
83
|
+
# api.register.create(input: { name: 'Safety', moduleType: 'form', filterTags: [] })
|
|
84
|
+
# api.register.preset_view.list('REG_ID')
|
|
85
|
+
# api.register.preset_view.create(input: { registerId: 'R', name: 'Active', fieldConfigs: [] })
|
|
86
|
+
def register
|
|
87
|
+
Ecoportal::API::GraphQL::Builder::Register.new(client)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Kickstand workflow engine controls — start/stop/fail individual workflows
|
|
91
|
+
# or bulk-update a set. Internal back-end automation; not for customer scripts.
|
|
92
|
+
def kickstand
|
|
93
|
+
Builder::Kickstand.new(client)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Template management — create/update/publish/unpublish templates and manage
|
|
97
|
+
# template information and related pages.
|
|
98
|
+
def template
|
|
99
|
+
Builder::Template.new(client)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# File upload orchestrator — get S3 credentials, upload to S3, register, poll.
|
|
103
|
+
# Returns a fileContainerId string usable in FileField/ImageGallery mutations.
|
|
104
|
+
#
|
|
105
|
+
# Usage:
|
|
106
|
+
# id = api.file_upload.upload('/path/to/report.pdf')
|
|
107
|
+
# page.components.get_by_name('Report').file_container_ids = [id]
|
|
108
|
+
# api.pages.update(page)
|
|
109
|
+
def file_upload
|
|
110
|
+
Ecoportal::API::GraphQL::FileUpload::Client.new(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def createContractorEntityMutation
|
|
116
|
+
Ecoportal::API::GraphQL::Mutation::ContractorEntity::Create.new(client)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
require_relative 'graphql/helpers'
|
|
123
|
+
require_relative 'graphql/concerns'
|
|
124
|
+
require_relative 'graphql/logic/base_model'
|
|
125
|
+
require_relative 'graphql/error'
|
|
126
|
+
require_relative 'graphql/interface'
|
|
127
|
+
require_relative 'graphql/base'
|
|
128
|
+
require_relative 'graphql/model'
|
|
129
|
+
require_relative 'graphql/logic'
|
|
130
|
+
require_relative 'graphql/connection'
|
|
131
|
+
require_relative 'graphql/payload'
|
|
132
|
+
require_relative 'graphql/input'
|
|
133
|
+
require_relative 'graphql/fragment'
|
|
134
|
+
require_relative 'graphql/query'
|
|
135
|
+
require_relative 'graphql/mutation'
|
|
136
|
+
require_relative 'graphql/builder'
|
|
137
|
+
require_relative 'graphql/compat'
|
|
138
|
+
require_relative 'graphql/file_upload'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'ecoportal/api-graphql'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'graphql'
|
|
4
|
+
|
|
5
|
+
# Validate EVERY gem query against a GraphQL schema, offline — catches the whole class of
|
|
6
|
+
# "field must have selections" (object field selected bare) and "selections can't be made on
|
|
7
|
+
# scalars" (scalar over-selected) bugs before they reach a client runner.
|
|
8
|
+
#
|
|
9
|
+
# This is how the 2026-07-03 LocationStructure `updatedAt` prod failure (and a sibling
|
|
10
|
+
# `createdAt` over-selection in the LocationDraft fragment) were found. Those queries had no
|
|
11
|
+
# spec; this script would have flagged them.
|
|
12
|
+
#
|
|
13
|
+
# It renders each query's block offline (auth is stubbed — no network), assembles its fragments,
|
|
14
|
+
# and validates the full document against a schema introspection JSON.
|
|
15
|
+
#
|
|
16
|
+
# Usage (run under bundler so the graphlient fork's #to_query_string is available):
|
|
17
|
+
# bundle exec ruby tests/validate_queries.rb [PATH_TO_SCHEMA_INTROSPECTION_JSON]
|
|
18
|
+
#
|
|
19
|
+
# Get a fresh schema JSON via an introspection query against the org's endpoint; the default
|
|
20
|
+
# below points at the last captured dump. Prefer a fresh dump — a stale one yields false
|
|
21
|
+
# "field doesn't exist" noise (harmless: this script only *fails* on the two structural classes).
|
|
22
|
+
|
|
23
|
+
DEFAULT_SCHEMA = File.expand_path('../.ai-assistance/tmp/20260605T101224_live_ep_graphql_schema.graphql.json', __dir__)
|
|
24
|
+
STRUCTURAL = ['must have selections', "can't be made on scalars"].freeze
|
|
25
|
+
|
|
26
|
+
schema_path = ARGV[0] || DEFAULT_SCHEMA
|
|
27
|
+
unless File.exist?(schema_path)
|
|
28
|
+
warn "Schema introspection JSON not found: #{schema_path}"
|
|
29
|
+
warn 'Pass a path: bundle exec ruby tests/validate_queries.rb <schema.json>'
|
|
30
|
+
exit 2
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build a client offline — stub the session token so no network/creds are needed for rendering.
|
|
34
|
+
Ecoportal::API::Common::GraphQL::Client.class_eval do
|
|
35
|
+
def session_token(*_args, **_kwargs)
|
|
36
|
+
'dummy'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
client = Ecoportal::API::Common::GraphQL::Client.new(
|
|
40
|
+
email: 'validate@local', pass: 'x', org_id: 'validate', host: 'live.ecoportal.com', no_schema: true
|
|
41
|
+
)
|
|
42
|
+
schema = GraphQL::Schema.from_introspection(JSON.parse(File.read(schema_path)))
|
|
43
|
+
|
|
44
|
+
# Collect every concrete query class (recursively, incl. nested like LocationStructure::Draft).
|
|
45
|
+
query_classes = []
|
|
46
|
+
collect = lambda do |mod|
|
|
47
|
+
mod.constants.each do |const|
|
|
48
|
+
obj = begin
|
|
49
|
+
mod.const_get(const)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
next unless obj.is_a?(Module)
|
|
54
|
+
|
|
55
|
+
query_classes << obj if obj.is_a?(Class) && obj.private_method_defined?(:basic_block)
|
|
56
|
+
collect.call(obj)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
collect.call(Ecoportal::API::GraphQL::Query)
|
|
60
|
+
query_classes.uniq!
|
|
61
|
+
|
|
62
|
+
structural_bugs = {}
|
|
63
|
+
other_errors = {}
|
|
64
|
+
render_skips = []
|
|
65
|
+
|
|
66
|
+
query_classes.sort_by(&:name).each do |klass|
|
|
67
|
+
inst = klass.new(client)
|
|
68
|
+
query_str = client.to_query_string(&inst.send(:basic_block))
|
|
69
|
+
fragments = begin
|
|
70
|
+
inst.send(:assemble_fragments, query_str)
|
|
71
|
+
rescue StandardError
|
|
72
|
+
''
|
|
73
|
+
end
|
|
74
|
+
full = fragments.to_s.empty? ? query_str : "#{query_str}\n\n#{fragments}"
|
|
75
|
+
messages = schema.validate(full).map(&:message)
|
|
76
|
+
bugs = messages.select { |m| STRUCTURAL.any? { |s| m.include?(s) } }
|
|
77
|
+
other = messages - bugs
|
|
78
|
+
structural_bugs[klass.name] = bugs unless bugs.empty?
|
|
79
|
+
other_errors[klass.name] = other unless other.empty?
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
render_skips << "#{klass.name} — #{e.class}: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
puts "Validated #{query_classes.size} query classes against #{File.basename(schema_path)}\n\n"
|
|
85
|
+
|
|
86
|
+
puts '## STRUCTURAL BUGS (fail the build — bare object / over-selected scalar):'
|
|
87
|
+
if structural_bugs.empty?
|
|
88
|
+
puts ' none ✅'
|
|
89
|
+
else
|
|
90
|
+
structural_bugs.each { |name, msgs| puts " #{name}\n - #{msgs.join("\n - ")}" }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n## OTHER validation findings (verify — may be schema-dump staleness or union handling):"
|
|
94
|
+
if other_errors.empty?
|
|
95
|
+
puts ' none'
|
|
96
|
+
else
|
|
97
|
+
other_errors.each { |name, msgs| puts " #{name}\n - #{msgs.first(3).join("\n - ")}" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
unless render_skips.empty?
|
|
101
|
+
puts "\n## RENDER-SKIPS (could not auto-render — check manually):"
|
|
102
|
+
render_skips.each { |s| puts " #{s}" }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
exit(structural_bugs.empty? ? 0 : 1)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ecoportal-api-graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oscar Segura
|
|
@@ -830,6 +830,7 @@ files:
|
|
|
830
830
|
- tests/loc_structure_update.rb
|
|
831
831
|
- tests/loc_structures_get.rb
|
|
832
832
|
- tests/local_libs.rb
|
|
833
|
+
- tests/validate_queries.rb
|
|
833
834
|
homepage: https://www.ecoportal.com
|
|
834
835
|
licenses:
|
|
835
836
|
- MIT
|