ecoportal-api-graphql 1.3.17 → 2.2.1

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +763 -11
  3. data/lib/ecoportal/api/common/graphql/http_client.rb +85 -2
  4. data/lib/ecoportal/api/common/graphql/model/diffable/leaf_diff_service.rb +97 -0
  5. data/lib/ecoportal/api/common/graphql/model/diffable.rb +1 -0
  6. data/lib/ecoportal/api/graphql/base/action.rb +1 -1
  7. data/lib/ecoportal/api/graphql/base/action_event.rb +23 -0
  8. data/lib/ecoportal/api/graphql/base/action_event_page.rb +16 -0
  9. data/lib/ecoportal/api/graphql/base/activity.rb +38 -0
  10. data/lib/ecoportal/api/graphql/base/activity_log.rb +38 -0
  11. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +12 -7
  12. data/lib/ecoportal/api/graphql/base/auth_log.rb +24 -0
  13. data/lib/ecoportal/api/graphql/base/authorization_result.rb +20 -0
  14. data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +17 -5
  15. data/lib/ecoportal/api/graphql/base/file_container.rb +12 -4
  16. data/lib/ecoportal/api/graphql/base/location_classification_type.rb +2 -0
  17. data/lib/ecoportal/api/graphql/base/location_structure.rb +3 -0
  18. data/lib/ecoportal/api/graphql/base/minimal_dashboard.rb +25 -0
  19. data/lib/ecoportal/api/graphql/base/organization.rb +2 -0
  20. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +25 -2
  21. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +46 -7
  22. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +5 -2
  23. data/lib/ecoportal/api/graphql/base/page/data_field/people.rb +50 -12
  24. data/lib/ecoportal/api/graphql/base/page/data_field.rb +12 -18
  25. data/lib/ecoportal/api/graphql/base/page/phased/stage.rb +51 -0
  26. data/lib/ecoportal/api/graphql/base/permissions.rb +15 -0
  27. data/lib/ecoportal/api/graphql/base/person_member_changelog.rb +23 -0
  28. data/lib/ecoportal/api/graphql/base/preset_view.rb +135 -0
  29. data/lib/ecoportal/api/graphql/base/register.rb +5 -0
  30. data/lib/ecoportal/api/graphql/base/template/change_log_entry.rb +118 -0
  31. data/lib/ecoportal/api/graphql/base/template.rb +2 -0
  32. data/lib/ecoportal/api/graphql/base.rb +9 -0
  33. data/lib/ecoportal/api/graphql/builder/contractor_entity.rb +10 -0
  34. data/lib/ecoportal/api/graphql/builder/page.rb +0 -16
  35. data/lib/ecoportal/api/graphql/builder/template.rb +46 -0
  36. data/lib/ecoportal/api/graphql/builder/template_builder.rb +21 -2
  37. data/lib/ecoportal/api/graphql/compat/pages.rb +7 -2
  38. data/lib/ecoportal/api/graphql/compat/response.rb +14 -0
  39. data/lib/ecoportal/api/graphql/concerns/threadable.rb +58 -0
  40. data/lib/ecoportal/api/graphql/concerns.rb +1 -0
  41. data/lib/ecoportal/api/graphql/connection/action.rb +1 -1
  42. data/lib/ecoportal/api/graphql/connection/action_category.rb +1 -1
  43. data/lib/ecoportal/api/graphql/connection/activity.rb +11 -0
  44. data/lib/ecoportal/api/graphql/connection/activity_log.rb +11 -0
  45. data/lib/ecoportal/api/graphql/connection/auth_log.rb +11 -0
  46. data/lib/ecoportal/api/graphql/connection/contractor_entity.rb +1 -1
  47. data/lib/ecoportal/api/graphql/connection/minimal_dashboard.rb +11 -0
  48. data/lib/ecoportal/api/graphql/connection/page.rb +1 -1
  49. data/lib/ecoportal/api/graphql/connection/pages_workflow_command.rb +1 -1
  50. data/lib/ecoportal/api/graphql/connection/person_member.rb +1 -1
  51. data/lib/ecoportal/api/graphql/connection/person_member_changelog.rb +11 -0
  52. data/lib/ecoportal/api/graphql/connection/preview_page.rb +1 -1
  53. data/lib/ecoportal/api/graphql/connection/template_change_log.rb +19 -0
  54. data/lib/ecoportal/api/graphql/connection.rb +6 -0
  55. data/lib/ecoportal/api/graphql/diff/change.rb +7 -2
  56. data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +19 -1
  57. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +21 -1
  58. data/lib/ecoportal/api/graphql/diff/version_diff.rb +13 -6
  59. data/lib/ecoportal/api/graphql/file_upload/client.rb +198 -139
  60. data/lib/ecoportal/api/graphql/fragment/activity.rb +84 -0
  61. data/lib/ecoportal/api/graphql/fragment/activity_log.rb +75 -0
  62. data/lib/ecoportal/api/graphql/fragment/auth_log.rb +29 -0
  63. data/lib/ecoportal/api/graphql/fragment/authorization_result.rb +14 -0
  64. data/lib/ecoportal/api/graphql/fragment/location_node.rb +1 -0
  65. data/lib/ecoportal/api/graphql/fragment/minimal_dashboard.rb +31 -0
  66. data/lib/ecoportal/api/graphql/fragment/page.rb +1 -0
  67. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +23 -0
  68. data/lib/ecoportal/api/graphql/fragment/permissions.rb +96 -0
  69. data/lib/ecoportal/api/graphql/fragment/person_member_changelog.rb +33 -0
  70. data/lib/ecoportal/api/graphql/fragment/template_change_log.rb +102 -0
  71. data/lib/ecoportal/api/graphql/fragment.rb +8 -0
  72. data/lib/ecoportal/api/graphql/input/action/update.rb +12 -0
  73. data/lib/ecoportal/api/graphql/input/contractor_entity/manager_settings.rb +20 -0
  74. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +6 -27
  75. data/lib/ecoportal/api/graphql/input/contractor_entity.rb +1 -0
  76. data/lib/ecoportal/api/graphql/input/date_range.rb +16 -0
  77. data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +2 -2
  78. data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +2 -2
  79. data/lib/ecoportal/api/graphql/input/location_structure/insert_command.rb +8 -0
  80. data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +6 -2
  81. data/lib/ecoportal/api/graphql/input/page/review_task.rb +14 -1
  82. data/lib/ecoportal/api/graphql/input/preset_view/update.rb +16 -1
  83. data/lib/ecoportal/api/graphql/input/workflow_command/add_checklist_field_item.rb +19 -0
  84. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +10 -9
  85. data/lib/ecoportal/api/graphql/input/workflow_command/add_select_field_option.rb +11 -5
  86. data/lib/ecoportal/api/graphql/input/workflow_command/add_smart_fill_config.rb +23 -0
  87. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +3 -2
  88. data/lib/ecoportal/api/graphql/input/workflow_command/add_viewable_field.rb +45 -0
  89. data/lib/ecoportal/api/graphql/input/workflow_command/archive_public_config.rb +19 -0
  90. data/lib/ecoportal/api/graphql/input/workflow_command/create_public_config.rb +19 -0
  91. data/lib/ecoportal/api/graphql/input/workflow_command/edit_checklist_field_item.rb +19 -0
  92. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +17 -2
  93. data/lib/ecoportal/api/graphql/input/workflow_command/edit_force.rb +5 -2
  94. data/lib/ecoportal/api/graphql/input/workflow_command/edit_public_config.rb +19 -0
  95. data/lib/ecoportal/api/graphql/input/workflow_command/edit_select_field_option.rb +6 -2
  96. data/lib/ecoportal/api/graphql/input/workflow_command/edit_smart_fill_config.rb +21 -0
  97. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_advanced_settings.rb +19 -0
  98. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_basic_settings.rb +32 -0
  99. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_container_uid.rb +45 -0
  100. data/lib/ecoportal/api/graphql/input/workflow_command/{edit_page_creator_permissions.rb → edit_template_creator_permissions.rb} +3 -2
  101. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_descriptions.rb +19 -0
  102. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/action_list.rb +31 -0
  103. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/contractor_entities.rb +6 -2
  104. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +9 -1
  105. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +15 -2
  106. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/file.rb +21 -0
  107. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +3 -2
  108. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/people.rb +27 -3
  109. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +9 -2
  110. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +3 -2
  111. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +3 -2
  112. data/lib/ecoportal/api/graphql/input/workflow_command/manage_copy_page_configuration.rb +47 -0
  113. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +3 -2
  114. data/lib/ecoportal/api/graphql/input/workflow_command/move_section_to_stage.rb +33 -0
  115. data/lib/ecoportal/api/graphql/input/workflow_command/move_viewable_field.rb +33 -0
  116. data/lib/ecoportal/api/graphql/input/workflow_command/remove_checklist_field_item.rb +19 -0
  117. data/lib/ecoportal/api/graphql/input/workflow_command/remove_smart_fill_config.rb +19 -0
  118. data/lib/ecoportal/api/graphql/input/workflow_command/remove_viewable_field.rb +21 -0
  119. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +3 -2
  120. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +3 -2
  121. data/lib/ecoportal/api/graphql/input/workflow_command.rb +48 -5
  122. data/lib/ecoportal/api/graphql/input.rb +1 -0
  123. data/lib/ecoportal/api/graphql/interface/base_page.rb +50 -3
  124. data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +2 -1
  125. data/lib/ecoportal/api/graphql/logic/base_query.rb +72 -5
  126. data/lib/ecoportal/api/graphql/logic/connection.rb +17 -0
  127. data/lib/ecoportal/api/graphql/logic/input.rb +127 -0
  128. data/lib/ecoportal/api/graphql/model/action_event.rb +14 -0
  129. data/lib/ecoportal/api/graphql/model/action_event_page.rb +10 -0
  130. data/lib/ecoportal/api/graphql/model/activity.rb +14 -0
  131. data/lib/ecoportal/api/graphql/model/activity_log.rb +31 -0
  132. data/lib/ecoportal/api/graphql/model/auth_log.rb +11 -0
  133. data/lib/ecoportal/api/graphql/model/authorization_result.rb +12 -0
  134. data/lib/ecoportal/api/graphql/model/minimal_dashboard.rb +14 -0
  135. data/lib/ecoportal/api/graphql/model/organization.rb +36 -0
  136. data/lib/ecoportal/api/graphql/model/page/phased.rb +17 -0
  137. data/lib/ecoportal/api/graphql/model/permissions.rb +185 -0
  138. data/lib/ecoportal/api/graphql/model/person_member.rb +7 -0
  139. data/lib/ecoportal/api/graphql/model/person_member_changelog.rb +15 -0
  140. data/lib/ecoportal/api/graphql/model/template/change_log_entry.rb +19 -0
  141. data/lib/ecoportal/api/graphql/model/template.rb +4 -1
  142. data/lib/ecoportal/api/graphql/model.rb +9 -0
  143. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +20 -10
  144. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +25 -10
  145. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update_manager_settings.rb +37 -0
  146. data/lib/ecoportal/api/graphql/mutation/contractor_entity.rb +1 -0
  147. data/lib/ecoportal/api/graphql/mutation/file_container/upload.rb +78 -0
  148. data/lib/ecoportal/api/graphql/mutation/file_container.rb +15 -0
  149. data/lib/ecoportal/api/graphql/mutation/template/update_related_page.rb +79 -0
  150. data/lib/ecoportal/api/graphql/mutation/template.rb +1 -0
  151. data/lib/ecoportal/api/graphql/mutation.rb +1 -1
  152. data/lib/ecoportal/api/graphql/payload/contractor_entity/manager_settings.rb +20 -0
  153. data/lib/ecoportal/api/graphql/payload/contractor_entity.rb +1 -0
  154. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_jobs.rb +2 -2
  155. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_workflows.rb +2 -2
  156. data/lib/ecoportal/api/graphql/payload/template/update_related_page.rb +15 -0
  157. data/lib/ecoportal/api/graphql/payload/template.rb +1 -0
  158. data/lib/ecoportal/api/graphql/payload/upload.rb +12 -0
  159. data/lib/ecoportal/api/graphql/payload.rb +1 -0
  160. data/lib/ecoportal/api/graphql/query/activity_logs.rb +78 -0
  161. data/lib/ecoportal/api/graphql/query/auth_logs.rb +51 -0
  162. data/lib/ecoportal/api/graphql/query/dashboard_search.rb +57 -0
  163. data/lib/ecoportal/api/graphql/query/location_structure.rb +2 -0
  164. data/lib/ecoportal/api/graphql/query/location_structures.rb +2 -0
  165. data/lib/ecoportal/api/graphql/query/page.rb +6 -1
  166. data/lib/ecoportal/api/graphql/query/page_activities.rb +56 -0
  167. data/lib/ecoportal/api/graphql/query/permissions.rb +35 -0
  168. data/lib/ecoportal/api/graphql/query/person_member_changelogs.rb +53 -0
  169. data/lib/ecoportal/api/graphql/query/person_members.rb +76 -0
  170. data/lib/ecoportal/api/graphql/query/template_change_log.rb +118 -0
  171. data/lib/ecoportal/api/graphql/query.rb +8 -0
  172. data/lib/ecoportal/api/graphql_version.rb +1 -1
  173. metadata +83 -14
  174. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +0 -28
  175. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +0 -36
  176. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +0 -40
  177. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +0 -13
