eco-helpers 3.2.13 → 3.2.16

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/conventions/code-working-tree-protocol.md +176 -0
  3. data/.ai-assistance/scripts/token-logger.js +220 -0
  4. data/.ai-assistance/scripts/token-report.ts +158 -0
  5. data/.ai-assistance/scripts/token-session-start.js +66 -0
  6. data/.ai-assistance/skills/ep-ai-manager/SKILL.md +417 -0
  7. data/.ai-assistance/skills/ruby-scripting/SKILL.md +215 -0
  8. data/.ai-assistance/standards-version.json +10 -0
  9. data/.ai-assistance/token-budget.json +39 -0
  10. data/.claude/settings.json +103 -0
  11. data/.gitignore +2 -0
  12. data/CHANGELOG.md +29 -1
  13. data/CLAUDE.md +83 -0
  14. data/eco-helpers.gemspec +1 -1
  15. data/lib/eco/api/usecases/CLAUDE.md +78 -0
  16. data/lib/eco/api/usecases/default/pages.rb +30 -0
  17. data/lib/eco/api/usecases/default/utils/add_page_id_case.rb +273 -0
  18. data/lib/eco/api/usecases/default/utils/cli/add_page_id_cli.rb +29 -0
  19. data/lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb +5 -0
  20. data/lib/eco/api/usecases/default/utils/cli/track_files_cli.rb +16 -0
  21. data/lib/eco/api/usecases/default/utils/group_csv_case/file_handler.rb +62 -0
  22. data/lib/eco/api/usecases/default/utils/group_csv_case.rb +64 -22
  23. data/lib/eco/api/usecases/default/utils/track_files_case.rb +179 -0
  24. data/lib/eco/api/usecases/default/utils.rb +2 -0
  25. data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
  26. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
  27. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
  28. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
  29. data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
  30. data/lib/eco/api/usecases/graphql/compat.rb +6 -0
  31. data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
  32. data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
  33. data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
  34. data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
  35. data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
  36. data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
  37. data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
  38. data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
  39. data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
  40. data/lib/eco/api/usecases/graphql/samples/pages.rb +7 -0
  41. data/lib/eco/api/usecases/graphql/samples.rb +1 -0
  42. data/lib/eco/api/usecases/graphql.rb +1 -0
  43. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
  44. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +7 -1
  45. data/lib/eco/version.rb +1 -1
  46. metadata +36 -3
