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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -21
  3. data/lib/eco/api/common/session/base_session.rb +4 -0
  4. data/lib/eco/api/common/session/environment.rb +5 -0
  5. data/lib/eco/api/custom/cli.rb +3 -0
  6. data/lib/eco/api/session/config/api.rb +33 -14
  7. data/lib/eco/api/usecases/graphql/helpers/access_logs/base/reader.rb +59 -0
  8. data/lib/eco/api/usecases/graphql/helpers/access_logs/base.rb +17 -0
  9. data/lib/eco/api/usecases/graphql/helpers/access_logs.rb +7 -0
  10. data/lib/eco/api/usecases/graphql/helpers/base/connection_reader.rb +70 -0
  11. data/lib/eco/api/usecases/graphql/helpers/base/graphql_env.rb +6 -2
  12. data/lib/eco/api/usecases/graphql/helpers/base.rb +1 -0
  13. data/lib/eco/api/usecases/graphql/helpers/contractors/base/manager_settings.rb +64 -0
  14. data/lib/eco/api/usecases/graphql/helpers/contractors/base.rb +2 -0
  15. data/lib/eco/api/usecases/graphql/helpers/dashboards/base/reader.rb +30 -0
  16. data/lib/eco/api/usecases/graphql/helpers/dashboards/base.rb +20 -0
  17. data/lib/eco/api/usecases/graphql/helpers/dashboards.rb +7 -0
  18. data/lib/eco/api/usecases/graphql/helpers/pages/activities.rb +51 -0
  19. data/lib/eco/api/usecases/graphql/helpers/pages.rb +1 -0
  20. data/lib/eco/api/usecases/graphql/helpers.rb +2 -0
  21. data/lib/eco/api/usecases/graphql/samples/contractors/dsl.rb +16 -0
  22. data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +46 -37
  23. data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +9 -2
  24. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +1 -1
  25. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +46 -7
  26. data/lib/eco/data/files/encoding.rb +5 -2
  27. data/lib/eco/version.rb +1 -1
  28. metadata +17 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2803cb1dc211ef8682ebcd33e4007f4e748b5eb42f56ecbaa931b5258064ead8
4
- data.tar.gz: 7c0f5f66654761ce6ee5cb37ca4eb354054161c0fc3914c37da27f7f6fce4273
3
+ metadata.gz: 3c5e64bb2f9a25359774eb01482180a7efdbade0e2b6e4d02a56a4c9dbb1d91f
4
+ data.tar.gz: ea2fd73dc9709ca1710e9aff527f0ae856a55fe0638a7c3862b71bfd9ce5270c
5
5
  SHA512:
6
- metadata.gz: 10235574cf1a38826f4de995d715747b65fd657e0ef4e183b04644f8e26bebb2471c4d118fbbc00eac72e174d74c72d4685c9fb49a9a3667d8320c005ef7983a
7
- data.tar.gz: e9f15c9f0e9c5243c835402fe64efd1384021773d3e0c0d65fbbfe17bd10128e944932e9a8ac21501cf31025040c9fcc0b2b26b8eb70844fd631f899008c822c
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
- ## [3.2.23] - 2026-09-30
5
+ ## [Unreleased]
6
6
 
7
- Security republish of 3.2.22. **Backwards-compatible; no code change**: same dependencies, same
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. It also carries the full-path packaging allowlist from 3.3.2, so the five internal
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.22] - 2026-08-14
20
-
21
- ### Fixed
22
-
23
- - Republish of `3.2.21`: its `lib/eco/version.rb` shipped with doubled carriage returns
24
- (`
25
- ` line endings), causing a `warning: encountered in middle of line` on every
26
- load. Functionally identical otherwise; `3.2.21` will be yanked.
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.2.21] - 2026-08-14
32
+ ## [3.3.1] - 2026-08-13
29
33
 
30
34
  ### Fixed
31
35
 
32
- - Packaging-only republish of `3.2.19` with an allowlisted gemspec (backported from `3.3.0`).
33
- Versions `3.2.16`-`3.2.19` shipped internal repository content (the repo's internal docs tooling,
34
- `.claude/settings.json`) to rubygems.org via the old denylist `spec.files`; `3.2.19` is
35
- yanked and `3.2.16`/`3.2.18` are queued for deletion by RubyGems support. `3.2.21` is the
36
- identical `lib/` code packaged clean, so constraints like `'~> 3.2.0', '>= 3.2.19'` keep
37
- resolving on the 3.2 line (its graphql dependency stays `~> 1.3`, satisfied by the clean
38
- `1.3.16`). The version number `3.2.20` is intentionally skipped: it exists as a
39
- tagged-but-deliberately-unpublished version (see the 3.3.0-era changelog corrections).
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
 
@@ -36,6 +36,10 @@ module Eco::API::Common::Session
36
36
  enviro.api?(version: version)
37
37
  end
38
38
 
39
+ def missing_api_params(version: nil)
40
+ enviro.missing_api_params(version: version)
41
+ end
42
+
39
43
  def file_manager
40
44
  enviro.file_manager
41
45
  end