@@ -105,14 +105,6 @@ module Ecoportal
105
105
  submitAiSummaryFeedbackMutation.query(**kargs, &block)
106
106
  end
107
107
 
108
- def generate_smart_fill(**kargs, &block)
109
- generateSmartFillMutation.query(**kargs, &block)
110
- end
111
-
112
- def submit_smart_fill_feedback(**kargs, &block)
113
- submitSmartFillFeedbackMutation.query(**kargs, &block)
114
- end
115
-
116
108
  private
117
109
 
118
110
  # Blank the page's externalId via updatePage (no patchVer needed — cleanup path).
@@ -184,14 +176,6 @@ module Ecoportal
184
176
  Mutation::AiSummary::SubmitFeedback.new(client)
185
177
  end
186
178
 
187
- def generateSmartFillMutation
188
- Mutation::SmartFill::Generate.new(client)
189
- end
190
-
191
- def submitSmartFillFeedbackMutation
192
- Mutation::SmartFill::SubmitFeedback.new(client)
193
- end
194
-
195
179
  def executeWorkflowCommandsMutation
196
180
  Mutation::Page::ExecuteWorkflowCommands.new(client)
197
181
  end
@@ -40,10 +40,48 @@ module Ecoportal
40
40
  )
41
41
  end
42
42
 