@@ -0,0 +1,103 @@
1
+ {
2
+ "permissions": {
3
+ "defaultMode": "auto",
4
+ "allowedTools": [
5
+ "Read"
6
+ ],
7
+ "permissionRules": [
8
+ {
9
+ "tool": "Write",
10
+ "pattern": "/**",
11
+ "action": "allow"
12
+ },
13
+ {
14
+ "tool": "StrReplace",
15
+ "pattern": "/**",
16
+ "action": "allow"
17
+ }
18
+ ],
19
+ "allow": [
20
+ "WebFetch(domain:anthropic.com)",
21
+ "WebFetch(domain:npmjs.com)",
22
+ "Write(.glaudeignore)",
23
+ "Update(.glaudeignore)",
24
+ "Bash(rm .ai-assistance/bridge/LOCK)",
25
+ "Read(.git/**)",
26
+ "Read(**/rubygems/**)",
27
+ "Write(*.md)",
28
+ "Edit(.ai-assistance/**)",
29
+ "Update(.ai-assistance/**)",
30
+ "Write(.ai-assistance/**)",
31
+ "Write(lib/**)",
32
+ "Write(spec/**)",
33
+ "Bash(git status)",
34
+ "Bash(git diff *)",
35
+ "Bash(git log *)",
36
+ "Bash(git add *)",
37
+ "Bash(git commit *)",
38
+ "PowerShell(git status)",
39
+ "PowerShell(git diff *)",
40
+ "PowerShell(git log *)",
41
+ "PowerShell(git add *)",
42
+ "PowerShell(git commit *)",
43
+ "Bash(npm test)",
44
+ "Bash(npm run lint)",
45
+ "Bash(npm run build)",
46
+ "Bash(vitest *)",
47
+ "Bash(jest *)",
48
+ "PowerShell(npm test)",
49
+ "PowerShell(npm run lint)",
50
+ "PowerShell(npm run build)",
51
+ "Bash(black .)",
52
+ "Bash(pytest)",
53
+ "Bash(python -m unittest)",
54
+ "Bash(ruff check *)",
55
+ "Bash(bundle exec rspec *)",
56
+ "Bash(bundle exec rubocop *)",
57
+ "PowerShell(pytest)",
58
+ "PowerShell(python -m unittest)",
59
+ "PowerShell(ruff check *)",
60
+ "PowerShell(bundle exec rspec *)",
61
+ "PowerShell(bundle exec rubocop *)",
62
+ "Bash(node .ai-assistance/scripts/token-logger.js)",
63
+ "Bash(node .ai-assistance/scripts/token-session-start.js)"
64
+ ],
65
+ "deny": [
66
+ "Read(*.env)",
67
+ "Read(./.env*)",
68
+ "Read(./secrets/**)",
69
+ "Bash(*cat *.env*)",
70
+ "Bash(*grep *.env*)",
71
+ "Bash(printenv*)",
72
+ "Bash(env)",
73
+ "Write(.git/*)",
74
+ "Edit(.git/*)",
75
+ "Bash(git push *)",
76
+ "Bash(rm -rf *)"
77
+ ]
78
+ },
79
+ "hooks": {
80
+ "SessionStart": [
81
+ {
82
+ "matcher": "",
83
+ "hooks": [
84
+ {
85
+ "type": "command",
86
+ "command": "node .ai-assistance/scripts/token-session-start.js 2>/dev/null || true"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "Stop": [
92
+ {
93
+ "matcher": "",
94
+ "hooks": [
95
+ {
96
+ "type": "command",
97
+ "command": "node .ai-assistance/scripts/token-logger.js 2>/dev/null || true"
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ }
103
+ }
data/.gitignore CHANGED
@@ -21,3 +21,5 @@ Gemfile.lock
21
21
  .rspec_status
22
22
  scratch.rb
23
23
  .byebug_history
24
+
25
+ .ai-assistance/local/
data/CHANGELOG.md CHANGED
@@ -2,7 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## [3.2.14] - 2026-04-xx
5
+ ## [3.2.16] - 2026-07-02
6
+
7
+ APIv2→GraphQL cutover — the OozeRedirect compat layer that runs the maintained ooze integrations
8
+ (toocs-coding, cans-upsert, supplier-documents) on GraphQL. Requires ecoportal-api-graphql >= 1.3.9.
9
+
10
+ ### Fixed / Changed
11
+
12
+ - **OozeRedirect slimmed onto the base OozeSamples loop** — removed the `with_each_entry` /
13
+ `update_ooze` / `process_ooze` overrides that duplicated and silently bypassed the base loop
14
+ (KPIs, dedup, queue, `dry_run_feedback`). Fetches go through `ooze → apiv2.pages.get` and saves
15
+ through `update_oozes → update_ooze → apiv2.pages.update`; a captured `submit!`/`sign_off!` rides
16
+ along on the single `updatePage` via `Input::Page::Update.from_model` (reads the `_compat_*` flags).
17
+ - **`dirty?`** also treats a pending `submit!`/`sign_off!` as dirty (so a submit-only page still saves).
18
+ - **`OozeBaseCase#dry_run_feedback`** prints an affirmative `[dry-run] would create/update <ref>` line.
19
+ - **`RegisterUpdateCase#enqueue`** accepts any duck-typed entry (`#dirty?` + `#as_update`), not just
20
+ v2 `Page`/`PageStage` — GraphQL compat pages queue correctly under OozeRedirect.
21
+
22
+ ## [3.2.15] - 2026-05-xx
6
23
 
7
24
  ### Added
8
25
 
@@ -10,6 +27,17 @@ All notable changes to this project will be documented in this file.
10
27
 
11
28
  ### Fixed
12
29
 
30
+ ## [3.2.14] - 2026-05-22
31
+
32
+ ### Added
33
+
34
+ - `track-files` case
35
+ - `add-page-id` case
36
+
37
+ ### Changed
38
+
39
+ - **improvement**: added `-format` argument to `-group-csv` to output a `jsonl` **custom** file.
40
+
13
41
  ## [3.2.13] - 2026-04-15
14
42
 
15
43
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,83 @@
1
+ # CLAUDE.md — eco-helpers
2
+
3
+ AI agent instructions for this repository.
4
+
5
+ **Cross-cutting architecture context lives in `ecoportal-api-graphql` — see its `CLAUDE.md` and `.claude/` folder for the full dependency map, project history, and shared skills.**
6
+
7
+ ---
8
+
9
+ ## Repository Role
10
+
11
+ `eco-helpers` is the **primary downstream consumer** of the EcoPortal API gem stack. It provides a scripting and automation framework for interacting with EcoPortal — CLI tooling, use-case orchestration, data transformation, and batch operations.
12
+
13
+ **Position in chain:**
14
+ ```
15
+ ecoportal-api
16
+ ecoportal-api-v2
17
+ ecoportal-api-graphql
18
+
19
+ eco-helpers ← THIS REPO
20
+ ```
21
+
22
+ **Remote:** https://gitlab.ecoportal.co.nz/oscar/script_api_helpers.git
23
+
24
+ **Gem dependencies on the stack:**
25
+ - `ecoportal-api ~> 0.10, >= 0.10.14`
26
+ - `ecoportal-api-v2 ~> 3.3, >= 3.3.1`
27
+ - `ecoportal-api-graphql ~> 1.3, >= 1.3.4`
28
+
29
+ ---
30
+
31
+ ## Key Folder Layout
32
+
33
+ ```
34
+ lib/eco/
35
+ api/ API integration layer
36
+ common/ Shared helpers
37
+ microcases/ Fine-grained reusable operations
38
+ organization/ Org-level resources
39
+ session.rb Session management (entry point for scripting)
40
+ usecases.rb Use-case registry
41
+ policies.rb Access policies
42
+ cli/ CLI framework
43
+ cli_default/ Default CLI options, filters, people workflows
44
+ common/ Cross-cutting utilities
45
+ csv/ CSV reading, streaming, splitting
46
+ data/ Data utilities (fuzzy match, hashes, locations, strings, files)
47
+ language/ Logging, curry, auxiliar utilities
48
+ assets/ Static assets (language files etc.)
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Namespace
54
+
55
+ `Eco::` — entirely separate namespace from `Ecoportal::`. Does not reopen upstream gem namespaces.
56
+
57
+ ---
58
+
59
+ ## Key Concerns
60
+
61
+ - This gem is the **backwards-compatibility target** for all upstream gems. When `ecoportal-api-graphql` changes its public interface, check usage here first.
62
+ - `Eco::API::Session` is the main consumer of `Ecoportal::API::GraphQL` — it's the first place to look when checking how GraphQL features are used downstream.
63
+ - The CLI layer (`Eco::CLI`) uses Thor-style commands — changes to API interfaces may silently break CLI workflows if not tested end-to-end.
64
+ - Many operations are batch-oriented with progress logging — error handling and partial-failure behaviour matters.
65
+
66
+ ---
67
+
68
+ ## How to Find GraphQL Usage
69
+
70
+ ```bash
71
+ grep -r "GraphQL\|graphql\|ecoportal-api-graphql" lib/ --include="*.rb" -l
72
+ ```
73
+
74
+ This shows which files directly use the GraphQL gem — useful when assessing impact of upstream changes.
75
+
76
+ ---
77
+
78
+ ## Running Tests
79
+
80
+ ```bash
81
+ bundle install
82
+ bundle exec rspec
83
+ ```
data/eco-helpers.gemspec CHANGED
@@ -42,7 +42,7 @@ Gem::Specification.new do |spec|
42
42
  spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
43
43
  spec.add_dependency 'dotenv', '~> 3'
44
44
  spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.14'
45
- spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.4'
45
+ spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.9'
46
46
  spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.1'
47
47
  spec.add_dependency 'ed25519', '~> 1.2'
48
48
  spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
@@ -0,0 +1,78 @@
1
+ # usecases
2
+
3
+ The use-case registry and all built-in case base classes for scripting against EcoPortal.
4
+
5
+ ---
6
+
7
+ ## What a use case is
8
+
9
+ A use case is a self-contained, named, runnable unit of work. It registers itself with
10
+ the CLI framework, receives `session`, `options`, and `usecase` from the runner, and
11
+ executes its `process` (or `process_ooze` / `process_page`) method.
12
+
13
+ ```
14
+ CLI invokes rake → rake finds registered case → UseCase#launch → main() → process()
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Directory structure
20
+
21
+ | Path | What lives there |
22
+ |------|-----------------|
23
+ | `graphql/` | GraphQL-native base cases + samples (see `graphql/CLAUDE.md`) |
24
+ | `ooze_samples/` | APIv2/REST base cases: `OozeBaseCase`, `RegisterUpdateCase` |
25
+ | `ooze_cases/` | Concrete built-in ooze cases (export register, etc.) |
26
+ | `default/` | Built-in people, location, and utility cases |
27
+ | `default_cases/` | Loader and samples for default cases |
28
+ | `samples/` | Misc driver samples |
29
+ | `graphql.rb` | GraphQL use case loader |
30
+ | `ooze_samples.rb` | Ooze/REST use case loader |
31
+ | `default.rb` | Default use case loader |
32
+
33
+ ---
34
+
35
+ ## Adding a new use case
36
+
37
+ 1. Subclass the appropriate base:
38
+
39
+ | Your use case | Inherit from |
40
+ |---|---|
41
+ | Process pages in a register (update workflow) | `Eco::API::UseCases::GraphQL::PageCase` |
42
+ | Process pages org-wide (cross-register, audit) | `Eco::API::UseCases::GraphQL::OrgPageCase` |
43
+ | Custom GraphQL script (export, report, one-off) | `Eco::API::UseCases::GraphQL::Base` |
44
+ | Legacy APIv2 register update | `Eco::API::UseCases::OozeSamples::RegisterUpdateCase` |
45
+
46
+ 2. Set `name` and `type`:
47
+ ```ruby
48
+ name 'my-case-name' # CLI identifier: called with -my-case-name
49
+ type :other # :people | :contractors | :other
50
+ ```
51
+
52
+ 3. Override the entry point (`process_page`, `process`, or `process_ooze`).
53
+
54
+ 4. Register in the org's `config/cli.rb`:
55
+ ```ruby
56
+ cases.add('-my-case-name', :other, 'Description')
57
+ ```
58
+
59
+ ---
60
+
61
+ ## How cases are launched
62
+
63
+ `Eco::API::UseCases::UseCase#launch` calls `callback.call(*uio.params)` where the
64
+ callback is bound to `method(:main)`. Before launch, `@session` and `@options` are
65
+ injected into the instance — subclasses access them via the `attr_reader` in `CaseEnv`.
66
+
67
+ The `:other` type passes `(session, options, usecase)` positionally to `main`.
68
+ For `GraphQL::Base` subclasses the signature is `main(*_args)` — `session` and
69
+ `options` are already available via the helpers module before `main` is called.
70
+
71
+ ---
72
+
73
+ ## Related
74
+
75
+ - `graphql/CLAUDE.md` — GraphQL case hierarchy, PageCase/OrgPageCase
76
+ - `ooze_samples/` — legacy v2 cases (RegisterUpdateCase, OozeBaseCase)
77
+ - `eco-helpers/CLAUDE.md` — top-level gem context
78
+ - `ecoportal-api-graphql` — upstream gem providing `SearchConf`, `Compat::Pages`, etc.
@@ -0,0 +1,30 @@
1
+ module Eco
2
+ module API
3
+ class UseCases
4
+ class Default
5
+ # Namespace for CLI-integrated page use cases.
6
+ # These are concrete, fully functional cases exposed to all org environments.
7
+ #
8
+ # == Convention
9
+ # Cases here should:
10
+ # - Inherit from Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
11
+ # (or OrgPage::Base for org-wide operations)
12
+ # - Be registered in the org's config/cli.rb
13
+ # - Be as org-agnostic as possible (accept register_id, filters via options)
14
+ #
15
+ # == Adding a new default page case
16
+ # 1. Create the file here: default/pages/my_case.rb
17
+ # 2. Inherit from the appropriate pages sample base class
18
+ # 3. Require it below
19
+ # 4. Wire CLI registration in usecases/default_cases/samples.rb or the
20
+ # org's config/cli.rb
21
+ #
22
+ # == Currently
23
+ # No default page cases yet — add them here as common patterns emerge
24
+ # across org implementations.
25
+ module Pages
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,273 @@
1
+ # @note you might add a `filter` method
2
+ #
3
+ # def filter
4
+ # @filter ||= proc do |row, _r_idx|
5
+ # next true
6
+ # next true unless (ref_id = row[pivot_column(row)])
7
+ # next false if excluded_ref_id?(ref_id)
8
+ #
9
+ # true
10
+ # end
11
+ # end
12
+ #
13
+ class Eco::API::UseCases::Default::Utils::AddPageId < Eco::API::Custom::UseCase
14
+ name 'add-page-id'
15
+ type :other
16
+
17
+ require_relative 'cli/add_page_id_cli'
18
+
19
+ PIVOT_FIELD = [
20
+ 'ref_id'
21
+ ].freeze
22
+
23
+ PAGE_ID = 'page_id'.freeze
24
+ EXCLUDED_REF_IDS = %w[].freeze
25
+
26
+ def main(*_args)
27
+ if simulate?
28
+ count = Eco::CSV.count(input_file)
29
+ log(:info) { "CSV '#{input_file}' has #{count} rows." }
30
+ else
31
+ generate_file(&filter)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :headers, :headers_rest
38
+
39
+ def filter
40
+ nil
41
+ end
42
+
43
+ def excluded_ref_id?(ref_id)
44
+ self.class::EXCLUDED_REF_IDS.include?(ref_id)
45
+ end
46
+
47
+ def generate_file # rubocop:disable Metrics/AbcSize
48
+ idx = -1
49
+ row_count = 0
50
+ headers_added = false
51
+
52
+ CSV.open(output_filename, 'wb') do |csv|
53
+ puts "\n"
54
+
55
+ Eco::CSV.foreach(input_file, headers: true, skip_blanks: true) do |row|
56
+ idx += 1
57
+
58
+ next unless !block_given? || yield(row, idx)
59
+
60
+ unless headers_added
61
+ headers!(row)
62
+ require_pivot_field!(row, file: input_file)
63
+
64
+ csv << headers
65
+ headers_added = true
66
+ end
67
+
68
+ unless (pivot_value = row[pivot_field])
69
+ msg = "Row #{idx} doesn't have value for pivot field '#{pivot_field}'"
70
+ msg << ". Skipping (discarded) ..."
71
+ log(:warn) { msg }
72
+ next
73
+ end
74
+
75
+ unless (page_id = input_maps[pivot_value])
76
+ warn_unknown_mapping_reference!(pivot_value)
77
+ next
78
+ end
79
+
80
+ row_count += 1
81
+
82
+ if (row_count % 500).zero?
83
+ print "... Mapped #{row_count} rows \r"
84
+ $stdout.flush
85
+ end
86
+
87
+ values = [page_id, pivot_value]
88
+ oth_values = row.values_at(*headers_rest)
89
+ values.concat(oth_values) unless headers_rest.empty?
90
+
91
+ csv << values
92
+ end
93
+ end
94
+ ensure
95
+ msg = "Generated file '#{output_filename}' with #{row_count} rows (out of #{idx})."
96
+ log(:info) { msg } unless simulate?
97
+ end
98
+
99
+ def warn_unknown_mapping_reference!(ref_id)
100
+ return if unknown.include?(ref_id)
101
+
102
+ unknown << ref_id
103
+ msg = "Could not map '#{pivot_field}' '#{ref_id}' to a '#{page_id_field}'"
104
+ msg << ". Skipping (discarded) ..."
105
+
106
+ log(:warn) { msg }
107
+ end
108
+
109
+ def unknown
110
+ @unknown ||= []
111
+ end
112
+
113
+ def headers!(row)
114
+ return if instance_variable_defined?(:@headers)
115
+
116
+ @headers_rest = row.headers - base_out_header(row)
117
+ @headers = [*base_out_header, *headers_rest]
118
+ end
119
+
120
+ def base_out_header(row = nil)
121
+ @base_out_header ||= [page_id_field, pivot_field(row)] # space: :output
122
+ end
123
+
124
+ def input_maps
125
+ return @input_maps if instance_variable_defined?(:@input_maps)
126
+
127
+ @input_maps = {}
128
+ idx = 0
129
+
130
+ Eco::CSV.foreach(input_maps_file, headers: true) do |row|
131
+ idx += 1
132
+
133
+ if (idx % 500).zero?
134
+ print "... Creating mappings table (#{idx} done) \r"
135
+ $stdout.flush
136
+ end
137
+
138
+ require_pivot_field!(row, space: :maps, file: input_maps_file)
139
+ require_page_id_field!(row, file: input_maps_file)
140
+
141
+ ref_id = row[pivot_field(space: :maps)]
142
+ page_id = row[page_id_field(space: :maps)]
143
+
144
+ @input_maps[ref_id] = page_id
145
+ end
146
+
147
+ @input_maps
148
+ end
149
+
150
+ def input_maps_file
151
+ options.dig(:input, :maps).tap do |file|
152
+ next if file && File.exist?(file)
153
+
154
+ log(:error) {
155
+ msg = "You must specify an existing maps file with the option '-maps-file'"
156
+ msg << ".\n * File: '#{file}' does not exist" unless file.nil?
157
+ msg
158
+ }
159
+ exit 1
160
+ end
161
+ end
162
+
163
+ def output_filename
164
+ return nil unless input_name
165
+
166
+ File.join(
167
+ input_dir,
168
+ "#{input_name}_mapped#{input_ext}"
169
+ )
170
+ end
171
+
172
+ def input_name
173
+ @input_name ||= File.basename(
174
+ input_basename,
175
+ input_ext
176
+ )
177
+ end
178
+
179
+ def input_ext
180
+ @input_ext ||= input_basename.split('.')[1..].join('.').then do |name|
181
+ ".#{name}"
182
+ end
183
+ end
184
+
185
+ def input_basename
186
+ @input_basename ||= File.basename(input_full_filename)
187
+ end
188
+
189
+ def input_dir
190
+ @input_dir = File.dirname(input_full_filename)
191
+ end
192
+
193
+ def input_full_filename
194
+ @input_full_filename ||= File.expand_path(input_file)
195
+ end
196
+
197
+ def input_file
198
+ options.dig(:input, :file)
199
+ end
200
+
201
+ def require_pivot_field!(row, file:, space: :input)
202
+ return true if row.key?(pivot_field(row, space: space))
203
+
204
+ msg = "Pivot field '#{pivot_field}' missing in header of file '#{file}'"
205
+ log(:error) { msg }
206
+ raise msg
207
+ end
208
+
209
+ def require_page_id_field!(row, file:)
210
+ return true if row.key?(page_id_field(space: :maps))
211
+
212
+ msg = "Page ID field '#{page_id_field(space: :maps)}' missing in header of file '#{file}'"
213
+ log(:error) { msg }
214
+ raise msg
215
+ end
216
+
217
+ def pivot_field(row = nil, space: :input)
218
+ @pivot_field ||= {}
219
+ return @pivot_field[space] if @pivot_field.key?(space)
220
+
221
+ @pivot_field[space] ||= pivot_fields(space: space).select do |name|
222
+ row.key?(name)
223
+ end.then do |sel|
224
+ next sel.first if sel.one?
225
+
226
+ msg = "Could not find any column named: #{pivot_fields.join(', ')}"
227
+ msg = "Multiple pivot columns: #{sel.join(', ')}" if sel.any?
228
+
229
+ log(:error) { msg }
230
+ raise msg
231
+ end.tap do |col|
232
+ log(:info) { "Using header '#{col}' as pivot column." }
233
+ end
234
+ end
235
+
236
+ def pivot_fields(space: :input)
237
+ @pivot_fields ||= {}
238
+ return @pivot_fields[space] if @pivot_fields.key?(space)
239
+
240
+ return (@pivot_fields[space] = [opts_pivot]) if opts_pivot && space == :input
241
+
242
+ unless self.class.const_defined?(:PIVOT_FIELD)
243
+ msg = "(#{self.class}) You must define PIVOT_FIELD constant"
244
+ log(:error) { msg }
245
+ raise msg
246
+ end
247
+
248
+ @pivot_fields[space] = self.class::PIVOT_FIELD.dup
249
+ end
250
+
251
+ def page_id_field(space: :output)
252
+ @page_id_field = {}
253
+ return @page_id_field[space] if @page_id_field.key?(space)
254
+
255
+ return (@page_id_field[space] = opts_page_id) if opts_page_id && space == :output
256
+
257
+ unless self.class.const_defined?(:PAGE_ID)
258
+ msg = "(#{self.class}) You must define PAGE_ID field constant"
259
+ log(:error) { msg }
260
+ raise msg
261
+ end
262
+
263
+ @page_id_field[space] = self.class::PAGE_ID
264
+ end
265
+
266
+ def opts_pivot
267
+ options.dig(:input, :pivot_field)
268
+ end
269
+
270
+ def opts_page_id
271
+ options.dig(:input, :page_id)
272
+ end
273
+ end
@@ -0,0 +1,29 @@
1
+ class Eco::API::UseCases::Default::Utils::AddPageId
2
+ class Cli < Eco::API::UseCases::Cli
3
+ desc 'Adds the page_id column based on mappings onto -pivot'
4
+
5
+ callback do |_session, options, _usecase|
6
+ if (file = SCR.get_file(cli_name, required: true, should_exist: true))
7
+ options.deep_merge!(input: {file: file})
8
+ end
9
+ end
10
+
11
+ add_option('-maps-file', 'Source file with he mappings') do |options|
12
+ if (file = SCR.get_file('-maps-file', required: true, should_exist: true))
13
+ options.deep_merge!(input: {maps: file})
14
+ end
15
+ end
16
+
17
+ add_option('-pivot', 'The column that should be used to pivot') do |options|
18
+ if (file = SCR.get_arg("-pivot", with_param: true))
19
+ options.deep_merge!(input: {pivot_field: file})
20
+ end
21
+ end
22
+
23
+ add_option('-page-id', 'The column that should be used to dump the id') do |options|
24
+ if (file = SCR.get_arg("-page-id", with_param: true))
25
+ options.deep_merge!(input: {page_id: file})
26
+ end
27
+ end
28
+ end
29
+ end
@@ -22,5 +22,10 @@ class Eco::API::UseCases::Default::Utils::GroupCsv
22
22
  options.deep_merge!(input: {group_by_field: file})
23
23
  end
24
24
  end
25
+
26
+ add_option('-format', 'Kind of extract (csv - default | jsonl') do |options|
27
+ format = SCR.get_arg('-format', with_param: true)
28
+ options.deep_merge!(output: {format: format})
29
+ end
25
30
  end
26
31
  end
@@ -0,0 +1,16 @@
1
+ class Eco::API::UseCases::Default::Utils::TrackFiles
2
+ class Cli < Eco::API::UseCases::Cli
3
+ desc 'Tracks the files of a folder in a CSV'
4
+
5
+ callback do |_session, options, _usecase|
6
+ if (folder = SCR.get_file(cli_name, required: true))
7
+ options.deep_merge!(input: {folder: folder})
8
+ end
9
+ end
10
+
11
+ add_option("-s3-path", "Relative subpath from the S3 uploads folder.") do |options|
12
+ path = SCR.get_arg("-s3-path", with_param: true)
13
+ options.deep_merge!(output: {s3_path: path})
14
+ end
15
+ end
16
+ end