eco-helpers 3.2.23 → 3.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +153 -21
- data/lib/eco/api/common/session/base_session.rb +4 -0
- data/lib/eco/api/common/session/environment.rb +5 -0
- data/lib/eco/api/custom/cli.rb +3 -0
- data/lib/eco/api/session/config/api.rb +33 -14
- data/lib/eco/api/usecases/graphql/helpers/access_logs/base/reader.rb +59 -0
- data/lib/eco/api/usecases/graphql/helpers/access_logs/base.rb +17 -0
- data/lib/eco/api/usecases/graphql/helpers/access_logs.rb +7 -0
- data/lib/eco/api/usecases/graphql/helpers/base/connection_reader.rb +70 -0
- data/lib/eco/api/usecases/graphql/helpers/base/graphql_env.rb +6 -2
- data/lib/eco/api/usecases/graphql/helpers/base.rb +1 -0
- data/lib/eco/api/usecases/graphql/helpers/contractors/base/manager_settings.rb +64 -0
- data/lib/eco/api/usecases/graphql/helpers/contractors/base.rb +2 -0
- data/lib/eco/api/usecases/graphql/helpers/dashboards/base/reader.rb +30 -0
- data/lib/eco/api/usecases/graphql/helpers/dashboards/base.rb +20 -0
- data/lib/eco/api/usecases/graphql/helpers/dashboards.rb +7 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/activities.rb +51 -0
- data/lib/eco/api/usecases/graphql/helpers/pages.rb +1 -0
- data/lib/eco/api/usecases/graphql/helpers.rb +2 -0
- data/lib/eco/api/usecases/graphql/samples/contractors/dsl.rb +16 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +46 -37
- data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +9 -2
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +1 -1
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +46 -7
- data/lib/eco/data/files/encoding.rb +5 -2
- data/lib/eco/version.rb +1 -1
- metadata +17 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c5e64bb2f9a25359774eb01482180a7efdbade0e2b6e4d02a56a4c9dbb1d91f
|
|
4
|
+
data.tar.gz: ea2fd73dc9709ca1710e9aff527f0ae856a55fe0638a7c3862b71bfd9ce5270c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37dfa8178019e8a4854f3b98386682def2ff8f51b1f7c9dcf95dff4947eea662f14145ab93434ceb8f749acc7981d6904114bc8ad8f4f6d251ed217a28a43ce5
|
|
7
|
+
data.tar.gz: 4653193b58ac2f88bb76b06c4ee094331b2f034ccae4929d5722398105e2035a982e136fd6fda78d207c33e5ddc4aff9f5c489a11abd155382607d94bca8105d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,41 +2,173 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [Unreleased]
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## [3.3.3] - 2026-09-30
|
|
8
|
+
|
|
9
|
+
Security republish of 3.3.2. **Backwards-compatible; no code change**: same dependencies, same
|
|
8
10
|
behaviour. Supersedes the published versions of this line, which are to be yanked.
|
|
9
11
|
|
|
10
12
|
### Security
|
|
11
13
|
|
|
12
14
|
- Removed customer and internal identifiers from shipped code comments and this changelog (a
|
|
13
15
|
comment listing customer organisations and their case names; internal repository names and
|
|
14
|
-
internal documentation paths). Comments and changelog only.
|
|
15
|
-
`CLAUDE.md` files that 3.2.22 packaged under `lib/` no longer ship.
|
|
16
|
+
internal documentation paths). Comments and changelog only.
|
|
16
17
|
- Packaging: the three runtime JSON files are declared in `.release-smoke-allow`, and the release
|
|
17
18
|
tasks accept `release/*` maintenance branches.
|
|
18
19
|
|
|
19
|
-
## [3.2
|
|
20
|
-
|
|
21
|
-
### Fixed
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
## [3.3.2] - 2026-09-02
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Gemspec `spec.files` allowlisted by ROOT DIRECTORY (`lib/`, `exe/`) rather than by full path,
|
|
25
|
+
so any non-`.rb` file added under `lib/` shipped silently. As of 3.3.1 that was five `CLAUDE.md`
|
|
26
|
+
agent-instruction files under `lib/eco/api/usecases/`. `spec.files` is now a full-path
|
|
27
|
+
allowlist — `lib/**/*.rb`, three named JSON files genuinely read at runtime
|
|
28
|
+
(`preferences_reference.json`, `presets_integrity.json`, `presets_values.json`), and
|
|
29
|
+
`README.md` / `CHANGELOG.md` / `LICENSE` at the root — intersected with `git ls-files`.
|
|
30
|
+
`spec/packaging_spec.rb` pins the allowlist so this cannot regress silently again.
|
|
27
31
|
|
|
28
|
-
## [3.
|
|
32
|
+
## [3.3.1] - 2026-08-13
|
|
29
33
|
|
|
30
34
|
### Fixed
|
|
31
35
|
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
- `Eco::Data::Files::Encoding#get_file_content_with_encoding` read files in **text mode**, which on
|
|
37
|
+
Windows silently translates `\r\n` to bare `\n` while loading — callers never see the file's
|
|
38
|
+
actual bytes. Both read branches (BOM and plain) now use binary mode (`mode: 'rb'`), preserving
|
|
39
|
+
the bytes exactly; BOM stripping is unchanged. Linux behaviour is identical before and after
|
|
40
|
+
(text mode never translated anything there) — this makes Windows match Linux. Surfaced by Travis
|
|
41
|
+
on a customer's tooltip CSV (reported from a downstream script repo).
|
|
42
|
+
|
|
43
|
+
**Precision note (2026-08-13, verified against the motivating file before this version was
|
|
44
|
+
tagged):** the earlier draft of this entry claimed `Eco::CSV.read` raised
|
|
45
|
+
`CSV::MalformedCSVError` on that CSV. It does not — `Eco::CSV.read` parses the collapsed
|
|
46
|
+
*string*, and a fully collapsed LF-only file with quoted embedded newlines is still legal CSV
|
|
47
|
+
(502 rows parse fine on 3.3.0). The `MalformedCSVError` in the BLOCKED-TR-04 report came from
|
|
48
|
+
stdlib `::CSV.read(path)`, whose own IO/row-sep handling is a different code path. The fix here
|
|
49
|
+
is byte-fidelity hardening: text-mode reads silently *altered data* (any consumer comparing,
|
|
50
|
+
hashing, or round-tripping file content got different bytes on Windows than on Linux), and
|
|
51
|
+
partial/mixed line-ending inputs remain genuinely parse-hazardous under translation.
|
|
52
|
+
|
|
53
|
+
## [3.3.0] - 2026-08-13
|
|
54
|
+
|
|
55
|
+
Adopts **`ecoportal-api-graphql` 2.0.0** (published 2026-08-13): floor raised from
|
|
56
|
+
`~> 1.3, >= 1.3.14` to `~> 2.0, >= 2.0.0`. Minor rather than major here — 2.0.0's breaking set
|
|
57
|
+
removed surfaces that either targeted mutations the live schema never had or were dead accessors,
|
|
58
|
+
and a sweep of this repo finds **no consumer of any of them** (`editPage`,
|
|
59
|
+
`editPageCreatorPermissions`, the AI-summary mutations, the SmartFill pair — zero hits outside
|
|
60
|
+
this repo's own readiness notes). Nothing in `lib/` changes behaviour as a result of the bump.
|
|
61
|
+
|
|
62
|
+
★ **What starts WORKING, which matters more here than what broke.** Several paths were sending
|
|
63
|
+
documents the schema rejected, so they always errored; on 2.0.0 they really write. For this gem
|
|
64
|
+
the one to watch is the **`OozeRedirect` force path** — malformed `executeWorkflowCommands` until
|
|
65
|
+
graphql 1.3.15 fixed it, so its behaviour flips from "always errored" to "really writes" with no
|
|
66
|
+
signature change to warn anyone. **Re-verify it on a sandbox org before trusting it in a customer
|
|
67
|
+
run.** Also newly live via the gem: the page-level `executeWorkflowCommands` bus, File-field
|
|
68
|
+
attach writes, `updatePresetView` column edits, viewable-field management and Date-field renewals.
|
|
69
|
+
|
|
70
|
+
★ **CORRECTION (2026-08-13, after publication).** The published 3.3.0 changelog claimed:
|
|
71
|
+
*"Consumers pinned to `~> 3.2` do not pick this up automatically, so adopting graphql 2.0.0 stays a
|
|
72
|
+
deliberate per-repo decision."* **That is wrong** and cannot be edited in the published gem, so it
|
|
73
|
+
is corrected here.
|
|
74
|
+
|
|
75
|
+
`~> 3.2` means `>= 3.2, < 4.0`, so **3.3.0 is in range** — it is not gated. What actually happens
|
|
76
|
+
depends on whether the consumer also declares graphql directly:
|
|
77
|
+
|
|
78
|
+
- A consumer declaring **only** `eco-helpers '~> 3.2'` adopts 3.3.0 on its next `bundle update`,
|
|
79
|
+
and reaches **graphql 2.0.0 transitively**.
|
|
80
|
+
- A consumer declaring **both** `eco-helpers '~> 3.2'` and `ecoportal-api-graphql '~> 1.3'` (which
|
|
81
|
+
is the case for two internal script repos) does **not** silently adopt — it **fails to
|
|
82
|
+
resolve**, because 3.3.0 requires graphql `~> 2.0`.
|
|
83
|
+
|
|
84
|
+
So the "2.0.0 gates adoption for free" argument only ever covered **direct** graphql dependencies.
|
|
85
|
+
Both consumers were pinned deliberately on 2026-08-13 rather than left to discover this.
|
|
86
|
+
|
|
87
|
+
★ **`3.2.20` was tagged but deliberately NEVER PUBLISHED.** rubygems goes 3.2.19 → 3.3.0. The tag
|
|
88
|
+
marks real history (the point where the 3.2.19 hotfix line rejoined master) but the gem was not
|
|
89
|
+
pushed, and **must not be**: it predates the `spec.files` allowlist fix below and would ship
|
|
90
|
+
internal AI-tooling and `docs/` content to rubygems. Its content is not lost — 3.3.0
|
|
91
|
+
descends from it. Anything needing the `Eco::API::Custom::Cli` reunification must use **3.3.0**,
|
|
92
|
+
not a `~> 3.2.0` pin.
|
|
93
|
+
|
|
94
|
+
★ **KNOWN AND APPROVED EXCEPTION — `OozeRedirect` ships unverified** (Oscar, 2026-08-13).
|
|
95
|
+
The force path never worked before graphql 1.3.15 and therefore **starts really writing** on this
|
|
96
|
+
release. It was **not** re-verified on a sandbox org before publishing. Approved deliberately:
|
|
97
|
+
the risk is **usage-gated** — `OozeRedirect` lives under `lib/eco/api/usecases/graphql/compat/` and
|
|
98
|
+
is opt-in per use case; nothing invokes it automatically on load or upgrade, so an unused install
|
|
99
|
+
cannot trigger it. Few paths use it, and shipping was judged more important than blocking on the
|
|
100
|
+
check.
|
|
101
|
+
**Revision note:** Travis to verify on a sandbox org **after** release. Until that verification is
|
|
102
|
+
recorded, treat any `OozeRedirect` run against customer data as unproven — dry-run first.
|
|
103
|
+
|
|
104
|
+
★ **The published gem is now `lib/` only.** `spec.files` excluded just `test|spec|features`, so
|
|
105
|
+
every top-level directory added since shipped by default — internal AI-tooling notes,
|
|
106
|
+
`docs/` (including the rolling worklog) and `.claude/`. `spec.files` is now an allowlist
|
|
107
|
+
(`lib/`, `exe/`, README, LICENSE, CHANGELOG). All `lib/` files are unchanged; nothing outside
|
|
108
|
+
`lib/` was ever loaded at runtime. Same fix as `ecoportal-api-graphql` 2.0.0.
|
|
109
|
+
|
|
110
|
+
## [3.2.20] - 2026-08-11
|
|
111
|
+
|
|
112
|
+
Reunites the released `3.2.19` hotfix line with `master`. `3.2.19` was cut from the `v3.2.18` tag
|
|
113
|
+
and **never merged back**, so two halves were split across branches: `master` carried
|
|
114
|
+
`Eco::API::Custom::Cli` (unreleased since 2026-07-25, and the reason a downstream CLI could not
|
|
115
|
+
load on any published gem) while `3.2.19` carried the ooze KPI counter fix and the graphql floor.
|
|
116
|
+
This release carries both. Dependency floors are the max of each side.
|
|
117
|
+
|
|
118
|
+
### Native GraphQL activity/dashboard readers + contractor manager-settings write
|
|
119
|
+
|
|
120
|
+
Additive, all **NATIVE** (no `Compat::` layer). Depends on the `ecoportal-api-graphql`
|
|
121
|
+
`feat/graphql-activity-logs` branch (polymorphic `activityLogs` / `page.activities` models);
|
|
122
|
+
during development it is wired via a temporary Gemfile `path:` override that a gem version bump
|
|
123
|
+
replaces on release.
|
|
124
|
+
|
|
125
|
+
### Changed
|
|
126
|
+
|
|
127
|
+
- `ecoportal-api` dependency floor raised to `0.10.17`: inherits the `WithRetry` fix making
|
|
128
|
+
`HTTP::TimeoutError` retryable (transient timeouts no longer kill batch loops).
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- **Shared connection-reader substrate** — `Helpers::Base::ConnectionReader`: an `Enumerable`
|
|
133
|
+
mixin that wraps the gem's lazy `QueryConnection#each` (cursor pagination over
|
|
134
|
+
`pageInfo.endCursor`/`hasNextPage`) behind a uniform `each` / `to_a(limit:)` / `page_size`
|
|
135
|
+
surface. All new readers build on it.
|
|
136
|
+
- **System Access Logs reader** — `Helpers::AccessLogs::Base#access_logs` over
|
|
137
|
+
`currentOrganization.activityLogs`, exposing the full filter set (user_ids, resource_ids,
|
|
138
|
+
filter_superusers, activity_type, resource_type, source, date_filter) → yields `Model::ActivityLog`.
|
|
139
|
+
- **Page history reader** — `Helpers::Pages::Activities#page_activities` over `page(id:).activities`
|
|
140
|
+
(page id + `sorters:`/`search_conf:` + lazy pagination) → yields `Model::Activity`.
|
|
141
|
+
- **Dashboards reader** — `Helpers::Dashboards::Base#dashboards` over `dashboardSearch` → yields
|
|
142
|
+
`Model::MinimalDashboard`.
|
|
143
|
+
- **Contractor manager-settings WRITE** — `Helpers::Contractors::Base::ManagerSettings#update_contractor_manager_settings`
|
|
144
|
+
(also exposed via `Samples::Contractors::DSL#update_manager_settings`), driving the gem's
|
|
145
|
+
`Builder::ContractorEntity#update_manager_settings`. Guarded: **dry-run/simulate** short-circuits
|
|
146
|
+
before any mutation (returns the intended input); **-no-email** discipline honoured
|
|
147
|
+
(`options.workflow.no_email` or explicit `no_email:` suppresses the helper's own notification).
|
|
148
|
+
- **Thin live drivers** under `tests/` (read-only for the three readers; dry-run-only for the
|
|
149
|
+
contractor write) plus a shared `tests/env_graphql.rb` harness.
|
|
150
|
+
- Offline specs for all of the above (23 examples).
|
|
151
|
+
- `Eco::API::Common::Loaders::CliConfig` exposed via `Eco::API::Custom::Cli`.
|
|
152
|
+
|
|
153
|
+
### Fixed
|
|
154
|
+
|
|
155
|
+
- **`Template::Base` could never run — `CommandEmitter` was unresolvable.** The class was declared
|
|
156
|
+
with the compact `class Template::Base` form inside `module ...::Pages`, which leaves `Template`
|
|
157
|
+
out of the file's lexical scope, so the bare `CommandEmitter` reference in `#desired_commands`
|
|
158
|
+
raised `NameError` on every call — meaning every build-from-scratch template use case failed
|
|
159
|
+
before emitting a single command. Now nested as `module Template` + `class Base`, matching the
|
|
160
|
+
sibling files (`command_emitter.rb`, `csv_build/builder.rb`) whose bare cross-references already
|
|
161
|
+
worked for exactly that reason. Public constant path is unchanged. Found by the first spec ever
|
|
162
|
+
written against this class.
|
|
163
|
+
|
|
164
|
+
- **Ooze update KPI counters now count GraphQL updates.** `RegisterUpdateCase` tallied
|
|
165
|
+
`updated`/`failed` only when the result `is_a?(Ecoportal::API::Common::Response)`, but the
|
|
166
|
+
GraphQL compat layer returns an `Ecoportal::API::GraphQL::Compat::Response` (duck-types
|
|
167
|
+
`success?`/`status`, not in that class hierarchy) — so GraphQL updates were silently
|
|
168
|
+
uncounted (`Updated 0 (attempted: N)`, `Failed 0`) even when the write applied. Guard is now
|
|
169
|
+
`respond_to?(:success?)`; `false`/`nil` (dry-run / no-op) still skip. Inherited by
|
|
170
|
+
`TargetOozesUpdateCase` → TOOCS / cans-upsert. Also shipped as the maintenance release
|
|
171
|
+
`3.2.19` off `v3.2.18`; regression spec added.
|
|
40
172
|
|
|
41
173
|
## [3.2.19] - 2026-07-16
|
|
42
174
|
|
|
@@ -63,6 +63,11 @@ module Eco::API::Common::Session
|
|
|
63
63
|
config.apis.active_api.version_available?(version)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# @return [Array<Symbol>] required parameters for `version` that are missing.
|
|
67
|
+
def missing_api_params(version:)
|
|
68
|
+
config.apis.active_api.missing_api_params(version)
|
|
69
|
+
end
|
|
70
|
+
|
|
66
71
|
# Shortcut to logger.
|
|
67
72
|
def log(level, &block)
|
|
68
73
|
return unless logger.respond_to?(:level)
|
|
@@ -104,8 +104,10 @@ module Eco
|
|
|
104
104
|
return current if current && !switch_logger
|
|
105
105
|
|
|
106
106
|
unless api_params?(version)
|
|
107
|
+
missing = missing_api_params(version)
|
|
107
108
|
msg = "The api configuration for #{description} "
|
|
108
|
-
msg << "is missing data for the api version '#{self.version(version)}'"
|
|
109
|
+
msg << "is missing data for the api version '#{self.version(version)}': "
|
|
110
|
+
msg << "missing params #{missing.map {|param| ":#{param}"}.join(', ')}"
|
|
109
111
|
raise ArgumentError, msg
|
|
110
112
|
end
|
|
111
113
|
|
|
@@ -196,6 +198,12 @@ module Eco
|
|
|
196
198
|
api_params?(version)
|
|
197
199
|
end
|
|
198
200
|
|
|
201
|
+
# @return [Array<Symbol>] the required parameters for `version` that are
|
|
202
|
+
# currently missing (empty when all of them are present).
|
|
203
|
+
def missing_api_params(version)
|
|
204
|
+
required_api_params(version).reject {|_param, value| value}.keys
|
|
205
|
+
end
|
|
206
|
+
|
|
199
207
|
def version(value = nil)
|
|
200
208
|
self.class.to_version(value || self['version'])
|
|
201
209
|
end
|
|
@@ -224,15 +232,18 @@ module Eco
|
|
|
224
232
|
when :v2
|
|
225
233
|
klass.new(user_key: user_key, org_key: external_key, host: host, logger: logger)
|
|
226
234
|
when :graphql
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
# NOTE: `GRAPHQL_FETCH_SCHEMA` is a documented no-op. `Ecoportal::API::GraphQL#initialize`
|
|
236
|
+
# (email:, pass:, org_id:, host:, api_key:) has never accepted a `no_schema:` keyword —
|
|
237
|
+
# it always builds its internal client with `no_schema: true` and has no public way to
|
|
238
|
+
# turn schema introspection on. Passing the unknown keyword raised ArgumentError, so the
|
|
239
|
+
# flag was worse than a no-op: setting `GRAPHQL_FETCH_SCHEMA=false` crashed api() instead
|
|
240
|
+
# of skipping a fetch that the gem's public API never performs regardless.
|
|
241
|
+
if ENV['GRAPHQL_FETCH_SCHEMA'] == "false"
|
|
242
|
+
warn "[eco-helpers] GRAPHQL_FETCH_SCHEMA is a no-op: " \
|
|
243
|
+
"Ecoportal::API::GraphQL never fetches the schema (no_schema: true, hardcoded)."
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
klass.new(host: host, org_id: org_id, email: email, pass: pass)
|
|
236
247
|
end.tap do |api|
|
|
237
248
|
next unless api
|
|
238
249
|
next if log_connection? # prevent over-logging
|
|
@@ -248,15 +259,23 @@ module Eco
|
|
|
248
259
|
|
|
249
260
|
# Checks if the necessary parameters for a specific `API` version are available.
|
|
250
261
|
def api_params?(version)
|
|
262
|
+
missing_api_params(version).empty?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Required parameters (as `name => value` pairs) for a specific `API` version.
|
|
266
|
+
# @note `to_version` always normalises to one of the known versions below.
|
|
267
|
+
def required_api_params(version)
|
|
251
268
|
case self.class.to_version(version)
|
|
252
269
|
when :v0
|
|
253
|
-
internal_key
|
|
270
|
+
{internal_key: internal_key, host: host}
|
|
254
271
|
when :v1
|
|
255
|
-
external_key
|
|
272
|
+
{external_key: external_key, host: host}
|
|
256
273
|
when :v2
|
|
257
|
-
external_key
|
|
274
|
+
{external_key: external_key, user_key: user_key, host: host}
|
|
258
275
|
when :graphql
|
|
259
|
-
email
|
|
276
|
+
{email: email, pass: pass, org_id: org_id, host: host}
|
|
277
|
+
else
|
|
278
|
+
{}
|
|
260
279
|
end
|
|
261
280
|
end
|
|
262
281
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::AccessLogs
|
|
2
|
+
module Base
|
|
3
|
+
# NATIVE reader over `currentOrganization.activityLogs` (the ActivityLogUnion behind the web
|
|
4
|
+
# "System Access Logs" page). Lazily paginates and yields `Model::ActivityLog` records.
|
|
5
|
+
#
|
|
6
|
+
# Exposes the full server filter set. Construct via `Helpers::AccessLogs::Base#access_logs`:
|
|
7
|
+
# access_logs(source: 'web', resource_type: 'view_dashboard').each { |log| ... }
|
|
8
|
+
# access_logs(date_filter: { from: from_iso, to: to_iso }).to_a(limit: 500)
|
|
9
|
+
class Reader
|
|
10
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base::ConnectionReader
|
|
11
|
+
|
|
12
|
+
# @param graphql [Ecoportal::API::GraphQL] the active gem client.
|
|
13
|
+
# @param user_ids [Array<String>, nil]
|
|
14
|
+
# @param resource_ids [Array<String>, nil]
|
|
15
|
+
# @param filter_superusers [Boolean, nil]
|
|
16
|
+
# @param activity_type [String, nil] free-text activity type filter.
|
|
17
|
+
# @param resource_type [String, nil] ActivityLogTypesEnum value (e.g. 'view_dashboard').
|
|
18
|
+
# @param source [String, nil] SourceEnum value (e.g. 'web', 'api').
|
|
19
|
+
# @param date_filter [Hash, nil] `{ from:, to: }` (ISO8601), maps to DateRange.
|
|
20
|
+
def initialize(
|
|
21
|
+
graphql,
|
|
22
|
+
user_ids: nil,
|
|
23
|
+
resource_ids: nil,
|
|
24
|
+
filter_superusers: nil,
|
|
25
|
+
activity_type: nil,
|
|
26
|
+
resource_type: nil,
|
|
27
|
+
source: nil,
|
|
28
|
+
date_filter: nil
|
|
29
|
+
)
|
|
30
|
+
@graphql = graphql
|
|
31
|
+
@user_ids = user_ids
|
|
32
|
+
@resource_ids = resource_ids
|
|
33
|
+
@filter_superusers = filter_superusers
|
|
34
|
+
@activity_type = activity_type
|
|
35
|
+
@resource_type = resource_type
|
|
36
|
+
@source = source
|
|
37
|
+
@date_filter = date_filter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def connection_query
|
|
43
|
+
@graphql.currentOrganization.activityLogs
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reader_params
|
|
47
|
+
{
|
|
48
|
+
userIds: @user_ids,
|
|
49
|
+
resourceIds: @resource_ids,
|
|
50
|
+
filterSuperusers: @filter_superusers,
|
|
51
|
+
activityType: @activity_type,
|
|
52
|
+
resourceType: @resource_type,
|
|
53
|
+
source: @source,
|
|
54
|
+
dateFilter: @date_filter
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require_relative 'base/reader'
|
|
2
|
+
|
|
3
|
+
module Eco::API::UseCases::GraphQL::Helpers::AccessLogs
|
|
4
|
+
# System Access Logs helper mixin — NATIVE reader over `activityLogs`.
|
|
5
|
+
module Base
|
|
6
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Build a lazy reader over the org's system access logs (ActivityLogUnion).
|
|
11
|
+
# @see Reader#initialize for the supported filters.
|
|
12
|
+
# @return [Reader]
|
|
13
|
+
def access_logs(**filters)
|
|
14
|
+
Reader.new(graphql, **filters)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Base
|
|
2
|
+
# Shared substrate for NATIVE GraphQL connection readers.
|
|
3
|
+
#
|
|
4
|
+
# The gem's `Logic::QueryConnection` is already `Enumerable` and paginates lazily over
|
|
5
|
+
# `first`/`after` using `pageInfo.endCursor`/`hasNextPage` (see its `#each`). This mixin wraps
|
|
6
|
+
# that machinery behind a small, uniform reader surface so every native reader (access logs,
|
|
7
|
+
# page activities, dashboards, ...) exposes the same `each` / `to_a` / `first(n)` behaviour and
|
|
8
|
+
# a consistent `page_size` without re-implementing cursor pagination.
|
|
9
|
+
#
|
|
10
|
+
# A concrete reader supplies a `#query` (a gem query object, e.g.
|
|
11
|
+
# `graphql.currentOrganization.activityLogs`) and the per-call filter/argument hash via
|
|
12
|
+
# `#reader_params`. Everything else (lazy iteration, batching, materialising) is inherited.
|
|
13
|
+
module ConnectionReader
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
# Default number of nodes fetched per network round-trip.
|
|
17
|
+
DEFAULT_PAGE_SIZE = 100
|
|
18
|
+
|
|
19
|
+
# Iterate over every node across all pages (lazy — one network page at a time).
|
|
20
|
+
# @yieldparam node [Object] a gem read model (e.g. Model::ActivityLog / Model::Activity).
|
|
21
|
+
# @return [Enumerator] when no block is given.
|
|
22
|
+
def each(&block)
|
|
23
|
+
return to_enum(:each) unless block
|
|
24
|
+
|
|
25
|
+
connection_query.each(**each_params, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Materialise a bounded number of nodes without draining the whole connection.
|
|
29
|
+
# @param limit [Integer, nil] maximum nodes to return; nil = all pages.
|
|
30
|
+
# @return [Array]
|
|
31
|
+
def to_a(limit: nil)
|
|
32
|
+
return super() if limit.nil?
|
|
33
|
+
|
|
34
|
+
take(limit)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Integer] nodes fetched per page.
|
|
38
|
+
def page_size
|
|
39
|
+
@page_size ||= DEFAULT_PAGE_SIZE
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param size [Integer]
|
|
43
|
+
def page_size=(size)
|
|
44
|
+
@page_size = Integer(size)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# The gem query object to iterate (must respond to `#each(**kargs)` — i.e. a
|
|
50
|
+
# `Logic::QueryConnection`). Concrete readers MUST override.
|
|
51
|
+
# @return [Ecoportal::API::GraphQL::Logic::QueryConnection]
|
|
52
|
+
def connection_query
|
|
53
|
+
raise NotImplementedError, "#{self.class} must implement #connection_query"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Per-call arguments (filters + page id + ...) merged into every page request.
|
|
57
|
+
# Concrete readers override to add their own filters. `first` defaults to `page_size`.
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def reader_params
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Final kargs handed to the gem's `QueryConnection#each` — reader_params with a
|
|
64
|
+
# default `first`, and nils stripped so unset filters are not sent.
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
def each_params
|
|
67
|
+
{ first: page_size }.merge(reader_params).compact
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -6,8 +6,12 @@ module Eco::API::UseCases::GraphQL::Helpers::Base
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def graphql
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
unless session.api?(version: :graphql)
|
|
10
|
+
missing = session.missing_api_params(version: :graphql)
|
|
11
|
+
msg = "The credentials or basic graphql config are missing for the active environment"
|
|
12
|
+
msg << " (missing params: #{missing.map {|param| ":#{param}"}.join(', ')})" if missing.any?
|
|
13
|
+
raise msg
|
|
14
|
+
end
|
|
11
15
|
|
|
12
16
|
@graphql ||= session.api(version: :graphql)
|
|
13
17
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Contractors
|
|
2
|
+
module Base
|
|
3
|
+
# NATIVE contractor **write** helper — `updateContractorManagerSettings`.
|
|
4
|
+
#
|
|
5
|
+
# Drives the gem's `Builder::ContractorEntity#update_manager_settings` to set the
|
|
6
|
+
# organisation's contractor-manager auto-approval settings and whether contractor admins
|
|
7
|
+
# may add other admins.
|
|
8
|
+
#
|
|
9
|
+
# Guards (aligned with the repo's write discipline):
|
|
10
|
+
# - **dry-run / simulate:** when `simulate?` (or `dry_run:` is passed), NO mutation is sent;
|
|
11
|
+
# the intended input is logged and returned so callers can inspect it.
|
|
12
|
+
# - **-no-email:** honoured — when email is suppressed (`options.dig(:workflow, :no_email)`
|
|
13
|
+
# or an explicit `no_email:`), the helper does not emit its own success notification. The
|
|
14
|
+
# server-side mutation itself sends no member email for this settings change.
|
|
15
|
+
module ManagerSettings
|
|
16
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Update the org's contractor manager settings.
|
|
21
|
+
# @param cm_auto_approvals_settings [Hash, nil] AutoApprovalsInput
|
|
22
|
+
# ({ enabled:, approvalConditions: }). Required by the schema for a real update.
|
|
23
|
+
# @param allow_contractor_admins_to_add_admins [Boolean, nil]
|
|
24
|
+
# @param dry_run [Boolean, nil] force simulate for this call (in addition to `simulate?`).
|
|
25
|
+
# @param no_email [Boolean, nil] suppress this helper's own notification for this call.
|
|
26
|
+
# @return [Hash] when simulated: the intended input hash. Otherwise: the mutation payload.
|
|
27
|
+
def update_contractor_manager_settings(
|
|
28
|
+
cm_auto_approvals_settings: nil,
|
|
29
|
+
allow_contractor_admins_to_add_admins: nil,
|
|
30
|
+
dry_run: nil,
|
|
31
|
+
no_email: nil
|
|
32
|
+
)
|
|
33
|
+
input = {}
|
|
34
|
+
input[:cmAutoApprovalsSettings] = cm_auto_approvals_settings unless cm_auto_approvals_settings.nil?
|
|
35
|
+
unless allow_contractor_admins_to_add_admins.nil?
|
|
36
|
+
input[:allowContractorAdminsToAddAdmins] =
|
|
37
|
+
allow_contractor_admins_to_add_admins
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if dry_run || simulate?
|
|
41
|
+
log(:info) { "[dry-run] updateContractorManagerSettings input: #{input}" }
|
|
42
|
+
return input
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
graphql.contractorEntity.update_manager_settings(input: input).tap do
|
|
46
|
+
notify_manager_settings_updated(input) unless suppress_email?(no_email)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Boolean] whether this helper's own notification email is suppressed.
|
|
51
|
+
def suppress_email?(no_email)
|
|
52
|
+
return true if no_email
|
|
53
|
+
return true if respond_to?(:options, true) && options.dig(:workflow, :no_email)
|
|
54
|
+
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Hook for a success notification (kept minimal; overridable). Logs by default.
|
|
59
|
+
def notify_manager_settings_updated(input)
|
|
60
|
+
log(:info) { "Updated contractor manager settings: #{input}" }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Dashboards
|
|
2
|
+
module Base
|
|
3
|
+
# NATIVE reader over `currentOrganization.dashboardSearch` (MinimalDashboardConnection).
|
|
4
|
+
# Lazily paginates and yields `Model::MinimalDashboard` records.
|
|
5
|
+
#
|
|
6
|
+
# Use via `Helpers::Dashboards::Base#dashboards`:
|
|
7
|
+
# dashboards(query: 'safety').each { |d| ... }
|
|
8
|
+
# dashboards.to_a
|
|
9
|
+
class Reader
|
|
10
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base::ConnectionReader
|
|
11
|
+
|
|
12
|
+
# @param graphql [Ecoportal::API::GraphQL] the active gem client.
|
|
13
|
+
# @param search_conf [Hash, nil] a `Search` input hash ({ query:, filters:, sorters: }).
|
|
14
|
+
def initialize(graphql, search_conf: nil)
|
|
15
|
+
@graphql = graphql
|
|
16
|
+
@search_conf = search_conf
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def connection_query
|
|
22
|
+
@graphql.currentOrganization.dashboardSearch
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reader_params
|
|
26
|
+
{ searchConf: @search_conf }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative 'base/reader'
|
|
2
|
+
|
|
3
|
+
module Eco::API::UseCases::GraphQL::Helpers::Dashboards
|
|
4
|
+
# Dashboards helper mixin — NATIVE reader over `dashboardSearch`.
|
|
5
|
+
module Base
|
|
6
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Build a lazy reader over the org's dashboards (MinimalDashboardConnection).
|
|
11
|
+
# @param query [String, nil] free-text search shorthand (→ search_conf { query: }).
|
|
12
|
+
# @param search_conf [Hash, nil] full Search input (takes precedence over `query`).
|
|
13
|
+
# @return [Reader]
|
|
14
|
+
def dashboards(query: nil, search_conf: nil)
|
|
15
|
+
conf = search_conf
|
|
16
|
+
conf ||= { query: query } if query
|
|
17
|
+
Reader.new(graphql, search_conf: conf)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Helpers::Pages
|
|
2
|
+
# NATIVE reader over `page(id:).activities` (the ActivityInterface behind a page's history /
|
|
3
|
+
# changelog feed). Lazily paginates and yields `Model::Activity` records.
|
|
4
|
+
#
|
|
5
|
+
# Use via `Helpers::Pages::Activities#page_activities`:
|
|
6
|
+
# page_activities(page_id).each { |a| ... }
|
|
7
|
+
# page_activities(page_id, sorters: [{ key: 'updated_at', direction: 'desc' }]).to_a(limit: 50)
|
|
8
|
+
module Activities
|
|
9
|
+
# Lazy activity-feed reader for a single page.
|
|
10
|
+
class Reader
|
|
11
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base::ConnectionReader
|
|
12
|
+
|
|
13
|
+
# @param graphql [Ecoportal::API::GraphQL] the active gem client.
|
|
14
|
+
# @param page_id [String] the page id whose activities to read.
|
|
15
|
+
# @param search_conf [Hash, nil] a `Search` input hash (typically `{ sorters: [...] }`).
|
|
16
|
+
# Defaults to the web feature's ordering: newest updates first.
|
|
17
|
+
def initialize(graphql, page_id, search_conf: nil)
|
|
18
|
+
@graphql = graphql
|
|
19
|
+
@page_id = page_id
|
|
20
|
+
@search_conf = search_conf || DEFAULT_SEARCH_CONF
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
DEFAULT_SEARCH_CONF = { sorters: [{ direction: 'desc', key: 'updated_at' }] }.freeze
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def connection_query
|
|
28
|
+
@graphql.currentOrganization.pageActivities
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reader_params
|
|
32
|
+
{ id: @page_id, searchConf: @search_conf }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
include Eco::API::UseCases::GraphQL::Helpers::Base
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Build a lazy reader over a page's activity/history feed.
|
|
41
|
+
# @param page_id [String]
|
|
42
|
+
# @param sorters [Array<Hash>, nil] shorthand for `search_conf: { sorters: ... }`.
|
|
43
|
+
# @param search_conf [Hash, nil] full Search input (takes precedence over `sorters`).
|
|
44
|
+
# @return [Reader]
|
|
45
|
+
def page_activities(page_id, sorters: nil, search_conf: nil)
|
|
46
|
+
conf = search_conf
|
|
47
|
+
conf ||= { sorters: sorters } if sorters
|
|
48
|
+
Reader.new(graphql, page_id, search_conf: conf)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
module Eco::API::UseCases::GraphQL::Samples
|
|
2
2
|
module Contractors::DSL
|
|
3
3
|
include Eco::API::UseCases::GraphQL::Helpers::Contractors::Base
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
# Sample command: update the org's contractor manager settings (auto-approvals +
|
|
8
|
+
# whether contractor admins may add other admins). Delegates to the guarded helper
|
|
9
|
+
# `#update_contractor_manager_settings` (dry-run/simulate + -no-email honoured).
|
|
10
|
+
#
|
|
11
|
+
# @param auto_approvals [Hash, nil] AutoApprovalsInput ({ enabled:, approvalConditions: }).
|
|
12
|
+
# @param allow_admins_add_admins [Boolean, nil]
|
|
13
|
+
# @return [Hash] the intended input when simulated, else the mutation payload.
|
|
14
|
+
def update_manager_settings(auto_approvals: nil, allow_admins_add_admins: nil)
|
|
15
|
+
update_contractor_manager_settings(
|
|
16
|
+
cm_auto_approvals_settings: auto_approvals,
|
|
17
|
+
allow_contractor_admins_to_add_admins: allow_admins_add_admins
|
|
18
|
+
)
|
|
19
|
+
end
|
|
4
20
|
end
|
|
5
21
|
end
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# NOTE: nested as `module Template` + `class Base` rather than the compact `class Template::Base`
|
|
3
|
+
# ON PURPOSE. The compact form leaves `Template` out of this file's lexical scope, so the bare
|
|
4
|
+
# `CommandEmitter` reference in #desired_commands resolved against Base and Pages only and raised
|
|
5
|
+
# NameError on every call -- the class could never run. Sibling files (command_emitter.rb,
|
|
6
|
+
# csv_build/builder.rb) all open the Template module properly, which is why their bare references
|
|
7
|
+
# to each other work. Keep this nesting.
|
|
8
|
+
#
|
|
2
9
|
# Build-from-scratch template (workflow) construction use case.
|
|
3
10
|
#
|
|
4
11
|
# A subclass declares the desired template structure in #declare(emitter) using the
|
|
@@ -17,54 +24,56 @@ module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
|
17
24
|
# end
|
|
18
25
|
# end
|
|
19
26
|
# end
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
module Template
|
|
28
|
+
class Base < Eco::API::UseCases::GraphQL::Base
|
|
29
|
+
name 'graphql-template-base'
|
|
30
|
+
type :other
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
require_relative 'command_emitter'
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
def process
|
|
35
|
+
commands = desired_commands
|
|
36
|
+
if simulate?
|
|
37
|
+
log(:info) { preview_message(commands) }
|
|
38
|
+
return nil
|
|
39
|
+
end
|
|
40
|
+
apply(commands)
|
|
31
41
|
end
|
|
32
|
-
apply(commands)
|
|
33
|
-
end
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
# == Subclass override point ================================================
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
# Declare the desired template structure onto the given CommandEmitter.
|
|
46
|
+
def declare(_emitter)
|
|
47
|
+
raise NotImplementedError, "Implement #declare(emitter) in #{self.class}"
|
|
48
|
+
end
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
# The ordered command batch this template would apply. Pure — no client needed.
|
|
51
|
+
def desired_commands
|
|
52
|
+
emitter = CommandEmitter.new
|
|
53
|
+
declare(emitter)
|
|
54
|
+
emitter.commands
|
|
55
|
+
end
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
protected
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
# Persist a fresh template from the command batch. Override to #update an existing one.
|
|
60
|
+
def apply(commands)
|
|
61
|
+
graphql.template.create(commands: commands)
|
|
62
|
+
end
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
# Update an existing template model with the command batch.
|
|
65
|
+
def apply_update(model, commands)
|
|
66
|
+
graphql.template.update(model, commands: commands)
|
|
67
|
+
end
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
private
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
def preview_message(commands)
|
|
72
|
+
[
|
|
73
|
+
"Simulate — would apply #{commands.size} workflow command(s):",
|
|
74
|
+
*commands.map { |c| " * #{c.keys.first}: #{c.values.first.inspect}" }
|
|
75
|
+
].join("\n")
|
|
76
|
+
end
|
|
68
77
|
end
|
|
69
78
|
end
|
|
70
79
|
end
|
|
@@ -115,8 +115,15 @@ module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
# Select-field option. dataFieldId threads the (placeholder) field id.
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
#
|
|
119
|
+
# `value:` is a REQUIRED keyword -- the backend's AddSelectFieldOptionInput declares
|
|
120
|
+
# `argument :value, String, required: true`, and it is the option's unique key (cast to a
|
|
121
|
+
# number for numeric select fields). There is no default and it is never derived here from
|
|
122
|
+
# label/weight/position: a plausible-looking derived value would silently store WRONG data on
|
|
123
|
+
# exactly the scored fields this feature is used for. Callers must supply an explicit value.
|
|
124
|
+
def option(label:, value:, weight: nil)
|
|
125
|
+
@emitter.emit(:addSelectFieldOption, data_field_id: @field_id, label: label, value: value,
|
|
126
|
+
weight: weight)
|
|
120
127
|
@field_id
|
|
121
128
|
end
|
|
122
129
|
end
|
|
@@ -111,7 +111,7 @@ module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
|
111
111
|
description: field.description
|
|
112
112
|
) do |field_builder|
|
|
113
113
|
Array(field.options).each do |opt|
|
|
114
|
-
field_builder.option(label: opt[:label], weight: opt[:weight])
|
|
114
|
+
field_builder.option(label: opt[:label], value: opt[:value], weight: opt[:weight])
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
end
|
|
@@ -15,8 +15,17 @@ module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
|
15
15
|
#
|
|
16
16
|
# * `field_type` — a template field type accepted by addField (e.g. plain_text, select,
|
|
17
17
|
# date, number, gauge, rich_text, people, ...). Passed through verbatim.
|
|
18
|
-
# * `field_options` — for select-type fields: pipe-separated `label:weight`
|
|
19
|
-
# e.g. "High:10|Medium:5|Low:0".
|
|
18
|
+
# * `field_options` — for select-type fields: pipe-separated `label:value:weight`
|
|
19
|
+
# triples, e.g. "High:10:10|Medium:5:5|Low:0:0". `value` is the
|
|
20
|
+
# backend's unique, REQUIRED select-option key (cast to a number for
|
|
21
|
+
# numeric select fields) -- NEVER derived from label/weight/position
|
|
22
|
+
# here, because a derived value can collide (duplicates are rejected
|
|
23
|
+
# server-side) or silently store the WRONG number on a scored field.
|
|
24
|
+
# Weight optional via a trailing empty part, e.g. "High:H:|Low:L:". A
|
|
25
|
+
# cell resolving to exactly two parts (the OLD `label:weight`
|
|
26
|
+
# shorthand -- now ambiguous with a value-only `label:value`) or a
|
|
27
|
+
# bare label RAISES ArgumentError naming the field, option label, and
|
|
28
|
+
# offending cell -- see `#options` below.
|
|
20
29
|
# * `field_description` — carries the SECTION/FIELD IDENTITY convention (see below).
|
|
21
30
|
#
|
|
22
31
|
# SECTION / FIELD IDENTITY CONVENTION (per the CSV-pipeline project notes): stable identity for a
|
|
@@ -68,20 +77,50 @@ module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
|
68
77
|
field_required: truthy?(row, :field_required),
|
|
69
78
|
field_column: integer(row, :field_column) || 0,
|
|
70
79
|
field_description: value(row, :field_description),
|
|
71
|
-
field_options: options(value(row, :field_options))
|
|
80
|
+
field_options: options(value(row, :field_options), field_label: value(row, :field_label))
|
|
72
81
|
)
|
|
73
82
|
end
|
|
74
83
|
|
|
75
|
-
# Parse the options cell into [ { label:, weight: }, ... ]. Empty → [].
|
|
76
|
-
|
|
84
|
+
# Parse the options cell into [ { label:, value:, weight: }, ... ]. Empty → [].
|
|
85
|
+
#
|
|
86
|
+
# `field_label:` is ONLY used for the ArgumentError message below -- it identifies which field's
|
|
87
|
+
# options cell is malformed, since a builder raising mid-batch on a 500-row CSV is otherwise
|
|
88
|
+
# nearly impossible to trace back to the offending row.
|
|
89
|
+
def options(cell, field_label: nil)
|
|
77
90
|
return [] if cell.nil? || cell.to_s.strip.empty?
|
|
78
91
|
|
|
79
92
|
cell.to_s.split(OPTIONS_DELIMITER).map do |token|
|
|
80
|
-
|
|
81
|
-
{ label: label, weight: (weight && !weight.empty? ? Integer(weight, exception: false) : nil) }
|
|
93
|
+
option_from_token(token, field_label: field_label, cell: cell)
|
|
82
94
|
end
|
|
83
95
|
end
|
|
84
96
|
|
|
97
|
+
# A token must resolve to exactly `label:value` or `label:value:weight` (weight optional, via a
|
|
98
|
+
# present-but-empty third part). Anything else -- a bare label, or exactly two parts -- means no
|
|
99
|
+
# explicit `value` was given. Because a `label:X` two-part cell is indistinguishable from the OLD
|
|
100
|
+
# `label:weight` shorthand, it is REJECTED rather than guessed at: see the module doc for why a
|
|
101
|
+
# derived value is worse than a loud failure.
|
|
102
|
+
def option_from_token(token, field_label:, cell:)
|
|
103
|
+
parts = token.split(OPTION_WEIGHT_SEPARATOR, 3).map(&:strip)
|
|
104
|
+
label = parts[0].to_s.empty? ? nil : parts[0]
|
|
105
|
+
value = parts[1]
|
|
106
|
+
|
|
107
|
+
if parts.size < 3 || value.nil? || value.empty?
|
|
108
|
+
raise ArgumentError, missing_option_value_message(field_label, label || token, cell)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
weight_raw = parts[2]
|
|
112
|
+
{ label: label, value: value,
|
|
113
|
+
weight: (weight_raw && !weight_raw.empty? ? Integer(weight_raw, exception: false) : nil) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def missing_option_value_message(field_label, option_label, cell)
|
|
117
|
+
"select option is missing an explicit `value` (field: #{field_label.inspect}, " \
|
|
118
|
+
"option: #{option_label.inspect}, cell: #{cell.to_s.inspect}). The old `label:weight` " \
|
|
119
|
+
'two-part shorthand is no longer accepted -- addSelectFieldOption requires a unique, ' \
|
|
120
|
+
'non-derived `value` per option. Add one explicitly, e.g. ' \
|
|
121
|
+
"\"#{option_label}:VALUE\" or \"#{option_label}:VALUE:WEIGHT\"."
|
|
122
|
+
end
|
|
123
|
+
|
|
85
124
|
def value(row, logical)
|
|
86
125
|
header = COLUMNS.fetch(logical)
|
|
87
126
|
raw = row[header]
|
|
@@ -26,12 +26,15 @@ module Eco
|
|
|
26
26
|
puts msg
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Binary mode: text mode on Windows silently translates "\r\n" to "\n",
|
|
30
|
+
# which corrupts files whose row terminators must be distinguished from
|
|
31
|
+
# bare "\n" inside quoted CSV fields (and diverges from Linux behaviour).
|
|
29
32
|
bom_enc = encoding && encoding.split('|')[0] == 'bom'
|
|
30
33
|
if bom?(file) || bom_enc
|
|
31
|
-
content = remove_bom(File.read(file, encoding: 'utf-8'))
|
|
34
|
+
content = remove_bom(File.read(file, mode: 'rb', encoding: 'utf-8'))
|
|
32
35
|
encoding = 'utf-8'
|
|
33
36
|
else
|
|
34
|
-
content = File.read(file, encoding: encoding)
|
|
37
|
+
content = File.read(file, mode: 'rb', encoding: encoding)
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
return unless content
|
data/lib/eco/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eco-helpers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oscar Segura
|
|
@@ -242,7 +242,7 @@ dependencies:
|
|
|
242
242
|
version: '0.10'
|
|
243
243
|
- - ">="
|
|
244
244
|
- !ruby/object:Gem::Version
|
|
245
|
-
version: 0.10.
|
|
245
|
+
version: 0.10.17
|
|
246
246
|
type: :runtime
|
|
247
247
|
prerelease: false
|
|
248
248
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -252,27 +252,27 @@ dependencies:
|
|
|
252
252
|
version: '0.10'
|
|
253
253
|
- - ">="
|
|
254
254
|
- !ruby/object:Gem::Version
|
|
255
|
-
version: 0.10.
|
|
255
|
+
version: 0.10.17
|
|
256
256
|
- !ruby/object:Gem::Dependency
|
|
257
257
|
name: ecoportal-api-graphql
|
|
258
258
|
requirement: !ruby/object:Gem::Requirement
|
|
259
259
|
requirements:
|
|
260
260
|
- - "~>"
|
|
261
261
|
- !ruby/object:Gem::Version
|
|
262
|
-
version: '
|
|
262
|
+
version: '2.0'
|
|
263
263
|
- - ">="
|
|
264
264
|
- !ruby/object:Gem::Version
|
|
265
|
-
version:
|
|
265
|
+
version: 2.0.0
|
|
266
266
|
type: :runtime
|
|
267
267
|
prerelease: false
|
|
268
268
|
version_requirements: !ruby/object:Gem::Requirement
|
|
269
269
|
requirements:
|
|
270
270
|
- - "~>"
|
|
271
271
|
- !ruby/object:Gem::Version
|
|
272
|
-
version: '
|
|
272
|
+
version: '2.0'
|
|
273
273
|
- - ">="
|
|
274
274
|
- !ruby/object:Gem::Version
|
|
275
|
-
version:
|
|
275
|
+
version: 2.0.0
|
|
276
276
|
- !ruby/object:Gem::Dependency
|
|
277
277
|
name: ecoportal-api-v2
|
|
278
278
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -619,6 +619,7 @@ files:
|
|
|
619
619
|
- lib/eco/api/common/version_patches/ruby3.rb
|
|
620
620
|
- lib/eco/api/common/version_patches/ruby3/object.rb
|
|
621
621
|
- lib/eco/api/custom.rb
|
|
622
|
+
- lib/eco/api/custom/cli.rb
|
|
622
623
|
- lib/eco/api/custom/config.rb
|
|
623
624
|
- lib/eco/api/custom/error_handler.rb
|
|
624
625
|
- lib/eco/api/custom/mailer.rb
|
|
@@ -826,13 +827,21 @@ files:
|
|
|
826
827
|
- lib/eco/api/usecases/graphql/compat/parity/harness.rb
|
|
827
828
|
- lib/eco/api/usecases/graphql/compat/parity/run_result.rb
|
|
828
829
|
- lib/eco/api/usecases/graphql/helpers.rb
|
|
830
|
+
- lib/eco/api/usecases/graphql/helpers/access_logs.rb
|
|
831
|
+
- lib/eco/api/usecases/graphql/helpers/access_logs/base.rb
|
|
832
|
+
- lib/eco/api/usecases/graphql/helpers/access_logs/base/reader.rb
|
|
829
833
|
- lib/eco/api/usecases/graphql/helpers/base.rb
|
|
830
834
|
- lib/eco/api/usecases/graphql/helpers/base/case_env.rb
|
|
835
|
+
- lib/eco/api/usecases/graphql/helpers/base/connection_reader.rb
|
|
831
836
|
- lib/eco/api/usecases/graphql/helpers/base/error_handling.rb
|
|
832
837
|
- lib/eco/api/usecases/graphql/helpers/base/graphql_env.rb
|
|
833
838
|
- lib/eco/api/usecases/graphql/helpers/contractors.rb
|
|
834
839
|
- lib/eco/api/usecases/graphql/helpers/contractors/base.rb
|
|
835
840
|
- lib/eco/api/usecases/graphql/helpers/contractors/base/load.rb
|
|
841
|
+
- lib/eco/api/usecases/graphql/helpers/contractors/base/manager_settings.rb
|
|
842
|
+
- lib/eco/api/usecases/graphql/helpers/dashboards.rb
|
|
843
|
+
- lib/eco/api/usecases/graphql/helpers/dashboards/base.rb
|
|
844
|
+
- lib/eco/api/usecases/graphql/helpers/dashboards/base/reader.rb
|
|
836
845
|
- lib/eco/api/usecases/graphql/helpers/location.rb
|
|
837
846
|
- lib/eco/api/usecases/graphql/helpers/location/base.rb
|
|
838
847
|
- lib/eco/api/usecases/graphql/helpers/location/base/classifications_parser.rb
|
|
@@ -858,6 +867,7 @@ files:
|
|
|
858
867
|
- lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_map.rb
|
|
859
868
|
- lib/eco/api/usecases/graphql/helpers/location/tags_remap/tags_set.rb
|
|
860
869
|
- lib/eco/api/usecases/graphql/helpers/pages.rb
|
|
870
|
+
- lib/eco/api/usecases/graphql/helpers/pages/activities.rb
|
|
861
871
|
- lib/eco/api/usecases/graphql/helpers/pages/copying.rb
|
|
862
872
|
- lib/eco/api/usecases/graphql/helpers/pages/creatable.rb
|
|
863
873
|
- lib/eco/api/usecases/graphql/helpers/pages/filters.rb
|