43
+ # Edits an existing related-pages entry. Only the keys you pass are sent — absent keys
44
+ # are left alone server-side, so this is a partial update, not a replace.
45
+ def update_related_page(template_id:, id:, patch_ver:, title: nil, register_id: nil, filters: nil, &block)
46
+ updateRelatedPageMutation.query(
47
+ template_id: template_id,
48
+ id: id,
49
+ patch_ver: patch_ver,
50
+ title: title,
51
+ register_id: register_id,
52
+ filters: filters,
53
+ &block
54
+ )
55
+ end
56
+
43
57
  def destroy_related_page(template_id:, id:, patch_ver:, &block)
44
58
  destroyRelatedPageMutation.query(template_id: template_id, id: id, patch_ver: patch_ver, &block)
45
59
  end
46
60
 
61
+ # TEMPLATE CHANGE LOG — who changed this template, what, and when. Newest first
62
+ # (server-side `_id: :desc`), paginated.
63
+ #
64
+ # api.template.change_log(id: template_page_id, first: 50) # => Connection
65
+ # api.template.change_log.each(id: template_page_id) { |e| ... } # auto-paginates
66
+ #
67
+ # ★ Two caveats, both silent — read `Base::Template::ChangeLogEntry` before relying on
68
+ # this: the connection is nil (not empty) unless the caller satisfies
69
+ # `Pages::TemplatePolicy#update_information?`, and only TEMPLATE_BUILDER-sourced
70
+ # commands appear (workflow-builder ones are on `Query::PagesWorkflowCommands`).
71
+ #
72
+ # Called with no arguments it returns the query object itself, so `#each` /`#map` and
73
+ # the rest of Enumerable are available — same idiom as the QueryIntegration queries on
74
+ # `Model::Organization`.
75
+ # @param detail [Boolean] when true, also select FIELD-LEVEL detail (what changed).
76
+ # Ignored when an explicit block is given -- the block wins, since it is the more
77
+ # specific instruction. Off by default so the common who/when read stays cheap.
78
+ def change_log(detail: false, **kargs, &block)
79
+ selection = block || (detail ? Query::TemplateChangeLog.detail_block : nil)
80
+ return changeLogQuery if kargs.empty? && !selection
81
+
82
+ changeLogQuery.query(**kargs, &selection)
83
+ end
84
+
47
85
  private
