ecoportal-api-graphql 1.3.9 → 1.3.11

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/code/diff_pairing_engine.md +243 -0
  3. data/.ai-assistance/code/graphql_domain_knowledge.md +20 -10
  4. data/.ai-assistance/code/template_diff_pairing_domain.md +175 -0
  5. data/.ai-assistance/code/workflow-command-guide.md +28 -0
  6. data/.ai-assistance/projects/TODO.md +21 -0
  7. data/.ai-assistance/projects/ooze-graphql-native-migration/INVENTORY.md +136 -0
  8. data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +6 -1
  9. data/.ai-assistance/projects/qa-services-delivery/DECISIONS.md +93 -0
  10. data/.ai-assistance/projects/qa-services-delivery/INTENT.md +76 -0
  11. data/.ai-assistance/projects/qa-services-delivery/PHASE3-SCOPE.md +115 -0
  12. data/.ai-assistance/projects/qa-services-delivery/ROADMAP.md +99 -0
  13. data/.ai-assistance/projects/qa-services-delivery/TODO.md +81 -0
  14. data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +77 -0
  15. data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +97 -0
  16. data/.ai-assistance/projects/template-diff-deploy/INTENT.md +12 -0
  17. data/.ai-assistance/projects/template-diff-deploy/TODO.md +9 -0
  18. data/.ai-assistance/projects/template-maintenance/PHASE0-FINDINGS.md +93 -0
  19. data/.ai-assistance/projects/template-maintenance/README.md +14 -0
  20. data/CHANGELOG.md +116 -0
  21. data/docs/worklog.md +408 -0
  22. data/ecoportal-api-graphql.gemspec +1 -1
  23. data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
  24. data/lib/ecoportal/api/graphql/builder/template_builder.rb +174 -0
  25. data/lib/ecoportal/api/graphql/builder.rb +17 -16
  26. data/lib/ecoportal/api/graphql/diff/change.rb +59 -0
  27. data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +329 -0
  28. data/lib/ecoportal/api/graphql/diff/cross_object_diff.rb +165 -0
  29. data/lib/ecoportal/api/graphql/diff/deploy.rb +121 -0
  30. data/lib/ecoportal/api/graphql/diff/id_resolver.rb +64 -0
  31. data/lib/ecoportal/api/graphql/diff/pairing/candidate.rb +32 -0
  32. data/lib/ecoportal/api/graphql/diff/pairing/engine.rb +173 -0
  33. data/lib/ecoportal/api/graphql/diff/pairing/ledger.rb +119 -0
  34. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +104 -0
  35. data/lib/ecoportal/api/graphql/diff/strategy.rb +113 -0
  36. data/lib/ecoportal/api/graphql/diff/version_diff.rb +332 -0
  37. data/lib/ecoportal/api/graphql/diff.rb +34 -0
  38. data/lib/ecoportal/api/graphql/fragment/location_draft.rb +53 -53
  39. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
  40. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
  41. data/lib/ecoportal/api/graphql/mutation/action/archive.rb +1 -1
  42. data/lib/ecoportal/api/graphql/mutation/action/create.rb +1 -1
  43. data/lib/ecoportal/api/graphql/mutation/action/update.rb +1 -1
  44. data/lib/ecoportal/api/graphql/mutation/contractor_entity/create.rb +1 -1
  45. data/lib/ecoportal/api/graphql/mutation/contractor_entity/destroy.rb +1 -1
  46. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update.rb +1 -1
  47. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +1 -1
  48. data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +1 -1
  49. data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +1 -1
  50. data/lib/ecoportal/api/graphql/query/location_structure/draft.rb +62 -62
  51. data/lib/ecoportal/api/graphql/query/location_structure.rb +61 -61
  52. data/lib/ecoportal/api/graphql.rb +139 -138
  53. data/lib/ecoportal/api/graphql_version.rb +1 -1
  54. data/tests/dump_template_model.rb +90 -0
  55. data/tests/validate_queries.rb +127 -0
  56. metadata +32 -3
