eco-helpers 3.2.14 → 3.2.22

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/lib/eco/api/usecases/CLAUDE.md +78 -0
  4. data/lib/eco/api/usecases/default/pages.rb +30 -0
  5. data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
  6. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
  7. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
  8. data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
  9. data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
  10. data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
  11. data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
  12. data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
  13. data/lib/eco/api/usecases/graphql/compat.rb +11 -0
  14. data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
  15. data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
  16. data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
  17. data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
  18. data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
  19. data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
  20. data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
  21. data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
  22. data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
  23. data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
  24. data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
  25. data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
  26. data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
  27. data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
  28. data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
  29. data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
  30. data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
  31. data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
  32. data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
  33. data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
  34. data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
  35. data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
  36. data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
  37. data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
  38. data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
  39. data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
  40. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
  41. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
  42. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
  43. data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
  44. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
  45. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
  46. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
  47. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
  48. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
  49. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
  50. data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
  51. data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
  52. data/lib/eco/api/usecases/graphql/samples/pages.rb +9 -0
  53. data/lib/eco/api/usecases/graphql/samples.rb +1 -0
  54. data/lib/eco/api/usecases/graphql.rb +1 -0
  55. data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
  56. data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +13 -3
  57. data/lib/eco/version.rb +1 -1
  58. metadata +52 -15
  59. data/.gitignore +0 -23
  60. data/.idea/.gitignore +0 -10
  61. data/.markdownlint.json +0 -4
  62. data/.rspec +0 -3
  63. data/.rubocop.yml +0 -103
  64. data/.ruby-version +0 -1
  65. data/.yardopts +0 -10
  66. data/Gemfile +0 -8
  67. data/Rakefile +0 -38
  68. data/eco-helpers.gemspec +0 -63