48
86
 
49
87
  def createMutation
@@ -70,9 +108,17 @@ module Ecoportal
70
108
  Mutation::Template::CreateRelatedPage.new(client)
71
109
  end
72
110
 
111
+ def updateRelatedPageMutation
112
+ Mutation::Template::UpdateRelatedPage.new(client)
113
+ end
114
+
73
115
  def destroyRelatedPageMutation
74
116
  Mutation::Template::DestroyRelatedPage.new(client)
75
117
  end
118
+
119
+ def changeLogQuery
120
+ Query::TemplateChangeLog.new(client)
121
+ end
76
122
  end
77
123
  end
78
124
  end
@@ -24,7 +24,12 @@ module Ecoportal
24
24
  # { field_type: 'PlainText', label: 'Description', column: 0,
25
25
  # description: 'ID:token' }, # addField (+ editFieldConfiguration for description)
26
26
  # { field_type: 'Select', label: 'Severity',
27
- # options: [ { label: 'Low', weight: 0 }, ... ], # addSelectFieldOption
27
+ # options: [ { label: 'Low', weight: 0, value: 'low' },
28
+ # { label: 'High', weight: 10, value: 'high' } ], # addSelectFieldOption
29
+ # # `value:` is REQUIRED per option (backend: AddSelectFieldOptionInput,
30
+ # # required: true) — it is the option's unique key (duplicate values within one
31
+ # # field are rejected), always sent as a String even for numeric select fields
32
+ # # (stored as an Integer server-side). A missing `value` raises ArgumentError.
28
33
  # config: { select: { multiple: false } } }, # editFieldConfiguration(byType:)
