eco-helpers 3.2.14 → 3.2.23
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 +221 -0
- data/lib/eco/api/usecases/default/pages.rb +30 -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 +223 -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/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/location/command/service/tree_update.rb +1 -1
- 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 +47 -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,155 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Deploy → verify → monitor orchestration (Phase 5).
|
|
4
|
+
#
|
|
5
|
+
# Ties the four offline-safe pieces into one pass:
|
|
6
|
+
# 1. APPLY — hand a gem `Diff::Deploy` plan to the `Applier` (dry-run by DEFAULT; live only
|
|
7
|
+
# behind an explicit `commit: true` + executor).
|
|
8
|
+
# 2. DRIFT — re-read the target and run a pre/post self-version diff, then compare the applied
|
|
9
|
+
# delta to the intended delta (`DriftReport`). Reports drift honestly.
|
|
10
|
+
# 3. VERIFY — run the post-deploy template doc through a `Verifier` (ecoportal-qa when present,
|
|
11
|
+
# else the null verifier).
|
|
12
|
+
# 4. MONITOR — optional sync-readiness scan of a register subset against the post-deploy template.
|
|
13
|
+
#
|
|
14
|
+
# Everything is OFFLINE against fixtures. The two seams that would touch a live API — applying the
|
|
15
|
+
# batch and re-reading the target — are injected as callables, so specs (and dry-runs) drive them
|
|
16
|
+
# with fixtures and a `RecordingExecutor`:
|
|
17
|
+
#
|
|
18
|
+
# loop = Deploy::Loop.new(
|
|
19
|
+
# plan: gem_deploy_plan,
|
|
20
|
+
# intended: source_version_diff, # the diff the plan was built from
|
|
21
|
+
# pre_doc: target_before_doc, # the target template BEFORE apply
|
|
22
|
+
# reread: -> { recording_executor.doc } # how to fetch the target AFTER apply (post-deploy doc)
|
|
23
|
+
# )
|
|
24
|
+
# result = loop.run(commit: false, executor: recording_executor, entries: register_entries)
|
|
25
|
+
#
|
|
26
|
+
# result.applied # => Applier::Outcome
|
|
27
|
+
# result.drift # => DriftReport (nil if pre_doc/reread not supplied)
|
|
28
|
+
# result.verdict # => Verifier::Verdict
|
|
29
|
+
# result.sync_readiness # => SyncReadiness (nil if no entries)
|
|
30
|
+
# result.ok? # => apply not blocked && drift match && verify pass
|
|
31
|
+
# result.report # => aggregate human summary
|
|
32
|
+
class Loop
|
|
33
|
+
# Aggregate outcome of one deploy→verify→monitor pass.
|
|
34
|
+
Result = Struct.new(:applied, :drift, :verdict, :sync_readiness, keyword_init: true) do
|
|
35
|
+
# Overall gate: the apply was not blocked, the drift (if checked) matched, and verify passed.
|
|
36
|
+
def ok?
|
|
37
|
+
applied_ok? && drift_ok? && verify_ok?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def applied_ok?
|
|
41
|
+
applied && applied.skipped_reason.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def drift_ok?
|
|
45
|
+
drift.nil? || drift.match?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_ok?
|
|
49
|
+
verdict.nil? || verdict.pass?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_h
|
|
53
|
+
{
|
|
54
|
+
ok: ok?,
|
|
55
|
+
applied: applied&.to_h,
|
|
56
|
+
drift: drift&.to_h,
|
|
57
|
+
verdict: verdict&.to_h,
|
|
58
|
+
sync_readiness: sync_readiness&.to_h
|
|
59
|
+
}.compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def report
|
|
63
|
+
[
|
|
64
|
+
"DEPLOY LOOP: #{ok? ? 'OK' : 'ATTENTION NEEDED'}",
|
|
65
|
+
applied_line,
|
|
66
|
+
drift&.report,
|
|
67
|
+
verdict&.report,
|
|
68
|
+
sync_readiness&.report
|
|
69
|
+
].compact.join("\n\n")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def applied_line
|
|
75
|
+
if applied.skipped_reason
|
|
76
|
+
"APPLY: BLOCKED — #{applied.skipped_reason}"
|
|
77
|
+
elsif applied.committed?
|
|
78
|
+
"APPLY: committed #{applied.commands.size} command(s)"
|
|
79
|
+
else
|
|
80
|
+
"APPLY: dry-run — #{applied.commands.size} command(s) would be sent"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param plan [Ecoportal::API::GraphQL::Diff::Deploy] the gem deploy plan.
|
|
86
|
+
# @param intended [#changes, nil] the source diff the plan was built from (for drift check).
|
|
87
|
+
# @param pre_doc [Hash, nil] the target template doc BEFORE apply (for drift check).
|
|
88
|
+
# @param reread [#call, nil] returns the target template doc AFTER apply (for drift + verify).
|
|
89
|
+
# @param target [#id, nil] the target model, forwarded to the Applier for a live apply.
|
|
90
|
+
# @param verifier [Verifier, nil] injected verifier; defaults to Verifier.default.
|
|
91
|
+
# @param diff_builder [#call, nil] `diff_builder.call(pre_doc, post_doc)` returns the applied
|
|
92
|
+
# self-version diff (must expose `#changes`). Defaults to the gem's `Diff::VersionDiff` — which
|
|
93
|
+
# lives in the (currently unreleased) gem Diff module, so it is resolved LAZILY at drift time,
|
|
94
|
+
# never at load. Injectable so offline callers/specs can supply their own diff builder.
|
|
95
|
+
def initialize(plan:, intended: nil, pre_doc: nil, reread: nil, target: nil, verifier: nil,
|
|
96
|
+
diff_builder: nil)
|
|
97
|
+
@plan = plan
|
|
98
|
+
@intended = intended
|
|
99
|
+
@pre_doc = pre_doc
|
|
100
|
+
@reread = reread
|
|
101
|
+
@target = target
|
|
102
|
+
@verifier = verifier || Verifier.default
|
|
103
|
+
@diff_builder = diff_builder
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Run the full pass.
|
|
107
|
+
#
|
|
108
|
+
# @param commit [Boolean] false = dry-run (default). true = live apply.
|
|
109
|
+
# @param executor [#execute_workflow_commands, nil] the mutation facade (live) or a
|
|
110
|
+
# RecordingExecutor (offline). Required for both commit AND for an offline drift check
|
|
111
|
+
# (the RecordingExecutor replays the batch to produce the post-deploy doc).
|
|
112
|
+
# @param allow_partial [Boolean] forwarded to the Applier / gem plan.
|
|
113
|
+
# @param entries [Array, nil] register subset for the sync-readiness monitor (optional).
|
|
114
|
+
# @return [Result]
|
|
115
|
+
def run(commit: false, executor: nil, allow_partial: false, entries: nil)
|
|
116
|
+
applied = Applier.new(@plan, target: @target).apply(
|
|
117
|
+
commit: commit, executor: executor, allow_partial: allow_partial
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
post_doc = applied.skipped_reason.nil? ? reread_post_doc : nil
|
|
121
|
+
drift = build_drift(post_doc)
|
|
122
|
+
verdict = post_doc ? @verifier.verify(post_doc) : nil
|
|
123
|
+
monitor = build_monitor(post_doc, entries)
|
|
124
|
+
|
|
125
|
+
Result.new(applied: applied, drift: drift, verdict: verdict, sync_readiness: monitor)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def reread_post_doc
|
|
131
|
+
@reread&.call
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_drift(post_doc)
|
|
135
|
+
return nil unless @intended && @pre_doc && post_doc
|
|
136
|
+
|
|
137
|
+
DriftReport.new(intended: @intended, applied: diff_builder.call(@pre_doc, post_doc))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The gem's Diff::VersionDiff is the default applied-diff builder. Resolved lazily so this file
|
|
141
|
+
# never forces the (unreleased) gem Diff module at load time.
|
|
142
|
+
def diff_builder
|
|
143
|
+
@diff_builder ||= lambda do |pre, post|
|
|
144
|
+
Ecoportal::API::GraphQL::Diff::VersionDiff.new(pre, post)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_monitor(post_doc, entries)
|
|
149
|
+
return nil if entries.nil? || Array(entries).empty?
|
|
150
|
+
|
|
151
|
+
SyncReadiness.new(template_doc: post_doc || @pre_doc, entries: entries)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Offline stand-in for a live workflow-command executor.
|
|
4
|
+
#
|
|
5
|
+
# Records every batch it is handed and (optionally) applies each command to an in-memory template
|
|
6
|
+
# doc via a caller-supplied `mutator`, so a deploy loop can compute a faithful POST-deploy snapshot
|
|
7
|
+
# WITHOUT any API call. This is what makes the pre/post self-version drift check runnable offline:
|
|
8
|
+
# the applied delta is derived from replaying the batch onto the pre-deploy doc.
|
|
9
|
+
#
|
|
10
|
+
# It satisfies BOTH executor contracts the gem may call:
|
|
11
|
+
# * `execute_workflow_commands(commands)` (gem Diff::Deploy#execute! positional)
|
|
12
|
+
# * `execute_workflow_commands(model, commands:)` (gem Builder::Page facade)
|
|
13
|
+
#
|
|
14
|
+
# rec = RecordingExecutor.new(doc: before_doc.dup, mutator: my_replayer)
|
|
15
|
+
# plan.execute!(rec)
|
|
16
|
+
# rec.batches # => [[cmd, cmd, ...]]
|
|
17
|
+
# rec.doc # => the mutated post-deploy doc (if a mutator was given)
|
|
18
|
+
class RecordingExecutor
|
|
19
|
+
attr_reader :batches, :doc
|
|
20
|
+
|
|
21
|
+
# @param doc [Hash, nil] a template/page doc to mutate as commands are replayed.
|
|
22
|
+
# @param mutator [#call, nil] `mutator.call(doc, command)` applies one command to `doc`
|
|
23
|
+
# in place (or returns the new doc). When nil, the doc is left untouched (record-only).
|
|
24
|
+
def initialize(doc: nil, mutator: nil)
|
|
25
|
+
@doc = doc
|
|
26
|
+
@mutator = mutator
|
|
27
|
+
@batches = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute_workflow_commands(model_or_commands, commands: nil)
|
|
31
|
+
batch = commands || model_or_commands
|
|
32
|
+
@batches << batch
|
|
33
|
+
replay(batch) if @mutator
|
|
34
|
+
record_response(batch)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Marker so the applier does not double-wrap this executor.
|
|
38
|
+
def arity_ok_for_positional_commands?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Every command across all recorded batches, flattened, in order.
|
|
43
|
+
def all_commands
|
|
44
|
+
@batches.flatten(1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def replay(batch)
|
|
50
|
+
Array(batch).each { |command| @doc = @mutator.call(@doc, command) || @doc }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def record_response(batch)
|
|
54
|
+
{ recorded: batch.size }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Sync-readiness monitoring.
|
|
4
|
+
#
|
|
5
|
+
# Given a register subset (its entries) and the register's ACTIVE template, report which entries
|
|
6
|
+
# CAN sync vs which cannot — where "can sync" means: every field the template marks REQUIRED is
|
|
7
|
+
# present on the entry with a non-empty value AND typed correctly (the entry field's type matches
|
|
8
|
+
# the template field's type). This is the pre-flight an integration runs before pushing entries
|
|
9
|
+
# downstream, and the drift-detection the customer-facing integrations mandate needs.
|
|
10
|
+
#
|
|
11
|
+
# Operates on raw docs (template doc + entry docs) so it is session-less and offline-runnable:
|
|
12
|
+
# * template doc: the JSON the gem returns for a template (stages/sections/dataFields).
|
|
13
|
+
# * entry docs: each register entry as { 'id' =>, 'fields' => [ {id/label/type/value}, ... ] }
|
|
14
|
+
# (or anything exposing the same shape). A field is matched to a template field by id first,
|
|
15
|
+
# then by label — mirroring how the pairing engine falls back.
|
|
16
|
+
#
|
|
17
|
+
# monitor = SyncReadiness.new(template_doc: tpl, entries: [e1, e2, ...])
|
|
18
|
+
# monitor.ready # => [EntryReadiness(ready: true), ...]
|
|
19
|
+
# monitor.not_ready # => [EntryReadiness(ready: false, ...), ...]
|
|
20
|
+
# monitor.summary # => { total:, ready:, not_ready: }
|
|
21
|
+
# monitor.report # => human summary
|
|
22
|
+
class SyncReadiness
|
|
23
|
+
# Per-entry readiness verdict.
|
|
24
|
+
EntryReadiness = Struct.new(:entry_id, :ready, :missing_required, :type_mismatches,
|
|
25
|
+
keyword_init: true) do
|
|
26
|
+
def ready?
|
|
27
|
+
ready ? true : false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reasons
|
|
31
|
+
out = Array(missing_required).map { |m| "missing required '#{m}'" }
|
|
32
|
+
Array(type_mismatches).each do |m|
|
|
33
|
+
out << "type mismatch '#{m[:label]}' (want #{m[:expected]}, got #{m[:actual]})"
|
|
34
|
+
end
|
|
35
|
+
out
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{ entry_id: entry_id, ready: ready?, missing_required: Array(missing_required),
|
|
40
|
+
type_mismatches: Array(type_mismatches) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_reader :entries
|
|
45
|
+
|
|
46
|
+
# @param template_doc [Hash] the active template doc.
|
|
47
|
+
# @param entries [Array<Hash,#id>] register entry docs to assess.
|
|
48
|
+
def initialize(template_doc:, entries:)
|
|
49
|
+
@template_doc = template_doc || {}
|
|
50
|
+
@entries = Array(entries)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def verdicts
|
|
54
|
+
@verdicts ||= @entries.map { |entry| assess(entry) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ready
|
|
58
|
+
verdicts.select(&:ready?)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def not_ready
|
|
62
|
+
verdicts.reject(&:ready?)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def summary
|
|
66
|
+
{ total: verdicts.size, ready: ready.size, not_ready: not_ready.size }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{ summary: summary, entries: verdicts.map(&:to_h) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def report
|
|
74
|
+
lines = ["SYNC READINESS: #{summary[:ready]}/#{summary[:total]} entries ready"]
|
|
75
|
+
not_ready.each do |v|
|
|
76
|
+
lines << " * entry #{v.entry_id}: NOT READY"
|
|
77
|
+
v.reasons.each { |r| lines << " - #{r}" }
|
|
78
|
+
end
|
|
79
|
+
lines.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# The template fields, indexed by id and by (downcased) label, carrying type + required.
|
|
85
|
+
def template_fields
|
|
86
|
+
@template_fields ||= begin
|
|
87
|
+
out = { by_id: {}, by_label: {} }
|
|
88
|
+
each_template_field do |f|
|
|
89
|
+
spec = { id: f['id'], label: f['label'], type: field_type(f), required: !!f['required'] }
|
|
90
|
+
out[:by_id][f['id']] = spec if f['id']
|
|
91
|
+
out[:by_label][label_key(f['label'])] = spec if f['label']
|
|
92
|
+
end
|
|
93
|
+
out
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def required_fields
|
|
98
|
+
@required_fields ||= template_fields[:by_id].values.select { |s| s[:required] }.
|
|
99
|
+
+(template_fields[:by_label].values.select { |s| s[:required] }).
|
|
100
|
+
uniq { |s| s[:id] || s[:label] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def assess(entry)
|
|
104
|
+
entry_fields = index_entry_fields(entry)
|
|
105
|
+
missing = []
|
|
106
|
+
mismatches = []
|
|
107
|
+
|
|
108
|
+
required_fields.each do |spec|
|
|
109
|
+
efield = entry_fields[:by_id][spec[:id]] || entry_fields[:by_label][label_key(spec[:label])]
|
|
110
|
+
if efield.nil? || value_empty?(efield[:value])
|
|
111
|
+
missing << spec[:label]
|
|
112
|
+
elsif spec[:type] && efield[:type] && spec[:type] != efield[:type]
|
|
113
|
+
mismatches << { label: spec[:label], expected: spec[:type], actual: efield[:type] }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
EntryReadiness.new(
|
|
118
|
+
entry_id: entry_id(entry),
|
|
119
|
+
ready: missing.empty? && mismatches.empty?,
|
|
120
|
+
missing_required: missing,
|
|
121
|
+
type_mismatches: mismatches
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def index_entry_fields(entry)
|
|
126
|
+
out = { by_id: {}, by_label: {} }
|
|
127
|
+
entry_field_docs(entry).each do |f|
|
|
128
|
+
spec = { id: f['id'] || f[:id], label: f['label'] || f[:label],
|
|
129
|
+
type: field_type(f), value: f['value'] || f[:value] }
|
|
130
|
+
out[:by_id][spec[:id]] = spec if spec[:id]
|
|
131
|
+
out[:by_label][label_key(spec[:label])] = spec if spec[:label]
|
|
132
|
+
end
|
|
133
|
+
out
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def entry_field_docs(entry)
|
|
137
|
+
return Array(entry['fields'] || entry[:fields]) if entry.is_a?(Hash)
|
|
138
|
+
return entry.components.to_a if entry.respond_to?(:components)
|
|
139
|
+
|
|
140
|
+
[]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def entry_id(entry)
|
|
144
|
+
return entry['id'] || entry[:id] if entry.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
entry.respond_to?(:id) ? entry.id : nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def each_template_field(&block)
|
|
150
|
+
Array(@template_doc['stages']).each do |st|
|
|
151
|
+
Array(st['sections']).each do |sec|
|
|
152
|
+
fields_of(sec).each(&block)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fields_of(section)
|
|
158
|
+
Array(section['dataFields']) + Array(section['leftDataFields']) + Array(section['rightDataFields'])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def field_type(field)
|
|
162
|
+
field['__typename'] || field['type'] || field[:type]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def label_key(label)
|
|
166
|
+
label.to_s.strip.downcase
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def value_empty?(value)
|
|
170
|
+
return true if value.nil?
|
|
171
|
+
return value.strip.empty? if value.is_a?(String)
|
|
172
|
+
return value.empty? if value.respond_to?(:empty?)
|
|
173
|
+
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
module Deploy
|
|
3
|
+
# Pluggable post-deploy verification interface.
|
|
4
|
+
#
|
|
5
|
+
# The deploy loop wants to run the freshly-deployed template through a check framework (ideally the
|
|
6
|
+
# `ecoportal-qa` gem's check runner). But `ecoportal-qa` is NOT a hard dependency of eco-helpers,
|
|
7
|
+
# so we do not `require` it here. Instead the loop calls a small `Verifier` interface; a concrete
|
|
8
|
+
# verifier is injected. When `ecoportal-qa` IS on the load path, `QaVerifier` wires it in; when it
|
|
9
|
+
# isn't, `NullVerifier` (always-pass, records that qa was unavailable) keeps the loop runnable
|
|
10
|
+
# offline.
|
|
11
|
+
#
|
|
12
|
+
# A verifier receives the post-deploy template doc and returns a `Verdict`.
|
|
13
|
+
#
|
|
14
|
+
# verdict = Verifier.default.verify(post_deploy_doc)
|
|
15
|
+
# verdict.pass? # => true/false
|
|
16
|
+
# verdict.findings # => [ {severity:, message:}, ... ]
|
|
17
|
+
class Verifier
|
|
18
|
+
# A verification verdict. `pass?` is the gate; `findings` carries per-check detail.
|
|
19
|
+
Verdict = Struct.new(:pass, :findings, :verifier, keyword_init: true) do
|
|
20
|
+
def pass?
|
|
21
|
+
pass ? true : false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failures
|
|
25
|
+
Array(findings).select { |f| %i[error fail].include?((f[:severity] || f['severity'])&.to_sym) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{ pass: pass?, verifier: verifier, findings: Array(findings) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def report
|
|
33
|
+
head = "VERIFY (#{verifier}): #{pass? ? 'PASS' : 'FAIL'}"
|
|
34
|
+
return head if Array(findings).empty?
|
|
35
|
+
|
|
36
|
+
([head] + Array(findings).map do |f|
|
|
37
|
+
" - [#{f[:severity] || f['severity']}] #{f[:message] || f['message']}"
|
|
38
|
+
end).join("\n")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The verifier the loop uses when none is injected: qa if available, else the null verifier.
|
|
43
|
+
def self.default
|
|
44
|
+
qa_available? ? QaVerifier.new : NullVerifier.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Is the ecoportal-qa check framework loadable?
|
|
48
|
+
def self.qa_available?
|
|
49
|
+
return @qa_available unless @qa_available.nil?
|
|
50
|
+
|
|
51
|
+
@qa_available = begin
|
|
52
|
+
require 'ecoportal/qa'
|
|
53
|
+
defined?(::Ecoportal::QA) ? true : false
|
|
54
|
+
rescue LoadError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reset the memoised availability probe (used by specs that stub the load path).
|
|
60
|
+
def self.reset_qa_probe!
|
|
61
|
+
@qa_available = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Subclass / duck-type contract: return a Verdict for the given template doc.
|
|
65
|
+
def verify(_template_doc)
|
|
66
|
+
raise NotImplementedError, "#{self.class} must implement #verify(template_doc)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Always-pass verifier used when no real check framework is wired in. It is honest about it:
|
|
70
|
+
# the verdict records that qa was unavailable so a report never falsely claims "qa verified".
|
|
71
|
+
class NullVerifier < Verifier
|
|
72
|
+
def verify(_template_doc)
|
|
73
|
+
Verdict.new(
|
|
74
|
+
pass: true,
|
|
75
|
+
verifier: 'null',
|
|
76
|
+
findings: [{ severity: :info, message: 'ecoportal-qa not available; no checks run' }]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Adapter onto the ecoportal-qa check runner. Kept dependency-free at load time — it only touches
|
|
82
|
+
# `Ecoportal::QA` inside #verify, so requiring this file never forces the qa gem. The exact qa
|
|
83
|
+
# runner API is intentionally accessed through a single seam (`run_qa`) so it is easy to correct
|
|
84
|
+
# once the qa public surface stabilises (see TODO).
|
|
85
|
+
#
|
|
86
|
+
# TODO(qa-integration): the qa check-runner public API is still moving (see the qa repo's
|
|
87
|
+
# PHASE3-SCOPE.md). This adapter assumes a `check_set.run(doc)`-style entry returning objects
|
|
88
|
+
# responding to `#pass?` and `#findings`. Confirm and adjust `run_qa` when qa is pinned as a dep.
|
|
89
|
+
class QaVerifier < Verifier
|
|
90
|
+
def initialize(check_set: nil)
|
|
91
|
+
super()
|
|
92
|
+
@check_set = check_set
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def verify(template_doc)
|
|
96
|
+
result = run_qa(template_doc)
|
|
97
|
+
Verdict.new(pass: result_pass?(result), verifier: 'ecoportal-qa', findings: result_findings(result))
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
Verdict.new(pass: false, verifier: 'ecoportal-qa',
|
|
100
|
+
findings: [{ severity: :error, message: "qa verification raised: #{e.message}" }])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# SINGLE integration seam — the one place that knows the qa runner shape.
|
|
106
|
+
def run_qa(template_doc)
|
|
107
|
+
set = @check_set || default_check_set
|
|
108
|
+
set.run(template_doc)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def default_check_set
|
|
112
|
+
raise NotImplementedError, 'no qa check_set injected and no default wired' unless defined?(::Ecoportal::QA)
|
|
113
|
+
|
|
114
|
+
# Placeholder: the concrete constructor is confirmed once qa is a pinned dependency.
|
|
115
|
+
if ::Ecoportal::QA.respond_to?(:default_check_set)
|
|
116
|
+
::Ecoportal::QA.default_check_set
|
|
117
|
+
else
|
|
118
|
+
(raise NotImplementedError,
|
|
119
|
+
'qa default check set not available')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def result_pass?(result)
|
|
124
|
+
return result.pass? if result.respond_to?(:pass?)
|
|
125
|
+
|
|
126
|
+
Array(result).all? { |r| r.respond_to?(:pass?) ? r.pass? : true }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def result_findings(result)
|
|
130
|
+
source = result.respond_to?(:findings) ? result.findings : result
|
|
131
|
+
Array(source).map do |f|
|
|
132
|
+
next f if f.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
{ severity: (f.respond_to?(:severity) ? f.severity : :info),
|
|
135
|
+
message: (f.respond_to?(:message) ? f.message : f.to_s) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages::Template
|
|
2
|
+
# Phase 5: deploy → verify → monitor loop for template (workflow) changes.
|
|
3
|
+
#
|
|
4
|
+
# Consumes a gem `Ecoportal::API::GraphQL::Diff::Deploy` command batch (from
|
|
5
|
+
# `Diff::Deploy.from_versions(...)`), dry-run applies it (live behind an explicit `commit:` flag),
|
|
6
|
+
# confirms the applied delta matches the intended delta via a pre/post self-version diff
|
|
7
|
+
# (`Diff::VersionDiff`), verifies the result through a pluggable `Verifier` (ecoportal-qa when
|
|
8
|
+
# present), and reports register sync-readiness against the deployed template.
|
|
9
|
+
#
|
|
10
|
+
# All components are session-less and offline-runnable against fixtures — the live seams (apply /
|
|
11
|
+
# re-read) are injected. See each file's header. Dry-run is the DEFAULT.
|
|
12
|
+
module Deploy
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative 'deploy/applier'
|
|
17
|
+
require_relative 'deploy/recording_executor'
|
|
18
|
+
require_relative 'deploy/drift_report'
|
|
19
|
+
require_relative 'deploy/verifier'
|
|
20
|
+
require_relative 'deploy/sync_readiness'
|
|
21
|
+
require_relative 'deploy/loop'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Eco::API::UseCases::GraphQL::Samples::Pages
|
|
2
|
+
# Template (workflow) build-from-scratch + (later) diff-and-update samples.
|
|
3
|
+
# See the ecoportal-api-graphql repo's internal docs.
|
|
4
|
+
module Template
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative 'template/command_emitter'
|
|
9
|
+
require_relative 'template/base'
|
|
10
|
+
require_relative 'template/csv_build'
|
|
11
|
+
require_relative 'template/deploy'
|
|
@@ -224,6 +224,10 @@ class Eco::API::UseCases::OozeSamples::OozeBaseCase < Eco::API::Common::Loaders:
|
|
|
224
224
|
return if options.dig(:feedback, :only_stats)
|
|
225
225
|
return unless (patch = (patch_doc(entry) || {})['page'])
|
|
226
226
|
|
|
227
|
+
# Affirmative dry-run line so an empty/blank page no longer reads as a silent no-op.
|
|
228
|
+
# A create body carries :templateId (CreateFromTemplateInput); anything else is an update.
|
|
229
|
+
verb = patch.is_a?(Hash) && patch[:templateId] ? 'create' : 'update'
|
|
230
|
+
log(:info) { "[dry-run] would #{verb} #{object_reference(entry)} with:" }
|
|
227
231
|
pp patch
|
|
228
232
|
end
|
|
229
233
|
|
|
@@ -54,7 +54,10 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
|
|
|
54
54
|
return unless (pending = queue_shift(ooze_id))
|
|
55
55
|
|
|
56
56
|
update_ooze(pending).tap do |result|
|
|
57
|
-
|
|
57
|
+
# Duck-type, not is_a?: the GraphQL Compat::Response responds to success?/status but is
|
|
58
|
+
# NOT an Ecoportal::API::Common::Response, so is_a? silently skipped every GraphQL update
|
|
59
|
+
# (updated/failed stuck at 0). false/nil returns (dry-run / no-op) still fall through.
|
|
60
|
+
if result.respond_to?(:success?)
|
|
58
61
|
if result.success?
|
|
59
62
|
@updated_oozes += 1
|
|
60
63
|
else
|
|
@@ -105,7 +108,13 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
|
|
|
105
108
|
def enqueue(object)
|
|
106
109
|
return unless object.respond_to?(:id)
|
|
107
110
|
|
|
108
|
-
|
|
111
|
+
# Accept v2 Page/PageStage, or any duck-typed entry that carries change-tracking
|
|
112
|
+
# (the GraphQL compat page responds to #dirty? and #as_update). The strict v2-class
|
|
113
|
+
# guard otherwise raised for GraphQL pages under OozeRedirect.
|
|
114
|
+
queueable = object.is_a?(Ecoportal::API::V2::Page) ||
|
|
115
|
+
object.is_a?(Ecoportal::API::V2::Pages::PageStage) ||
|
|
116
|
+
(object.respond_to?(:dirty?) && object.respond_to?(:as_update))
|
|
117
|
+
unless queueable
|
|
109
118
|
msg = "Queuing is just for entries. Expecting Page or PageStage. Given: #{object.class}"
|
|
110
119
|
raise ArgumentError, msg
|
|
111
120
|
end
|
|
@@ -182,7 +191,8 @@ class Eco::API::UseCases::OozeSamples::RegisterUpdateCase < Eco::API::UseCases::
|
|
|
182
191
|
def update_oozes(batched_oozes = batch_queue)
|
|
183
192
|
batched_oozes.each do |ooze|
|
|
184
193
|
update_ooze(ooze).tap do |result|
|
|
185
|
-
|
|
194
|
+
# Duck-type, not is_a? — see #before_loading_new_target (GraphQL Compat::Response).
|
|
195
|
+
if result.respond_to?(:success?)
|
|
186
196
|
if result.success?
|
|
187
197
|
@updated_oozes += 1
|
|
188
198
|
else
|
data/lib/eco/version.rb
CHANGED