@@ -0,0 +1,332 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Diff
5
+ # Self-version diff: two snapshots of the SAME page/template (same Mongo ids).
6
+ #
7
+ # Because the object keeps its ids across versions, pairing is EXACT (match by id) — no
8
+ # equivalence matching needed. This is the "what did this ticket change to the UAT template"
9
+ # tool, and the basis of a portable changelog ("commit") that — once id-translated through a
10
+ # pairing map — can be replayed onto PROD. Cross-object diff (UAT<->PROD, page<->template)
11
+ # is a different, harder problem handled by the pairing engine (not here).
12
+ #
13
+ # Operates on the raw page/template doc (the JSON the gem returns for a page/template), so it
14
+ # is a shared, session-less capability usable by admin/troubleshooting/integration/QA alike.
15
+ #
16
+ # diff = Diff::VersionDiff.new(before_doc, after_doc)
17
+ # diff.changes # => [Change, ...]
18
+ # diff.summary # => { added:, removed:, changed:, moved:, total: }
19
+ # diff.changelog # => ["~ field 'Severity' ...", ...]
20
+ #
21
+ # MODALITIES — pass a `Diff::Strategy` to select a scope / move-sensitivity. The default
22
+ # strategy (id-paired, structural, move-aware, changelog) reproduces the pre-Phase-4 behaviour
23
+ # exactly, so `VersionDiff.new(before, after).changes` is unchanged. A narrower scope
24
+ # (`:config_only`, `:data_migration`) or a move-insensitive strategy filters the SAME computed
25
+ # change-set — the pairing is still by-id here (`VersionDiff` is the `:id` pairing front-end).
26
+ # Cross-object pairings (:genome/:type_label/:assisted) are handled by `CrossObjectDiff`.
27
+ class VersionDiff
28
+ # Typed field-configuration properties that this diff EMITS, keyed by the field's
29
+ # `__typename`. Each entry maps a `byType` input key (the sub-hash the schema's
30
+ # `editFieldConfiguration.byType` accepts) to the list of doc property names that make up
31
+ # that sub-hash. Only pairs where the READ property name matches the WRITE (byType) input
32
+ # key EXACTLY are listed here — this diff never fabricates a config change it cannot map
33
+ # back to a real command input.
34
+ #
35
+ # Deliberately conservative: e.g. PlainText carries `multiline` in the read model but the
36
+ # `plainText` byType input has no `multiline` key (it exposes `maxLength`), so PlainText
37
+ # config is NOT emitted here (it would be unmappable). Likewise `required` has no
38
+ # editFieldConfiguration key at all, so a `required` flip is not a config change we emit.
39
+ BYTYPE_CONFIG = {
40
+ 'Gauge' => { gauge: %w[max] },
41
+ 'Select' => { select: %w[dataType multiple flat other otherDesc] },
42
+ 'Date' => { date: %w[showTime pastOnly todayButton] }
43
+ }.freeze
44
+
45
+ def initialize(before_doc, after_doc, strategy: Strategy.default)
46
+ @before = before_doc || {}
47
+ @after = after_doc || {}
48
+ @strategy = strategy || Strategy.default
49
+ end
50
+
51
+ # The change-set, filtered by the strategy's scope + move-sensitivity. The full set is
52
+ # computed once (`all_changes`); the strategy decides what survives. The default strategy
53
+ # keeps everything (the historical behaviour).
54
+ def changes
55
+ @changes ||= @strategy.filter(all_changes)
56
+ end
57
+
58
+ def summary
59
+ by_op = changes.group_by(&:op)
60
+ {
61
+ added: by_op.fetch(:added, []).size,
62
+ removed: by_op.fetch(:removed, []).size,
63
+ changed: by_op.fetch(:changed, []).size,
64
+ moved: by_op.fetch(:moved, []).size,
65
+ total: changes.size
66
+ }
67
+ end
68
+
69
+ def changelog
70
+ changes.map(&:description)
71
+ end
72
+
73
+ def to_h
74
+ { summary: summary, changes: changes.map(&:to_h) }
75
+ end
76
+
77
+ private
78
+
79
+ # The complete id-paired change-set (all kinds, all ops) before any strategy filtering.
80
+ def all_changes
81
+ stage_changes + section_changes + field_changes + option_changes +
82
+ field_config_changes + gauge_stop_changes
83
+ end
84
+
85
+ # Generic set-diff over an index {id => info}. Emits added/removed (label/path pulled from
86
+ # info via the given procs) and delegates matched-id comparison to the block.
87
+ #
88
+ # `parent:` (optional) is a proc mapping an entry's info to the id of its OWNING node — the
89
+ # section id for a field, the stage id for a section. Recorded on ADDED changes as
90
+ # `parent_id` so the synthesiser can wire the structural back-ref (addField.sectionId /
91
+ # addStageSection.stageId), threading through a placeholder when the parent is created in
92
+ # the same batch. nil (default) means no parent id is known/recorded — the historical
93
+ # behaviour, unchanged.
94
+ def diff_index(kind, before_idx, after_idx, label:, path:, parent: nil, owner: nil, &compare)
95
+ procs = { label: label, path: path, parent: parent, owner: owner }
96
+ removed = (before_idx.keys - after_idx.keys).map { |id| removed_change(kind, id, before_idx[id], procs) }
97
+ added = (after_idx.keys - before_idx.keys).map { |id| added_change(kind, id, after_idx[id], procs) }
98
+ matched = (before_idx.keys & after_idx.keys).flat_map { |id| Array(compare.call(id, before_idx[id], after_idx[id])) }
99
+ removed + added + matched
100
+ end
101
+
102
+ def removed_change(kind, id, info, procs)
103
+ Change.new(op: :removed, kind: kind, id: id, label: procs[:label].call(info), path: procs[:path].call(info))
104
+ end
105
+
106
+ def added_change(kind, id, info, procs)
107
+ Change.new(op: :added, kind: kind, id: id, label: procs[:label].call(info), path: procs[:path].call(info),
108
+ parent_id: procs[:parent]&.call(info), owner_id: procs[:owner]&.call(info))
109
+ end
110
+
111
+ def stage_changes
112
+ diff_index(:stage, index_stages(@before), index_stages(@after),
113
+ label: ->(s) { s['name'] }, path: ->(s) { s['name'] }) do |id, b, a|
114
+ cmp = []
115
+ cmp << changed(:stage, id, a['name'], a['name'], 'name', b['name'], a['name']) if b['name'] != a['name']
116
+ cmp << moved(:stage, id, a['name'], 'ordering', b['ordering'], a['ordering']) if b['ordering'] != a['ordering']
117
+ cmp
118
+ end
119
+ end
120
+
121
+ def section_changes
122
+ diff_index(:section, index_sections(@before), index_sections(@after),
123
+ label: ->(s) { s[:heading] }, path: ->(s) { s[:path] },
124
+ parent: ->(s) { s[:stage_id] }) do |id, b, a|
125
+ cmp = []
126
+ cmp << changed(:section, id, a[:heading], a[:path], 'heading', b[:heading], a[:heading]) if b[:heading] != a[:heading]
127
+ cmp << moved(:section, id, a[:heading], 'stage', b[:stage], a[:stage]) if b[:stage] != a[:stage]
128
+ cmp
129
+ end
130
+ end
131
+
132
+ def field_changes
133
+ diff_index(:field, index_fields(@before), index_fields(@after),
134
+ label: ->(f) { f[:label] }, path: ->(f) { f[:path] },
135
+ parent: ->(f) { f[:section_id] }, owner: ->(f) { f[:stage_id] }, &method(:compare_field))
136
+ end
137
+
138
+ def compare_field(id, before, after)
139
+ cmp = []
140
+ cmp << changed(:field, id, after[:label], after[:path], 'label', before[:label], after[:label]) if before[:label] != after[:label]
141
+ cmp << changed(:field, id, after[:label], after[:path], 'type', before[:type], after[:type]) if before[:type] != after[:type]
142
+ cmp << moved(:field, id, after[:label], 'section', before[:section], after[:section]) if before[:section] != after[:section]
143
+ cmp
144
+ end
145
+
146
+ def option_changes
147
+ bf = index_fields(@before)
148
+ af = index_fields(@after)
149
+ (bf.keys & af.keys).flat_map do |fid|
150
+ options_diff(fid, af[fid][:path], bf[fid][:field], af[fid][:field])
151
+ end
152
+ end
153
+
154
+ def options_diff(field_id, path, before_field, after_field)
155
+ diff_index(:option, index_options(before_field), index_options(after_field),
156
+ label: ->(o) { o[:label] }, path: ->(_o) { path }) do |k, b, a|
157
+ cmp = []
158
+ cmp << changed(:option, k, a[:label], path, 'label', b[:label], a[:label], field_id) if b[:label] != a[:label]
159
+ cmp << changed(:option, k, a[:label], path, 'weight', b[:weight], a[:weight], field_id) if b[:weight] != a[:weight]
160
+ cmp
161
+ end.each { |c| c.parent_id ||= field_id }
162
+ end
163
+
164
+ # --- typed field configuration (byType) ---------------------------
165
+
166
+ # Per-type field-configuration changes (the `byType` body of editFieldConfiguration).
167
+ # Matched by retained field id (self-version). For each field type in BYTYPE_CONFIG, emit
168
+ # a `:field_config` change per changed property, carrying the byType key so the synthesiser
169
+ # can nest it under the right sub-hash. Properties absent from BYTYPE_CONFIG are ignored
170
+ # here (unmappable → never fabricated).
171
+ def field_config_changes
172
+ bf = index_fields(@before)
173
+ af = index_fields(@after)
174
+ (bf.keys & af.keys).flat_map do |fid|
175
+ config_diff_for_field(fid, bf[fid], af[fid])
176
+ end
177
+ end
178
+
179
+ def config_diff_for_field(field_id, before_info, after_info)
180
+ type = after_info[:type]
181
+ return [] if before_info[:type] != type # a type change is handled/blocked elsewhere
182
+
183
+ (BYTYPE_CONFIG[type] || {}).flat_map do |by_type, props|
184
+ props.filter_map do |prop|
185
+ bv = before_info[:field][prop]
186
+ av = after_info[:field][prop]
187
+ next if bv == av
188
+
189
+ config_change(field_id, after_info, by_type, prop, bv, av)
190
+ end
191
+ end
192
+ end
193
+
194
+ def config_change(field_id, after_info, by_type, prop, before, after) # rubocop:disable Metrics/ParameterLists
195
+ Change.new(op: :changed, kind: :field_config, id: field_id, label: after_info[:label],
196
+ path: after_info[:path], attribute: prop, before: before, after: after,
197
+ parent_id: field_id, by_type: by_type)
198
+ end
199
+
200
+ # --- gauge stops ---------------------------------------------------
201
+
202
+ # Gauge fields carry an ordered list of `stops` ({ id, threshold, color }). Diff them per
203
+ # gauge field, matched by retained stop id (self-version). Added / removed / changed
204
+ # (threshold or color). parent_id = the gauge field id (the stop commands address it).
205
+ def gauge_stop_changes
206
+ bf = index_fields(@before)
207
+ af = index_fields(@after)
208
+ (bf.keys & af.keys).flat_map do |fid|
209
+ next [] unless af[fid][:type] == 'Gauge' && bf[fid][:type] == 'Gauge'
210
+
211
+ stops_diff(fid, af[fid][:path], bf[fid][:field], af[fid][:field])
212
+ end
213
+ end
214
+
215
+ # Explicit add/remove/change diff (NOT the generic diff_index): an ADDED stop must carry
216
+ # its full spec (threshold + color) on the Change so addGaugeFieldStop can be built without
217
+ # re-reading the doc.
218
+ def stops_diff(field_id, path, before_field, after_field)
219
+ before_idx = index_stops(before_field)
220
+ after_idx = index_stops(after_field)
221
+
222
+ removed = (before_idx.keys - after_idx.keys).map do |sid|
223
+ Change.new(op: :removed, kind: :gauge_stop, id: sid, label: before_idx[sid][:label],
224
+ path: path, parent_id: field_id)
225
+ end
226
+ added = (after_idx.keys - before_idx.keys).map do |sid|
227
+ s = after_idx[sid]
228
+ Change.new(op: :added, kind: :gauge_stop, id: sid, label: s[:label], path: path,
229
+ parent_id: field_id, after: { threshold: s[:threshold], color: s[:color] })
230
+ end
231
+ changed = (before_idx.keys & after_idx.keys).flat_map do |sid|
232
+ stop_attr_changes(field_id, sid, path, before_idx[sid], after_idx[sid])
233
+ end
234
+ removed + added + changed
235
+ end
236
+
237
+ def stop_attr_changes(field_id, stop_id, path, before, after)
238
+ cmp = []
239
+ cmp << stop_changed(field_id, stop_id, path, 'threshold', before[:threshold], after[:threshold]) if before[:threshold] != after[:threshold]
240
+ cmp << stop_changed(field_id, stop_id, path, 'color', before[:color], after[:color]) if before[:color] != after[:color]
241
+ cmp
242
+ end
243
+
244
+ def stop_changed(field_id, stop_id, path, attribute, before, after) # rubocop:disable Metrics/ParameterLists
245
+ Change.new(op: :changed, kind: :gauge_stop, id: stop_id, label: "#{attribute} #{after}".strip,
246
+ path: path, attribute: attribute, before: before, after: after, parent_id: field_id)
247
+ end
248
+
249
+ # Stops keyed by id, carrying threshold + color (an ADD needs both to build the command).
250
+ def index_stops(field)
251
+ Array(field['stops']).each_with_object({}) do |s, h|
252
+ next unless s.is_a?(Hash) && s['id']
253
+
254
+ h[s['id']] = { threshold: s['threshold'], color: s['color'],
255
+ label: "#{s['threshold']} #{s['color']}".strip }
256
+ end
257
+ end
258
+
259
+ # --- indexing helpers ---------------------------------------------
260
+
261
+ def index_stages(doc)
262
+ stages(doc).each_with_object({}) { |st, h| h[st['id']] = st if st['id'] }
263
+ end
264
+
265
+ def index_sections(doc)
266
+ out = {}
267
+ stages(doc).each do |st|
268
+ sections(st).each do |sec|
269
+ next unless sec['id']
270
+
271
+ out[sec['id']] = { heading: sec['heading'], stage: st['name'], stage_id: st['id'],
272
+ path: "#{st['name']} / #{sec['heading']}" }
273
+ end
274
+ end
275
+ out
276
+ end
277
+
278
+ def index_fields(doc)
279
+ out = {}
280
+ stages(doc).each do |st|
281
+ sections(st).each do |sec|
282
+ fields(sec).each do |f|
283
+ next unless f['id']
284
+
285
+ out[f['id']] = {
286
+ field: f, label: f['label'], type: f['__typename'] || f['type'],
287
+ section: sec['heading'], section_id: sec['id'], stage_id: st['id'],
288
+ path: "#{st['name']} / #{sec['heading']} / #{f['label']}"
289
+ }
290
+ end
291
+ end
292
+ end
293
+ out
294
+ end
295
+
296
+ # Options keyed by id when present, else by value (primary) / name (auxiliary) — the
297
+ # correct select-option identity (NOT genome).
298
+ def index_options(field)
299
+ Array(field['options']).each_with_object({}) do |o, h|
300
+ next unless o.is_a?(Hash)
301
+
302
+ key = o['id'] || o['value'] || o['label'] || o['name']
303
+ h[key] = { label: o['label'] || o['name'] || o['value'], weight: o['weight'] }
304
+ end
305
+ end
306
+
307
+ def stages(doc)
308
+ Array(doc['stages'])
309
+ end
310
+
311
+ def sections(stage)
312
+ Array(stage['sections'])
313
+ end
314
+
315
+ def fields(section)
316
+ Array(section['dataFields']) + Array(section['leftDataFields']) + Array(section['rightDataFields'])
317
+ end
318
+
319
+ def changed(kind, id, label, path, attribute, before, after, parent_id = nil) # rubocop:disable Metrics/ParameterLists
320
+ Change.new(op: :changed, kind: kind, id: id, label: label, path: path,
321
+ attribute: attribute, before: before, after: after, parent_id: parent_id)
322
+ end
323
+
324
+ def moved(kind, id, label, attribute, before, after) # rubocop:disable Metrics/ParameterLists
325
+ Change.new(op: :moved, kind: kind, id: id, label: label,
326
+ attribute: attribute, before: before, after: after)
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,34 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ # Diff engines over the page/template model.
5
+ #
6
+ # `VersionDiff` handles the EASY, exact case — two snapshots of the SAME object (same ids),
7
+ # e.g. a UAT template at version N vs N+1 ("what did this ticket change"). The HARD case —
8
+ # pairing two DIFFERENT objects with different ids (UAT<->PROD, page<->template) — is an
9
+ # equivalence-matching problem handled by `Diff::Pairing` (multi-signal + learning ledger).
10
+ #
11
+ # `CommandSynthesizer` turns a change-set into an ordered WorkflowCommand batch (the replayable
12
+ # "commit"); `IdResolver` fills in move-target ids from a target doc; `Deploy` orchestrates
13
+ # diff -> commands -> apply, gating on the honest `unsupported` list.
14
+ #
15
+ # `Strategy` makes diffing a COMPOSABLE family (pairing x scope x move-sensitivity x intent);
16
+ # `CrossObjectDiff` is the cross-object front-end (pairs fields via `Pairing::Engine`, then
17
+ # emits the same `Change` output against the pairing map).
18
+ module Diff
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ require_relative 'diff/change'
25
+ require_relative 'diff/strategy'
26
+ require_relative 'diff/version_diff'
27
+ require_relative 'diff/id_resolver'
28
+ require_relative 'diff/command_synthesizer'
29
+ require_relative 'diff/pairing/ledger'
30
+ require_relative 'diff/pairing/candidate'
31
+ require_relative 'diff/pairing/signals'
32
+ require_relative 'diff/pairing/engine'
33
+ require_relative 'diff/cross_object_diff'
34
+ require_relative 'diff/deploy'
@@ -1,53 +1,53 @@
1
- module Ecoportal
2
- module API
3
- class GraphQL
4
- class Fragment
5
- fragment :LocationDraft, <<~GRAPHQL
6
- fragment LocationDraft on Draft {
7
- id
8
- name
9
- notes
10
- committed
11
- createdAt { dateTime }
12
- ok
13
- conflictingIds
14
- results {
15
- ok
16
- command {
17
- id
18
- state
19
- __typename
20
- ... on LocationInsertCommand {
21
- nodeId name parentId insertBefore classificationIds
22
- }
23
- ... on LocationUpdateCommand {
24
- nodeId newId
25
- newName: name
26
- newClassificationIds: classificationIds
27
- }
28
- ... on LocationArchiveCommand { nodeId }
29
- ... on LocationUnarchiveCommand { nodeId }
30
- ... on LocationMoveCommand { nodeId parentId insertBefore }
31
- ... on LocationReorderCommand { parentId orderedIds }
32
- }
33
- error {
34
- message
35
- conflictingIds
36
- validationErrors {
37
- property
38
- error
39
- message
40
- }
41
- }
42
- }
43
- commands {
44
- id
45
- state
46
- __typename
47
- }
48
- }
49
- GRAPHQL
50
- end
51
- end
52
- end
53
- end
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ class Fragment
5
+ fragment :LocationDraft, <<~GRAPHQL
6
+ fragment LocationDraft on Draft {
7
+ id
8
+ name
9
+ notes
10
+ committed
11
+ createdAt
12
+ ok
13
+ conflictingIds
14
+ results {
15
+ ok
16
+ command {
17
+ id
18
+ state
19
+ __typename
20
+ ... on LocationInsertCommand {
21
+ nodeId name parentId insertBefore classificationIds
22
+ }
23
+ ... on LocationUpdateCommand {
24
+ nodeId newId
25
+ newName: name
26
+ newClassificationIds: classificationIds
27
+ }
28
+ ... on LocationArchiveCommand { nodeId }
29
+ ... on LocationUnarchiveCommand { nodeId }
30
+ ... on LocationMoveCommand { nodeId parentId insertBefore }
31
+ ... on LocationReorderCommand { parentId orderedIds }
32
+ }
33
+ error {
34
+ message
35
+ conflictingIds
36
+ validationErrors {
37
+ property
38
+ error
39
+ message
40
+ }
41
+ }
42
+ }
43
+ commands {
44
+ id
45
+ state
46
+ __typename
47
+ }
48
+ }
49
+ GRAPHQL
50
+ end
51
+ end
52
+ end
53
+ end
@@ -119,6 +119,7 @@ module Ecoportal
119
119
  __typename