29
34
  # { field_type: 'Gauge', label: 'Risk',
30
35
  # stops: [ { threshold: 0, color: '#0f0' }, ... ], # addGaugeFieldStop
@@ -125,9 +130,23 @@ module Ecoportal
125
130
  add(:editFieldConfiguration, data_field_id: field_ph, description: description)
126
131
  end
127
132
 
133
+ # addSelectFieldOption requires `value` (backend: AddSelectFieldOptionInput, required: true
134
+ # — a real field distinct from label/weight, cast to a number server-side for numeric
135
+ # select fields). The DSL has no natural default for it (unlike label/weight, it is
136
+ # business data this builder cannot infer), so it MUST be supplied per option in the spec;
137
+ # a missing one raises rather than silently falling back to the label/weight (which would
138
+ # risk persisting the wrong stored value for a scored/numeric select field).
128
139
  def emit_options(field, field_ph)
129
140
  Array(field[:options]).each do |opt|
130
- add(:addSelectFieldOption, data_field_id: field_ph, label: opt[:label], weight: opt[:weight])
141
+ value = opt[:value]
142
+ if value.nil?
143
+ raise ArgumentError,
144
+ "TemplateBuilder: option '#{opt[:label]}' on field '#{field[:label]}' is " \
145
+ 'missing `value` (addSelectFieldOption requires it) — supply ' \
146
+ 'options: [{ label:, weight:, value: }, ...] in the spec'
147
+ end
148
+
149
+ add(:addSelectFieldOption, data_field_id: field_ph, label: opt[:label], value: value, weight: opt[:weight])
131
150
  end
132
151
  end
133
152
 
@@ -18,8 +18,13 @@ module Ecoportal
18
18
  # Fetch a page by ID with full data — metadata, stages, sections, data fields.
19
19
  # When stage_id is provided, wraps the result in a StageView that filters
20
20
  # sections and components to that stage only — matching v2 per-stage behaviour.
21
- def get(id, stage_id: nil)
22
- page = Query::Page.new(@client).query(id: id) do
21
+ # @param variables [Hash] optional CommonPageUnion document-variable overrides
22
+ # (`showHiddenData:`, `content:`, `only_content:`, `fields:`). `content: false`
23
+ # skips field values AND `attachedFiles` — the one selection preauthorized on the
24
+ # `files: download` ability — so structure-only readers (e.g. template tooling on a
25
+ # least-privilege account) can read pages without that grant.
26
+ def get(id, stage_id: nil, **variables)
27
+ page = Query::Page.new(@client).query(id: id, **variables) do
23
28
  spread :CommonPageUnion
24
29
  end
25
30
  stage_id ? StageView.new(page, stage_id) : page
@@ -5,7 +5,21 @@ module Ecoportal
5
5
  # Wraps a GraphQL mutation payload to expose the v2-compatible response interface.
6
6
  # Scripts check .success? and .status after update/create operations.
7
7
  class Response
8
+ # The payload contract this wrapper delegates to. Guarded at construction so a
9
+ # payload of the wrong shape fails LOUD here, instead of silently downstream.
10
+ # This is the class of drift behind the eco-helpers 3.2.19 KPI-counter
11
+ # regression, where a Compat::Response (duck-typed, not a Common::Response)
12
+ # slipped through an `is_a?` check and every GraphQL update went uncounted.
13
+ PAYLOAD_CONTRACT = %i[success? error? error_doc item].freeze
14
+
8
15
  def initialize(payload)
16
+ missing = PAYLOAD_CONTRACT.reject { |meth| payload.respond_to?(meth) }
17
+ unless missing.empty?
18
+ raise ArgumentError,
19
+ "Compat::Response payload #{payload.class} does not honour the expected " \
20
+ "response contract; missing: #{missing.join(', ')}"
21
+ end
22
+
9
23
  @payload = payload
10
24
  end
11
25
 