@@ -0,0 +1,102 @@
1
+ module Eco::API::UseCases::GraphQL::Compat
2
+ module Parity
3
+ # A/B parity harness for the ooze -> native GraphQL strangler-fig migration.
4
+ #
5
+ # Runs the SAME logical register-processing case through the two code paths —
6
+ # A: legacy (v2 / OozeRedirect shim), B: native GraphQL — and asserts that the
7
+ # observable outcome (KPI counters + per-page update payloads) is equivalent.
8
+ #
9
+ # The comparison logic (RunResult / Comparison) is PURE and offline-runnable.
10
+ # The harness itself is a thin orchestrator: you hand it two callables, each of
11
+ # which runs its side and returns a RunResult. In a live run each callable would
12
+ # execute the case in `-simulate` mode against the same test org, using a
13
+ # Recorder to capture what each side WOULD write (no data is mutated). Wiring the
14
+ # two live runs needs credentials + a test org; the structural machinery here is
15
+ # verified offline.
16
+ #
17
+ # == Usage (live)
18
+ #
19
+ # harness = Parity::Harness.new(
20
+ # legacy: -> { Parity::Harness.record('legacy') { |rec| run_legacy_case(rec) } },
21
+ # native: -> { Parity::Harness.record('native') { |rec| run_native_case(rec) } }
22
+ # )
23
+ # comparison = harness.run
24
+ # raise comparison.report unless comparison.equivalent?
25
+ #
26
+ # == Usage (offline / spec)
27
+ #
28
+ # a = Parity::RunResult.new(label: 'legacy', kpis: {...}, updates: {...})
29
+ # b = Parity::RunResult.new(label: 'native', kpis: {...}, updates: {...})
30
+ # Parity::Harness.new(legacy: -> { a }, native: -> { b }).run.equivalent?
31
+ class Harness
32
+ # @param legacy [#call] returns the legacy-path RunResult.
33
+ # @param native [#call] returns the native-path RunResult.
34
+ def initialize(legacy:, native:)
35
+ @legacy = legacy
36
+ @native = native
37
+ end
38
+
39
+ # Execute both sides and compare. Returns a Parity::Comparison.
40
+ def run
41
+ a = @legacy.call
42
+ b = @native.call
43
+ assert_run_result!(a, 'legacy')
44
+ assert_run_result!(b, 'native')
45
+ a.compare_to(b)
46
+ end
47
+
48
+ # Capture the observable outcome of a case run into a RunResult.
49
+ #
50
+ # Yields a Recorder. The caller runs the case, feeding each page's would-be
51
+ # update payload to `recorder.record_update(page_id, payload)` (typically wired
52
+ # by stubbing the case's `update_page` to call `graphql.pages.get_body(page)`
53
+ # instead of persisting). After the run, pass the case's KPI hash to
54
+ # `recorder.kpis = case.kpis_hash` (or set counters individually).
55
+ #
56
+ # @param label [String] 'legacy' or 'native'.
57
+ # @yieldparam recorder [Recorder]
58
+ # @return [RunResult]
59
+ def self.record(label)
60
+ recorder = Recorder.new(label)
61
+ yield(recorder) if block_given?
62
+ recorder.to_run_result
63
+ end
64
+
65
+ private
66
+
67
+ def assert_run_result!(obj, side)
68
+ return if obj.is_a?(RunResult)
69
+
70
+ raise TypeError, "The #{side} callable must return a Parity::RunResult, got #{obj.class}"
71
+ end
72
+
73
+ # Collects a run's KPIs + per-page update payloads, then bakes a RunResult.
74
+ class Recorder
75
+ attr_accessor :kpis
76
+
77
+ def initialize(label)
78
+ @label = label
79
+ @kpis = {}
80
+ @updates = {}
81
+ end
82
+
83
+ # Record what a run WOULD send for `page_id`. Later ids for the same page
84
+ # overwrite (the last state wins — matching a real save of the final page).
85
+ def record_update(page_id, payload)
86
+ @updates[page_id.to_s] = payload
87
+ self
88
+ end
89
+
90
+ # Set a single canonical/native KPI counter by name.
91
+ def kpi(name, value)
92
+ @kpis[name.to_sym] = value
93
+ self
94
+ end
95
+
96
+ def to_run_result
97
+ RunResult.new(label: @label, kpis: @kpis, updates: @updates)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,96 @@
1
+ module Eco::API::UseCases::GraphQL::Compat
2
+ module Parity
3
+ # A normalised, comparable snapshot of ONE use-case run.
4
+ #
5
+ # The A/B parity harness reduces a run — whether it went through the legacy
6
+ # (v2 / OozeRedirect) path or the native GraphQL path — to just its OBSERVABLE
7
+ # outcome:
8
+ # * `kpis` — the KPI counter hash (search/retrieved/updated/created/… )
9
+ # * `updates` — the per-page update payload the run WOULD send, keyed by page id
10
+ #
11
+ # Two runs are "equivalent" when, after normalisation, both are identical. This
12
+ # is the unit the strangler-fig migration is gated on: flip a case name only once
13
+ # its native RunResult matches the legacy RunResult over the same input.
14
+ #
15
+ # Everything here is PURE (no session, no network) so equivalence can be asserted
16
+ # offline in specs; the live capture step (feeding real runs in) is the only part
17
+ # that needs credentials.
18
+ class RunResult
19
+ attr_reader :label, :kpis, :updates
20
+
21
+ # @param label [String] human tag for this side ('legacy' / 'native').
22
+ # @param kpis [Hash] KPI counters. Keys are symbolised; values coerced to Integer.
23
+ # @param updates [Hash{String=>Object}] page_id => update payload (any Hash-ish).
24
+ def initialize(label:, kpis: {}, updates: {})
25
+ @label = label.to_s
26
+ @kpis = normalize_kpis(kpis)
27
+ @updates = normalize_updates(updates)
28
+ end
29
+
30
+ # The set of page ids this run acted on.
31
+ def page_ids
32
+ updates.keys
33
+ end
34
+
35
+ # Compare against another RunResult, returning a Comparison value object.
36
+ def compare_to(other)
37
+ Comparison.new(self, other)
38
+ end
39
+
40
+ # Convenience: are the two runs equivalent?
41
+ def equivalent_to?(other)
42
+ compare_to(other).equivalent?
43
+ end
44
+
45
+ # Merge/normalise into a plain hash (handy for golden-file capture).
46
+ def to_h
47
+ { label: label, kpis: kpis, updates: updates }
48
+ end
49
+
50
+ # KPI names differ between the legacy and native cases (e.g. `updated_oozes`
51
+ # vs `updated_pages`). We compare on a CANONICAL subset of counters that both
52
+ # sides expose the same way, mapping each side's names onto the canon.
53
+ CANON_KPI_ALIASES = {
54
+ search: %i[total_search_oozes total_search_pages search],
55
+ duplicated: %i[dupped_search_oozes dupped_search_pages duplicated],
56
+ retrieved: %i[retrieved_oozes retrieved_pages retrieved],
57
+ updated: %i[updated_oozes updated_pages updated],
58
+ created: %i[created_oozes created_pages created],
59
+ skipped: %i[skipped_pages skipped],
60
+ failed: %i[failed_update_oozes failed_pages failed]
61
+ }.freeze
62
+
63
+ private
64
+
65
+ def normalize_kpis(raw)
66
+ source = raw.each_with_object({}) do |(k, v), out|
67
+ out[k.to_sym] = v
68
+ end
69
+
70
+ CANON_KPI_ALIASES.each_with_object({}) do |(canon, aliases), out|
71
+ key = aliases.find {|a| source.key?(a)}
72
+ out[canon] = key ? source[key].to_i : 0
73
+ end
74
+ end
75
+
76
+ # Normalise each update payload so a superficial key-order / string-vs-symbol
77
+ # difference between the two paths does not read as a real divergence.
78
+ def normalize_updates(raw)
79
+ raw.each_with_object({}) do |(id, payload), out|
80
+ out[id.to_s] = deep_normalize(payload)
81
+ end
82
+ end
83
+
84
+ def deep_normalize(value)
85
+ case value
86
+ when Hash
87
+ value.map {|k, v| [k.to_s, deep_normalize(v)]}.sort_by(&:first).to_h
88
+ when Array
89
+ value.map {|v| deep_normalize(v)}
90
+ else
91
+ value
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ module Eco::API::UseCases::GraphQL
2
+ module Compat
3
+ module Parity
4
+ end
5
+ end
6
+ end
7
+
8
+ require_relative 'compat/ooze_redirect'
9
+ require_relative 'compat/parity/run_result'
10
+ require_relative 'compat/parity/comparison'
11
+ require_relative 'compat/parity/harness'
@@ -0,0 +1,79 @@
1
+ # usecases/graphql/helpers
2
+
3
+ Mixin modules providing domain-specific helper methods for GraphQL use cases.
4
+ All modules are ultimately included via `Helpers::Base` into `GraphQL::Base`.
5
+
6
+ ---
7
+
8
+ ## Include chain
9
+
10
+ ```
11
+ GraphQL::Base
12
+ includes Helpers::Base
13
+ includes CaseEnv → session, options, config, simulate?, log, ErrorHandling
14
+ includes GraphQLEnv → graphql (lazy, memoized)
15
+ includes Helpers (loader)
16
+ includes Helpers::Location → location tree helpers
17
+ includes Helpers::Contractors → contractor entity helpers
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Helpers::Base (`helpers/base.rb`)
23
+
24
+ Core environment — included in every GraphQL use case.
25
+
26
+ | Method | Source | Description |
27
+ |--------|--------|-------------|
28
+ | `session` | `CaseEnv` | Current `Eco::API::Session` |
29
+ | `options` | `CaseEnv` | Options hash from CLI/runner |
30
+ | `config` | `CaseEnv` | `session.config` shortcut |
31
+ | `simulate?` | `CaseEnv` | `options[:simulate] \|\| options[:dry_run]` |
32
+ | `log(level)` | `CaseEnv` | Logger proxy |
33
+ | `graphql` | `GraphQLEnv` | Lazy-loaded `Ecoportal::API::GraphQL` instance |
34
+ | `backup(data, type:)` | `Helpers::Base` | Save JSON to requests folder |
35
+ | `exit_error(msg)` | `Helpers::Base` | Log error and `exit(1)` |
36
+
37
+ ---
38
+
39
+ ## Helpers::Location (`helpers/location/`)
40
+
41
+ Location tree access, tag remapping, classification parsing.
42
+
43
+ - `helpers/location/base.rb` — `Location::Base` mixin
44
+ - `helpers/location/base/tree_tracking.rb` — track tree mutations
45
+ - `helpers/location/base/classifications_parser.rb` — parse location classifications
46
+ - `helpers/location/tags_remap/` — remapping tags across location changes
47
+ - `helpers/location/command/` — apply/diff location structure commands
48
+
49
+ ---
50
+
51
+ ## Helpers::Contractors (`helpers/contractors/`)
52
+
53
+ Contractor entity loading helpers.
54
+
55
+ - `helpers/contractors/base.rb` — base contractor helpers
56
+ - `helpers/contractors/base/load.rb` — batch load contractor entities
57
+
58
+ ---
59
+
60
+ ## Adding a new helper
61
+
62
+ 1. Create `helpers/my_domain/base.rb`:
63
+ ```ruby
64
+ module Eco::API::UseCases::GraphQL::Helpers
65
+ module MyDomain
66
+ module Base
67
+ private
68
+ def my_helper_method
69
+ graphql.myDomainQuery(...)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ ```
75
+ 2. Create `helpers/my_domain.rb` as a loader that includes `Base`
76
+ 3. Add `require_relative 'my_domain'` to `helpers.rb`
77
+
78
+ The helper is then available in all cases that include `Helpers::Base` (i.e., all
79
+ subclasses of `GraphQL::Base` including `PageCase` and `OrgPageCase`).
@@ -29,7 +29,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
29
29
  messages
30
30
  }
31
31
  draft { # rubocop:disable Style/BlockDelimiters
32
- ___Ecoportal__API__GraphQL__Fragment__LocationDraft
32
+ spread :LocationDraft
33
33
  }
34
34
  }
35
35
  end
@@ -44,7 +44,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
44
44
  messages
45
45
  }
46
46
  draft { # rubocop:disable Style/BlockDelimiters
47
- ___Ecoportal__API__GraphQL__Fragment__LocationDraft
47
+ spread :LocationDraft
48
48
  }
49
49
  }
50
50
  end
@@ -54,7 +54,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
54
54
  clientMutationId
55
55
  ok
56
56
  error { # rubocop:disable Style/BlockDelimiters
57
- ___Ecoportal__API__GraphQL__Fragment__LocationsError
57
+ spread :LocationsError
58
58
  }
59
59
  errors { # rubocop:disable Style/BlockDelimiters
60
60
  details
@@ -69,7 +69,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Location
69
69
  __typename
70
70
  }
71
71
  error { # rubocop:disable Style/BlockDelimiters
72
- ___Ecoportal__API__GraphQL__Fragment__LocationsError
72
+ spread :LocationsError
73
73
  }
74
74
  }
75
75
  }
@@ -0,0 +1,71 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Pages
3
+ # Native GraphQL re-expression of OozeSamples::HelpersMigration::Copying.
4
+ #
5
+ # Provides the "pair source fields to destination fields and copy their
6
+ # content across" flow used by the native migration case. Pairing is delegated
7
+ # to `TypedFieldsPairing`; the per-field content copy (`copy_field_content`)
8
+ # dispatches on the GraphQL DataField TYPE STRING (PlainText/Select/Date/...),
9
+ # mirroring how OozeHandlers#merge_values ports the v2 type-class dispatch.
10
+ #
11
+ # Only the generic-typed-pairing flow is ported (the v2 regex/JSON hooked-field
12
+ # mapping — `copy_hooked_fields` / `with_src_dst_field_pair` — is a later
13
+ # increment and is intentionally omitted here).
14
+ module Copying
15
+ include Eco::Language::AuxiliarLogger
16
+ include Eco::API::UseCases::GraphQL::Helpers::Pages::Shortcuts
17
+
18
+ def session
19
+ defined?(super) ? super : @session
20
+ end
21
+
22
+ # Pair the same-type/same-label fields of `src` and `dst`, and copy the
23
+ # content of each pair from source to destination.
24
+ #
25
+ # @param src [Object] the source page model.
26
+ # @param dst [Object] the destination (draft) page model.
27
+ # @param src_excluded [Array] source fields to exclude from pairing.
28
+ # @param dst_excluded [Array] destination fields to exclude from pairing.
29
+ # @param exclude_ximport [Boolean] skip ximport-marked fields.
30
+ # @yield [src_fld, dst_fld, pairing] optional post-copy callback per pair.
31
+ # @return [TypedFieldsPairing] the tracker (unpaired / multi-pair inspection).
32
+ def copy_generic_paired_fields(src, dst, src_excluded: [], dst_excluded: [], exclude_ximport: false)
33
+ TypedFieldsPairing.new(
34
+ src,
35
+ dst,
36
+ src_excluded: src_excluded,
37
+ dst_excluded: dst_excluded,
38
+ exclude_ximport: exclude_ximport
39
+ ).tap do |pairing|
40
+ pairing.each_pair do |src_fld, dst_fld|
41
+ copy_field_content(src_fld, dst_fld)
42
+ yield(src_fld, dst_fld, pairing) if block_given?
43
+ end
44
+ end
45
+ end
46
+
47
+ # Copy the content of `src` into `dst`, dispatching on the GraphQL DataField
48
+ # TYPE STRING. `src` and `dst` are guaranteed to be the same type by the
49
+ # pairing. Types with no meaningful/copyable content (or whose native setter
50
+ # is not yet supported) are left untouched.
51
+ #
52
+ # @param src [#type] the source data field.
53
+ # @param dst [#type] the destination data field.
54
+ # @return [Object] result of the per-type copy (or a "won't copy" marker).
55
+ def copy_field_content(src, dst)
56
+ case src.type.to_s
57
+ when 'PlainText', 'Number', 'Date', 'Gauge'
58
+ dst.value = src.value
59
+ when 'RichText'
60
+ dst.content = src.content
61
+ when 'Select'
62
+ dst.select_option(*src.selected_options.map { |opt| opt['id'] })
63
+ when 'CrossReference'
64
+ dst.page_ids = src.page_ids
65
+ else
66
+ "won't copy"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,78 @@
1
+ module Eco::API::UseCases::GraphQL::Helpers
2
+ module Pages
3
+ # Native GraphQL re-expression of OozeSamples::Helpers::Creatable.
4
+ #
5
+ # Provides the "create a new entry (page) from a template" flow used by
6
+ # register-processing use cases. Mirrors the v2 Creatable structure
7
+ # (creating_new_page -> drafting_entry -> create_entry -> create_ooze) but
8
+ # talks to the native GraphQL stack (`graphql.pages.get_new` / `graphql.pages.create`)
9
+ # and respects `simulate?` for dry-run feedback.
10
+ #
11
+ # Included via the base-type guard: it can only be mixed into the native
12
+ # register case (Samples::Pages::Register::Base), which supplies the
13
+ # `create_ooze` hook + KPI counters it drives.
14
+ module Creatable
15
+ module InstanceMethods
16
+ private
17
+
18
+ # Draft a page from `template_id`, let the caller fill it, then persist it.
19
+ # @param draft_reference [String] human-readable label for logs.
20
+ # @param template_id [String] the template to build the draft from.
21
+ # @yield [draft] the built (still unsaved) draft page to mutate.
22
+ # @return [String, nil] the created page id, or nil on dry-run / failure.
23
+ def creating_new_page(draft_reference = 'new entry', template_id:)
24
+ page_id = nil
25
+ drafting_entry(template_id) do |draft|
26
+ yield(draft) if block_given?
27
+
28
+ if (page_id = create_entry(draft, reference: draft_reference))
29
+ log(:info) { "Page '#{page_id}' created successfully -- #{draft_reference}" }
30
+ elsif simulate?
31
+ log(:info) { "Simulated launch for #{draft_reference}" }
32
+ end
33
+ end
34
+ page_id
35
+ end
36
+
37
+ # @return [Object] a draft page of `template_id` (still not saved).
38
+ def drafting_entry(template_id)
39
+ raise ArgumentError, 'Expecting block, but not given' unless block_given?
40
+
41
+ draft = graphql.pages.get_new(template_id)
42
+ yield(draft)
43
+ end
44
+
45
+ # Does the actual creation of the entry (wrapped so a per-item error does
46
+ # not abort the whole run).
47
+ # @return [String, nil] the created page id, or nil.
48
+ def create_entry(draft, reference: "new entry from #{draft&.template_id}")
49
+ with_rescue(reference) do
50
+ if (result = create_ooze(draft, template_id: draft.template_id))
51
+ return result.page_id
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ class << self
58
+ def included(base)
59
+ super
60
+
61
+ validate_base_type!(base)
62
+ base.include(InstanceMethods)
63
+ end
64
+
65
+ def validate_base_type!(base)
66
+ return super if defined?(super)
67
+
68
+ register_base = Eco::API::UseCases::GraphQL::Samples::Pages::Register::Base
69
+ msg = "#{self} can only be included in #{register_base}"
70
+ msg << "\nCan't be included in #{base}"
71
+ raise LoadError, msg unless base <= register_base
72
+
73
+ true
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,114 @@
1
+ # rubocop:disable Lint/SymbolConversion, Style/QuotedSymbols
2
+ module Eco::API::UseCases::GraphQL::Helpers
3
+ module Pages
4
+ # Search-filter + time-math helpers, ported from OozeSamples::Helpers::Filters.
5
+ #
6
+ # The time/date/tag builders are pure and ported verbatim. #field_key_name's field branch
7
+ # is re-expressed with duck-typing (was a `V2::Page::Component` case) — see the note there.
8
+ # A `weekday` helper is added because the original relied on one defined outside Filters
9
+ # (undefined in lib/), which made the sunday/monday chain latent-broken.
10
+ module Filters
11
+ FILTER_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
12
+
13
+ def tags_filter(tags, any: true, negate: false)
14
+ tags = [tags].flatten.compact
15
+ return nil if tags.empty?
16
+ key, name = field_key_name(:tags)
17
+ {
18
+ "tags": tags,
19
+ "mode": any ? "any" : "all",
20
+ "negate": negate,
21
+ "key": key,
22
+ "name": name,
23
+ "type": "tag_filter"
24
+ }
25
+ end
26
+
27
+ def date_range_filter(from: nil, to: nil, key: :updated_at)
28
+ return nil unless from || to
29
+ key, name = field_key_name(key)
30
+ {
31
+ "relstart": "today",
32
+ "time_zone": "Pacific/Auckland",
33
+ "relative": false,
34
+ "key": key,
35
+ "name": name,
36
+ "type": "date_filter"
37
+ }.tap do |out|
38
+ out.merge!("lbound": to_date_filter(from)) if from
39
+ out.merge!("ubound": to_date_filter(to)) if to
40
+ end
41
+ end
42
+
43
+ def to_date_filter(date)
44
+ daystart(date).utc.strftime(FILTER_TIME_FORMAT)
45
+ end
46
+
47
+ def set_time(date, hour, min, sec)
48
+ Time.new(date.year, date.month, date.day, hour, min, sec)
49
+ end
50
+
51
+ def weeks(num)
52
+ num * days(7)
53
+ end
54
+
55
+ def days(num)
56
+ num * 60 * 60 * 24
57
+ end
58
+
59
+ def today
60
+ Date.today.to_time
61
+ end
62
+
63
+ # 0 = Sunday .. 6 = Saturday (Date#wday). Added: the original relied on a `weekday`
64
+ # helper defined outside Filters (not present in lib/), leaving the chain below broken.
65
+ def weekday(date)
66
+ (date.respond_to?(:to_date) ? date.to_date : date).wday
67
+ end
68
+
69
+ def sunday(date)
70
+ date + days(7 - weekday(date))
71
+ end
72
+
73
+ def midnight(date)
74
+ set_time(date, 23, 59, 59)
75
+ end
76
+
77
+ def daystart(date)
78
+ set_time(date, 0, 0, 0)
79
+ end
80
+
81
+ def previous_sunday(date)
82
+ midnight(sunday(date - weeks(1)))
83
+ end
84
+
85
+ def this_monday(date)
86
+ set_time(previous_sunday(date) + days(1), 0, 0, 0)
87
+ end
88
+
89
+ # Resolve a filter [key, name] pair for the register search.
90
+ #
91
+ # @note **Search-key gotcha:** the register search matches on the field DEFINITION key,
92
+ # not the field's name/label. For a data-field object we therefore prefer its backend
93
+ # reference key. The exact GraphQL accessor is duck-typed here (ref_backend -> key) and
94
+ # must be confirmed against the live field model before relying on field-scoped search
95
+ # filters (a wrong key silently matches 0 rows). See the CANS search-key note.
96
+ def field_key_name(value)
97
+ case value
98
+ when :updated_at then ["updated_at", "Last updated"]
99
+ when :created_at then ["created_at", "Page created"]
100
+ when :tags then ["tags", "Location Tags"]
101
+ else
102
+ if value.respond_to?(:ref_backend)
103
+ [value.ref_backend, (value.respond_to?(:label) ? value.label : nil)]
104
+ elsif value.respond_to?(:key)
105
+ [value.key, (value.respond_to?(:label) ? value.label : nil)]
106
+ else
107
+ [nil, nil]
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ # rubocop:enable Lint/SymbolConversion, Style/QuotedSymbols