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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +207 -0
- data/lib/eco/api/usecases/CLAUDE.md +78 -0
- data/lib/eco/api/usecases/default/pages.rb +30 -0
- data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
- data/lib/eco/api/usecases/graphql/compat/parity/comparison.rb +70 -0
- data/lib/eco/api/usecases/graphql/compat/parity/harness.rb +102 -0
- data/lib/eco/api/usecases/graphql/compat/parity/run_result.rb +96 -0
- data/lib/eco/api/usecases/graphql/compat.rb +11 -0
- data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
- data/lib/eco/api/usecases/graphql/helpers/location/command/end_points/optimizations.rb +4 -4
- data/lib/eco/api/usecases/graphql/helpers/pages/copying.rb +71 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/creatable.rb +78 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/filters.rb +114 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/ooze_handlers.rb +112 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/rescuable.rb +52 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/shortcuts.rb +186 -0
- data/lib/eco/api/usecases/graphql/helpers/pages/typed_fields_pairing.rb +303 -0
- data/lib/eco/api/usecases/graphql/helpers/pages.rb +21 -0
- data/lib/eco/api/usecases/graphql/helpers.rb +1 -0
- data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
- data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +1 -1
- data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/base.rb +181 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/migration_case.rb +132 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register/target_oozes_update_case.rb +163 -0
- data/lib/eco/api/usecases/graphql/samples/pages/register.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/base.rb +70 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/command_emitter.rb +139 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/builder.rb +126 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/format_map.rb +108 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build/parser.rb +98 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/csv_build.rb +17 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/applier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/drift_report.rb +104 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/loop.rb +155 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/recording_executor.rb +58 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/sync_readiness.rb +178 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy/verifier.rb +141 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template/deploy.rb +21 -0
- data/lib/eco/api/usecases/graphql/samples/pages/template.rb +11 -0
- data/lib/eco/api/usecases/graphql/samples/pages.rb +9 -0
- data/lib/eco/api/usecases/graphql/samples.rb +1 -0
- data/lib/eco/api/usecases/graphql.rb +1 -0
- data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
- data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +13 -3
- data/lib/eco/version.rb +1 -1
- metadata +52 -15
- data/.gitignore +0 -23
- data/.idea/.gitignore +0 -10
- data/.markdownlint.json +0 -4
- data/.rspec +0 -3
- data/.rubocop.yml +0 -103
- data/.ruby-version +0 -1
- data/.yardopts +0 -10
- data/Gemfile +0 -8
- data/Rakefile +0 -38
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|