@@ -0,0 +1,58 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Concerns
5
+ # Bounded-concurrency helpers for batch operations.
6
+ #
7
+ # Ported from `ecoportal-api-v2`'s `Common::Concerns::Threadable` rather than required
8
+ # from it: that gem is being decommissioned, and this is exactly the kind of small,
9
+ # stable primitive that should not create a new dependency on it.
10
+ #
11
+ # `new_thread` keeps at most `max` threads alive by joining the oldest before starting
12
+ # a new one — a deliberately simple back-pressure valve, not a pool. `max: 1` runs
13
+ # inline (no threads at all), which keeps single-file paths and specs deterministic.
14
+ module Threadable
15
+ private
16
+
17
+ # @param threads [Array<Thread>] the caller's thread accumulator (joined by the caller).
18
+ # @param max [Integer] maximum concurrent threads; `1` means run inline.
19
+ # @return [Array<Thread>, void]
20
+ def new_thread(threads, max: 20, &block)
21
+ if max <= 1
22
+ yield
23
+ return []
24
+ end
25
+
26
+ threads.shift.join if threads.count >= max
27
+ threads << Thread.new(&block)
28
+ end
29
+
30
+ # Named mutexes, created on demand. Use one key per shared resource so unrelated
31
+ # critical sections never serialise against each other.
32
+ def mutex(key = :general)
33
+ key = key.to_sym
34
+ @mutex_map ||= {}
35
+ # The map itself must be created atomically the first time.
36
+ @mutex_map[key] ||= Mutex.new
37
+ end
38
+
39
+ # Batch work must not be killed by one file raising in a thread, and must not spam
40
+ # the caller's stderr with per-thread reports — the caller gets the error in its
41
+ # result object instead. Restores both globals afterwards.
42
+ def with_preserved_thread_globals(abort: false, report: false)
43
+ mutex(:thread_globals).synchronize do
44
+ prev_abort = Thread.abort_on_exception
45
+ prev_report = Thread.report_on_exception
46
+ Thread.abort_on_exception = abort
47
+ Thread.report_on_exception = report
48
+ yield
49
+ ensure
50
+ Thread.abort_on_exception = prev_abort
51
+ Thread.report_on_exception = prev_report
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -12,3 +12,4 @@ require_relative 'concerns/fragment_definitions'
12
12
  require_relative 'concerns/snake_camel_access'
13
13
  require_relative 'concerns/page_compat'
14
14
  require_relative 'concerns/data_field_access'
15
+ require_relative 'concerns/threadable'
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class Action < Logic::Connection
6
- class_resolver :node_class, Model::Action
6
+ nodes_of Model::Action
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class ActionCategory < Logic::Connection
6
- class_resolver :node_class, Base::ActionCategory
6
+ nodes_of Base::ActionCategory
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ class Activity < Logic::Connection
6
+ nodes_of Model::Activity
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ class ActivityLog < Logic::Connection
6
+ nodes_of Model::ActivityLog
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ class AuthLog < Logic::Connection
6
+ nodes_of Model::AuthLog
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class ContractorEntity < Logic::Connection
6
- class_resolver :node_class, Model::ContractorEntity
6
+ nodes_of Model::ContractorEntity
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ class MinimalDashboard < Logic::Connection
6
+ nodes_of Model::MinimalDashboard
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class Page < Logic::Connection
6
- class_resolver :node_class, 'Ecoportal::API::GraphQL::Model::PageUnion'
6
+ nodes_of 'Ecoportal::API::GraphQL::Model::PageUnion'
7
7
  end
8
8
  end
9
9
  end
@@ -5,7 +5,7 @@ module Ecoportal
5
5
  class PagesWorkflowCommand < Logic::Connection
6
6
  SCHEMA_VERSION = '20260605'.freeze
7
7
 
8
- class_resolver :node_class, 'Ecoportal::API::GraphQL::Base::PagesWorkflow::CommandInterface'
8
+ nodes_of 'Ecoportal::API::GraphQL::Base::PagesWorkflow::CommandInterface'
9
9
  end
10
10
  end
11
11
  end
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class PersonMemberConnection < Logic::Connection
6
- class_resolver :node_class, Model::PersonMember
6
+ nodes_of Model::PersonMember
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,11 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ class PersonMemberChangelog < Logic::Connection
6
+ nodes_of Model::PersonMemberChangelog
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,7 +3,7 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Connection
5
5
  class PreviewPage < Logic::Connection
6
- class_resolver :node_class, 'Ecoportal::API::GraphQL::Model::PreviewPage'
6
+ nodes_of 'Ecoportal::API::GraphQL::Model::PreviewPage'
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,19 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Connection
5
+ # `Template.commands` — a `PagesWorkflowCommandInterfaceConnection` carrying
6
+ # `totalCount` (added by `Types::CustomConnections::BaseConnection`), `pageInfo`
7
+ # and `nodes`.
8
+ #
9
+ # ★ A nil connection here means NOT PERMITTED, not "no changes" — see
10
+ # `Base::Template::ChangeLogEntry` for the two access caveats.
11
+ class TemplateChangeLog < Logic::Connection
12
+ # `nodes_of` (not `class_resolver :node_class`) — see Logic::Connection for why the
13
+ # shared dynamic collection poisons sibling connections otherwise.
14
+ nodes_of Model::Template::ChangeLogEntry
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,3 +15,9 @@ require_relative 'connection/page'
15
15
  require_relative 'connection/preview_page'