120
120
  id @skip(if: $only_content)
121
121
  label
122
+ genomeSignature @skip(if: $only_content)
122
123
  deindex @include(if: $content)
123
124
  linkedFieldConfig @skip(if: $only_content) {
124
125
  crossReferenceId
@@ -1,18 +1,27 @@
1
- module Ecoportal
2
- module API
3
- class GraphQL
4
- module Input
5
- class WorkflowCommand
6
- module AddField
7
- SCHEMA_VERSION = '20260605'.freeze
8
- VALID_KEYS = %i[placeholderId fieldType label stageId sectionId column].freeze
9
-
10
- def self.build(**kwargs)
11
- kwargs.slice(*VALID_KEYS).compact
12
- end
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Input
5
+ class WorkflowCommand
6
+ module AddField
7
+ SCHEMA_VERSION = '20260605'.freeze
8
+ # VERIFIED against the live schema dump (WorkflowAddFieldInput, 20260605): the create
9
+ # command accepts ONLY these keys. There is NO `description` and NO `required` field on
10
+ # WorkflowAddFieldInput. `description` is instead a top-level key of
11
+ # WorkflowEditFieldConfigurationInput, so a field's description is persisted via a
12
+ # follow-up editFieldConfiguration command (see Builder::TemplateBuilder /
13
+ # Diff::CommandSynthesizer). `required` has NO input key anywhere in the schema (not on
14
+ # AddField, not on editFieldConfiguration top-level or any byType sub-input) and so
15
+ # cannot be persisted through the workflow command bus at all — callers must treat it as
16
+ # a read-only/unsupported attribute. Do NOT add these keys here (the server rejects them).
17
+ VALID_KEYS = %i[placeholderId fieldType label stageId sectionId column].freeze
18
+
19
+ def self.build(**kwargs)
20
+ kwargs.slice(*VALID_KEYS).compact
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -29,7 +29,7 @@ module Ecoportal
29
29
  fullMessages
30
30
  }