@@ -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)
@@ -0,0 +1,3 @@
1
+ # Helper class to create a custom `Cli` config
2
+ class Eco::API::Custom::Cli < Eco::API::Common::Loaders::CliConfig
3
+ end
@@ -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
- kargs = {
228
- host: host,
229
- org_id: org_id,
230
- email: email,
231
- pass: pass
232
- }
233
-
234
- kargs.merge!({no_schema: true}) if ENV['GRAPHQL_FETCH_SCHEMA'] == "false"
235
- klass.new(**kargs)
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 && host
270
+ {internal_key: internal_key, host: host}
254
271
  when :v1
255
- external_key && host
272
+ {external_key: external_key, host: host}
256
273
  when :v2
257
- external_key && user_key && host
274
+ {external_key: external_key, user_key: user_key, host: host}
258
275
  when :graphql
259
- email && pass && org_id && host
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,7 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ # NATIVE System Access Logs readers (currentOrganization.activityLogs).
3
+ module AccessLogs
4
+ end
5
+ end
6
+
7
+ require_relative 'access_logs/base'
@@ -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
- msg = "The credentials or basic graphql config are missing for the active environment"
10
- raise msg unless session.api?(version: :graphql)
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
@@ -3,6 +3,7 @@ module Eco::API::UseCases::GraphQL::Helpers
3
3
  require_relative 'base/error_handling'
4
4
  require_relative 'base/case_env'
5
5
  require_relative 'base/graphql_env'
6
+ require_relative 'base/connection_reader'
6
7
 
7
8
  include CaseEnv
8
9
  include GraphQLEnv
@@ -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
@@ -1,8 +1,10 @@
1
1
  require_relative 'base/load'
2
+ require_relative 'base/manager_settings'
2
3
 
3
4
  module Eco::API::UseCases::GraphQL::Helpers::Contractors
4
5
  module Base
5
6
  include Eco::API::UseCases::GraphQL::Helpers::Base
6
7
  include Load
8
+ include ManagerSettings
7
9
  end
8
10
  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,7 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ # NATIVE dashboard readers (currentOrganization.dashboardSearch).
3
+ module Dashboards
4
+ end
5
+ end
6
+
7
+ require_relative 'dashboards/base'
@@ -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
@@ -13,6 +13,7 @@ module Eco::API::UseCases::GraphQL::Helpers
13
13
  end
14
14
 
15
15
  require_relative 'pages/shortcuts'
16
+ require_relative 'pages/activities'
16
17
  require_relative 'pages/filters'
17
18
  require_relative 'pages/ooze_handlers'
18
19
  require_relative 'pages/rescuable'
@@ -7,3 +7,5 @@ require_relative 'helpers/base'
7
7
  require_relative 'helpers/location'
8
8
  require_relative 'helpers/contractors'
9
9
  require_relative 'helpers/pages'
10
+ require_relative 'helpers/access_logs'
11
+ require_relative 'helpers/dashboards'
@@ -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
- class Template::Base < Eco::API::UseCases::GraphQL::Base
21
- name 'graphql-template-base'
22
- type :other
27
+ module Template
28
+ class Base < Eco::API::UseCases::GraphQL::Base
29
+ name 'graphql-template-base'
30
+ type :other
23
31
 
24
- require_relative 'command_emitter'
32
+ require_relative 'command_emitter'
25
33
 
26
- def process
27
- commands = desired_commands
28
- if simulate?
29
- log(:info) { preview_message(commands) }
30
- return nil
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
- # == Subclass override point ================================================
43
+ # == Subclass override point ================================================
36
44
 
37
- # Declare the desired template structure onto the given CommandEmitter.
38
- def declare(_emitter)
39
- raise NotImplementedError, "Implement #declare(emitter) in #{self.class}"
40
- end
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
- # The ordered command batch this template would apply. Pure — no client needed.
43
- def desired_commands
44
- emitter = CommandEmitter.new
45
- declare(emitter)
46
- emitter.commands
47
- end
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
- protected
57
+ protected
50
58
 
51
- # Persist a fresh template from the command batch. Override to #update an existing one.
52
- def apply(commands)
53
- graphql.template.create(commands: commands)
54
- end
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
- # Update an existing template model with the command batch.
57
- def apply_update(model, commands)
58
- graphql.template.update(model, commands: commands)
59
- end
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
- private
69
+ private
62
70
 
63
- def preview_message(commands)
64
- [
65
- "Simulate — would apply #{commands.size} workflow command(s):",
66
- *commands.map { |c| " * #{c.keys.first}: #{c.values.first.inspect}" }
67
- ].join("\n")
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
- def option(label:, weight: nil)
119
- @emitter.emit(:addSelectFieldOption, data_field_id: @field_id, label: label, weight: weight)
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` pairs,
19
- # e.g. "High:10|Medium:5|Low:0". Weight optional ("High|Medium|Low").
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
- def options(cell)
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
- label, weight = token.split(OPTION_WEIGHT_SEPARATOR, 2).map(&:strip)
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
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = '3.2.23'.freeze
2
+ VERSION = '3.3.3'.freeze
3
3
  end
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.2.23
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.14
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.14
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: '1.3'
262
+ version: '2.0'
263
263
  - - ">="
264
264
  - !ruby/object:Gem::Version
265
- version: 1.3.14
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: '1.3'
272
+ version: '2.0'
273
273
  - - ">="
274
274
  - !ruby/object:Gem::Version
275
- version: 1.3.14
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