16
16
  require_relative 'connection/preset_view'
17
17
  require_relative 'connection/pages_workflow_command'
18
+ require_relative 'connection/activity'
19
+ require_relative 'connection/activity_log'
20
+ require_relative 'connection/auth_log'
21
+ require_relative 'connection/person_member_changelog'
22
+ require_relative 'connection/minimal_dashboard'
23
+ require_relative 'connection/template_change_log'
@@ -23,13 +23,18 @@ module Ecoportal
23
23
  # that parent_id alone cannot carry. Set for an ADDED :field (parent_id = its section id,
24
24
  # owner_id = its stage id) because addField requires BOTH sectionId and stageId (both
25
25
  # NON_NULL in the schema). nil for every other kind/op.
26
+ #
27
+ # value: for an ADDED :option, the select option's `value` (the schema's addSelectFieldOption
28
+ # requires it — see AddSelectFieldOptionInput, backend authority). Read from the source doc's
29
+ # option.value (VersionDiff#index_options); nil when the source doc did not carry one. NEVER
30
+ # defaulted to label/weight by the synthesiser — see CommandSynthesizer#option_command.
26
31
  Change = Struct.new(:op, :kind, :id, :label, :path, :attribute, :before, :after, :parent_id,
27
- :by_type, :owner_id, keyword_init: true) do
32
+ :by_type, :owner_id, :value, keyword_init: true) do
28
33
  def to_h
29
34
  {
30
35
  op: op, kind: kind, id: id, label: label, path: path,
31
36
  attribute: attribute, before: before, after: after, parent_id: parent_id,
32
- by_type: by_type, owner_id: owner_id
37
+ by_type: by_type, owner_id: owner_id, value: value
33
38
  }.compact
34
39
  end
35
40
 
@@ -277,13 +277,31 @@ module Ecoportal
277
277
 
278
278
  field_ref = ref(field_id)
279
279
  case change.op
280
- when :added then build(:addSelectFieldOption, data_field_id: field_ref, label: change.label)
280
+ when :added then add_option(change, field_ref)
281
281
  when :removed then build(:removeSelectFieldOption, data_field_id: field_ref, option_id: change.id)
282
282
  when :changed then edit_option(change, field_ref)
283
283
  else unsupported!(change)
284
284
  end
285
285
  end
286
286
 
287
+ # addSelectFieldOption requires `value` (backend: AddSelectFieldOptionInput, required: true
288
+ # — a real, distinct field from label/weight; see OptionType#value). NEVER default it to
289
+ # the label/weight (a numeric select field stores `value` as a number, so a label stand-in
290
+ # would silently persist wrong data). When the source doc's option carried no `value`
291
+ # (Change#value nil — see VersionDiff#index_options), raise loudly instead of guessing.
292
+ def add_option(change, field_ref)
293
+ if change.value.nil?
294
+ raise ArgumentError,
295
+ "CommandSynthesizer: cannot build addSelectFieldOption for option " \
296
+ "'#{change.label}' (field #{field_ref}) — Change#value is nil. The source doc's " \
297
+ "option is missing `value` (VersionDiff#index_options reads it from " \
298
+ "option['value']); supply it on the source doc, or on the Change directly when " \
299
+ "hand-assembling one."
300
+ end
301
+
302
+ build(:addSelectFieldOption, data_field_id: field_ref, label: change.label, value: change.value)
303
+ end
304
+
287
305
  def edit_option(change, field_id)
288
306
  case change.attribute
289
307
  when 'label' then build(:editSelectFieldOption, data_field_id: field_id, option_id: change.id, label: change.after)
@@ -25,13 +25,33 @@ module Ecoportal
25
25
  # MISMATCH scores 0 rather than vetoing (the field may have been re-purposed keeping its
26
26
  # genome, or genome may be absent on newer field types). Returns nil when neither side
27
27
  # carries a genome — the signal simply does not apply and is excluded from the average.
28
+ #
29
+ # LINEAGE (verified live 2026-08-05, same-org backup restore of a full template): a
30
+ # restore does NOT equal-copy the signature — it APPENDS one segment to it, for every
31
+ # membrane AND force (e.g. `2zwxkb6gns` → `2zwxkb6gns53jp`, `ktnrya` → `ktnrya877l`;
32
+ # 28/28 fields + 4/4 forces followed the rule). So an ancestor's signature is a strict
33
+ # PREFIX of its descendant's, and equality-only scoring would give the exact
34
+ # cross-object case this engine exists for (UAT backup → PROD restore) a 0.0 — worse
35
+ # than no signal. Prefix ancestry therefore scores just below an exact match.
28
36
  def genome(source, target)