31
31
  item {
32
- ___Fragment__Action
32
+ spread :Action
33
33
  }
34
34
  }
35
35
  end
@@ -28,7 +28,7 @@ module Ecoportal
28
28
  fullMessages
29
29
  }
30
30
  item {
31
- ___Fragment__Action
31
+ spread :Action
32
32
  }
33
33
  }
34
34
  end
@@ -28,7 +28,7 @@ module Ecoportal
28
28
  fullMessages
29
29
  }
30
30
  item {
31
- ___Fragment__Action
31
+ spread :Action
32
32
  }
33
33
  }
34
34
  end
@@ -28,7 +28,7 @@ module Ecoportal
28
28
  fullMessages
29
29
  }
30
30
  item {
31
- ___Fragment__ContractorEntity
31
+ spread :ContractorEntity
32
32
  }
33
33
  }
34
34
  end
@@ -28,7 +28,7 @@ module Ecoportal
28
28
  fullMessages
29
29
  }
30
30
  item {
31
- ___Fragment__ContractorEntity
31
+ spread :ContractorEntity
32
32
  }
33
33
  }
34
34
  end
@@ -28,7 +28,7 @@ module Ecoportal
28
28
  fullMessages
29
29
  }
30
30
  item {
31
- ___Fragment__ContractorEntity
31
+ spread :ContractorEntity
32
32
  }
33
33
  }
34
34
  end
@@ -27,7 +27,7 @@ module Ecoportal
27
27
 
28
28
  def default_payload_block
29
29
  proc {
30
- item { id name status failedAt }
30
+ item { id name status failedAt { dateTime timeZone } }
31
31
  errors { details fullMessages }
32
32
  }
33
33
  end
@@ -27,7 +27,7 @@ module Ecoportal
27
27
 
28
28
  def default_payload_block
29
29
  proc {
30
- item { id name status startedAt }
30
+ item { id name status startedAt { dateTime timeZone } }
31
31
  errors { details fullMessages }
32
32
  }
33
33
  end
@@ -27,7 +27,7 @@ module Ecoportal
27
27
 
28
28
  def default_payload_block
29
29
  proc {
30
- item { id name status stoppedAt }
30
+ item { id name status stoppedAt { dateTime timeZone } }
31
31
  errors { details fullMessages }
32
32
  }
33
33
  end