29
37
  a = source['genomeSignature']
30
38
  b = target['genomeSignature']
31
39
  return nil if blank?(a) && blank?(b)
32
40
  return 0.0 if blank?(a) || blank?(b)
41
+ return 1.0 if a == b
33
42
 
34
- a == b ? 1.0 : 0.0
43
+ lineage?(a, b) ? 0.9 : 0.0
44
+ end
45
+
46
+ # True when one signature is a strict prefix of the other (restore/copy appends a
47
+ # lineage segment — see #genome). Direction is unknowable at a pairing call (either
48
+ # side may be the older document), so both directions count. Very short signatures
49
+ # are rejected as ancestry evidence — a trivial prefix can collide by chance.
50
+ def lineage?(sig_a, sig_b)
51
+ short, long = [sig_a.to_s, sig_b.to_s].minmax_by(&:length)
52
+ return false if short.length < 4
53
+
54
+ short != long && long.start_with?(short)
35
55
  end
36
56
 
37
57
  # Field type (__typename) equality. A type change is a strong DISQUALIFIER for a data
@@ -91,8 +91,11 @@ module Ecoportal
91
91
  # addStageSection.stageId), threading through a placeholder when the parent is created in
92
92
  # the same batch. nil (default) means no parent id is known/recorded — the historical
93
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 }
94
+ # `value:` (optional proc) is recorded on ADDED changes only — currently used for :option,
95
+ # whose addSelectFieldOption command requires the source doc's option.value (see
96
+ # Change#value). nil (default) means the kind has no `value` concept.
97
+ def diff_index(kind, before_idx, after_idx, label:, path:, parent: nil, owner: nil, value: nil, &compare)
98
+ procs = { label: label, path: path, parent: parent, owner: owner, value: value }
96
99
  removed = (before_idx.keys - after_idx.keys).map { |id| removed_change(kind, id, before_idx[id], procs) }
97
100
  added = (after_idx.keys - before_idx.keys).map { |id| added_change(kind, id, after_idx[id], procs) }
98
101
  matched = (before_idx.keys & after_idx.keys).flat_map { |id| Array(compare.call(id, before_idx[id], after_idx[id])) }
@@ -105,7 +108,8 @@ module Ecoportal
105
108
 
106
109
  def added_change(kind, id, info, procs)
107
110
  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))
111
+ parent_id: procs[:parent]&.call(info), owner_id: procs[:owner]&.call(info),
112
+ value: procs[:value]&.call(info))
109
113
  end
110
114
 
111
115
  def stage_changes
@@ -153,7 +157,7 @@ module Ecoportal
153
157
 
154
158
  def options_diff(field_id, path, before_field, after_field)
155
159
  diff_index(:option, index_options(before_field), index_options(after_field),
156
- label: ->(o) { o[:label] }, path: ->(_o) { path }) do |k, b, a|
160
+ label: ->(o) { o[:label] }, path: ->(_o) { path }, value: ->(o) { o[:value] }) do |k, b, a|
157
161
  cmp = []
158
162
  cmp << changed(:option, k, a[:label], path, 'label', b[:label], a[:label], field_id) if b[:label] != a[:label]
159
163
  cmp << changed(:option, k, a[:label], path, 'weight', b[:weight], a[:weight], field_id) if b[:weight] != a[:weight]
@@ -294,13 +298,16 @@ module Ecoportal
294
298
  end
295
299
 
296
300
  # Options keyed by id when present, else by value (primary) / name (auxiliary) — the
297
- # correct select-option identity (NOT genome).
301
+ # correct select-option identity (NOT genome). `value` is carried through separately from
302
+ # `label` (they are distinct schema fields — OptionType#value vs #label/#name) so an ADDED
303
+ # option's Change can supply the real value addSelectFieldOption requires, never a
304
+ # label/weight stand-in.
298
305
  def index_options(field)
299
306
  Array(field['options']).each_with_object({}) do |o, h|
300
307
  next unless o.is_a?(Hash)
301
308
 
302
309
  key = o['id'] || o['value'] || o['label'] || o['name']
303
- h[key] = { label: o['label'] || o['name'] || o['value'], weight: o['weight'] }
310
+ h[key] = { label: o['label'] || o['name'] || o['value'], weight: o['weight'], value: o['value'] }
304
311
  end
305
312
  end
306
313