ecoportal-api-graphql 1.3.16 → 2.0.0

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +384 -6
  3. data/lib/ecoportal/api/common/graphql/model/diffable/leaf_diff_service.rb +97 -0
  4. data/lib/ecoportal/api/common/graphql/model/diffable.rb +1 -0
  5. data/lib/ecoportal/api/graphql/base/action.rb +1 -1
  6. data/lib/ecoportal/api/graphql/base/action_event.rb +23 -0
  7. data/lib/ecoportal/api/graphql/base/action_event_page.rb +16 -0
  8. data/lib/ecoportal/api/graphql/base/activity.rb +38 -0
  9. data/lib/ecoportal/api/graphql/base/activity_log.rb +38 -0
  10. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +12 -7
  11. data/lib/ecoportal/api/graphql/base/auth_log.rb +24 -0
  12. data/lib/ecoportal/api/graphql/base/authorization_result.rb +20 -0
  13. data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +17 -5
  14. data/lib/ecoportal/api/graphql/base/file_container.rb +12 -4
  15. data/lib/ecoportal/api/graphql/base/location_classification_type.rb +2 -0
  16. data/lib/ecoportal/api/graphql/base/location_structure.rb +3 -0
  17. data/lib/ecoportal/api/graphql/base/minimal_dashboard.rb +25 -0
  18. data/lib/ecoportal/api/graphql/base/organization.rb +2 -0
  19. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +26 -3
  20. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +47 -8
  21. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +5 -2
  22. data/lib/ecoportal/api/graphql/base/page/data_field/people.rb +50 -12
  23. data/lib/ecoportal/api/graphql/base/page/data_field.rb +12 -18
  24. data/lib/ecoportal/api/graphql/base/permissions.rb +15 -0
  25. data/lib/ecoportal/api/graphql/base/person_member_changelog.rb +23 -0
  26. data/lib/ecoportal/api/graphql/base/preset_view.rb +136 -1
  27. data/lib/ecoportal/api/graphql/base/register.rb +5 -0
  28. data/lib/ecoportal/api/graphql/base.rb +9 -0
  29. data/lib/ecoportal/api/graphql/builder/contractor_entity.rb +10 -0
  30. data/lib/ecoportal/api/graphql/builder/page.rb +0 -16
  31. data/lib/ecoportal/api/graphql/builder/template.rb +18 -0
  32. data/lib/ecoportal/api/graphql/compat/pages.rb +7 -2
  33. data/lib/ecoportal/api/graphql/compat/response.rb +14 -0
  34. data/lib/ecoportal/api/graphql/concerns/threadable.rb +58 -0
  35. data/lib/ecoportal/api/graphql/concerns.rb +1 -0
  36. data/lib/ecoportal/api/graphql/connection/action.rb +1 -1
  37. data/lib/ecoportal/api/graphql/connection/action_category.rb +1 -1
  38. data/lib/ecoportal/api/graphql/connection/activity.rb +11 -0
  39. data/lib/ecoportal/api/graphql/connection/activity_log.rb +11 -0
  40. data/lib/ecoportal/api/graphql/connection/auth_log.rb +11 -0
  41. data/lib/ecoportal/api/graphql/connection/contractor_entity.rb +1 -1
  42. data/lib/ecoportal/api/graphql/connection/minimal_dashboard.rb +11 -0
  43. data/lib/ecoportal/api/graphql/connection/page.rb +1 -1
  44. data/lib/ecoportal/api/graphql/connection/pages_workflow_command.rb +1 -1
  45. data/lib/ecoportal/api/graphql/connection/person_member.rb +1 -1
  46. data/lib/ecoportal/api/graphql/connection/person_member_changelog.rb +11 -0
  47. data/lib/ecoportal/api/graphql/connection/preview_page.rb +1 -1
  48. data/lib/ecoportal/api/graphql/connection.rb +5 -0
  49. data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +21 -1
  50. data/lib/ecoportal/api/graphql/file_upload/client.rb +199 -140
  51. data/lib/ecoportal/api/graphql/fragment/activity.rb +84 -0
  52. data/lib/ecoportal/api/graphql/fragment/activity_log.rb +75 -0
  53. data/lib/ecoportal/api/graphql/fragment/auth_log.rb +29 -0
  54. data/lib/ecoportal/api/graphql/fragment/authorization_result.rb +14 -0
  55. data/lib/ecoportal/api/graphql/fragment/location_node.rb +1 -0
  56. data/lib/ecoportal/api/graphql/fragment/minimal_dashboard.rb +31 -0
  57. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +21 -0
  58. data/lib/ecoportal/api/graphql/fragment/permissions.rb +98 -0
  59. data/lib/ecoportal/api/graphql/fragment/person_member_changelog.rb +33 -0
  60. data/lib/ecoportal/api/graphql/fragment.rb +7 -0
  61. data/lib/ecoportal/api/graphql/input/action/update.rb +12 -0
  62. data/lib/ecoportal/api/graphql/input/contractor_entity/manager_settings.rb +20 -0
  63. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +6 -27
  64. data/lib/ecoportal/api/graphql/input/contractor_entity.rb +1 -0
  65. data/lib/ecoportal/api/graphql/input/date_range.rb +16 -0
  66. data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +2 -2
  67. data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +2 -2
  68. data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +6 -2
  69. data/lib/ecoportal/api/graphql/input/preset_view/update.rb +16 -1
  70. data/lib/ecoportal/api/graphql/input/workflow_command/add_checklist_field_item.rb +19 -0
  71. data/lib/ecoportal/api/graphql/input/workflow_command/add_smart_fill_config.rb +23 -0
  72. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +3 -2
  73. data/lib/ecoportal/api/graphql/input/workflow_command/add_viewable_field.rb +45 -0
  74. data/lib/ecoportal/api/graphql/input/workflow_command/archive_public_config.rb +19 -0
  75. data/lib/ecoportal/api/graphql/input/workflow_command/create_public_config.rb +19 -0
  76. data/lib/ecoportal/api/graphql/input/workflow_command/edit_checklist_field_item.rb +19 -0
  77. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +12 -2
  78. data/lib/ecoportal/api/graphql/input/workflow_command/edit_public_config.rb +19 -0
  79. data/lib/ecoportal/api/graphql/input/workflow_command/edit_smart_fill_config.rb +21 -0
  80. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_advanced_settings.rb +19 -0
  81. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_basic_settings.rb +27 -0
  82. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_container_uid.rb +45 -0
  83. data/lib/ecoportal/api/graphql/input/workflow_command/{edit_page_creator_permissions.rb → edit_template_creator_permissions.rb} +3 -2
  84. data/lib/ecoportal/api/graphql/input/workflow_command/edit_template_descriptions.rb +19 -0
  85. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/action_list.rb +31 -0
  86. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +3 -1
  87. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +15 -2
  88. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/file.rb +21 -0
  89. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +3 -2
  90. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/people.rb +27 -3
  91. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +3 -2
  92. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +3 -2
  93. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +3 -2
  94. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +3 -2
  95. data/lib/ecoportal/api/graphql/input/workflow_command/move_viewable_field.rb +33 -0
  96. data/lib/ecoportal/api/graphql/input/workflow_command/remove_checklist_field_item.rb +19 -0
  97. data/lib/ecoportal/api/graphql/input/workflow_command/remove_smart_fill_config.rb +19 -0
  98. data/lib/ecoportal/api/graphql/input/workflow_command/remove_viewable_field.rb +21 -0
  99. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +3 -2
  100. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +3 -2
  101. data/lib/ecoportal/api/graphql/input/workflow_command.rb +43 -5
  102. data/lib/ecoportal/api/graphql/input.rb +1 -0
  103. data/lib/ecoportal/api/graphql/interface/base_page.rb +36 -3
  104. data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +2 -1
  105. data/lib/ecoportal/api/graphql/logic/connection.rb +17 -0
  106. data/lib/ecoportal/api/graphql/logic/input.rb +127 -0
  107. data/lib/ecoportal/api/graphql/model/action_event.rb +14 -0
  108. data/lib/ecoportal/api/graphql/model/action_event_page.rb +10 -0
  109. data/lib/ecoportal/api/graphql/model/activity.rb +14 -0
  110. data/lib/ecoportal/api/graphql/model/activity_log.rb +31 -0
  111. data/lib/ecoportal/api/graphql/model/auth_log.rb +11 -0
  112. data/lib/ecoportal/api/graphql/model/authorization_result.rb +12 -0
  113. data/lib/ecoportal/api/graphql/model/minimal_dashboard.rb +14 -0
  114. data/lib/ecoportal/api/graphql/model/organization.rb +27 -0
  115. data/lib/ecoportal/api/graphql/model/permissions.rb +179 -0
  116. data/lib/ecoportal/api/graphql/model/person_member.rb +7 -0
  117. data/lib/ecoportal/api/graphql/model/person_member_changelog.rb +15 -0
  118. data/lib/ecoportal/api/graphql/model.rb +9 -0
  119. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +20 -10
  120. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +25 -10
  121. data/lib/ecoportal/api/graphql/mutation/contractor_entity/update_manager_settings.rb +37 -0
  122. data/lib/ecoportal/api/graphql/mutation/contractor_entity.rb +1 -0
  123. data/lib/ecoportal/api/graphql/mutation/file_container/upload.rb +78 -0
  124. data/lib/ecoportal/api/graphql/mutation/file_container.rb +15 -0
  125. data/lib/ecoportal/api/graphql/mutation/template/update_related_page.rb +79 -0
  126. data/lib/ecoportal/api/graphql/mutation/template.rb +1 -0
  127. data/lib/ecoportal/api/graphql/mutation.rb +1 -1
  128. data/lib/ecoportal/api/graphql/payload/contractor_entity/manager_settings.rb +20 -0
  129. data/lib/ecoportal/api/graphql/payload/contractor_entity.rb +1 -0
  130. data/lib/ecoportal/api/graphql/payload/template/update_related_page.rb +15 -0
  131. data/lib/ecoportal/api/graphql/payload/template.rb +1 -0
  132. data/lib/ecoportal/api/graphql/payload/upload.rb +12 -0
  133. data/lib/ecoportal/api/graphql/payload.rb +1 -0
  134. data/lib/ecoportal/api/graphql/query/activity_logs.rb +78 -0
  135. data/lib/ecoportal/api/graphql/query/auth_logs.rb +51 -0
  136. data/lib/ecoportal/api/graphql/query/dashboard_search.rb +57 -0
  137. data/lib/ecoportal/api/graphql/query/location_structure.rb +2 -0
  138. data/lib/ecoportal/api/graphql/query/location_structures.rb +2 -0
  139. data/lib/ecoportal/api/graphql/query/page.rb +6 -1
  140. data/lib/ecoportal/api/graphql/query/page_activities.rb +56 -0
  141. data/lib/ecoportal/api/graphql/query/permissions.rb +35 -0
  142. data/lib/ecoportal/api/graphql/query/person_member_changelogs.rb +53 -0
  143. data/lib/ecoportal/api/graphql/query.rb +6 -0
  144. data/lib/ecoportal/api/graphql_version.rb +1 -1
  145. metadata +73 -12
  146. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +0 -28
  147. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +0 -36
  148. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +0 -40
  149. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +0 -13
@@ -1,15 +1,150 @@
1
- module Ecoportal
1
+ module Ecoportal
2
2
  module API
3
3
  class GraphQL
4
4
  module Base
5
5
  # A register preset view — saved column/sort/filter configuration for previewPages.
6
6
  # Scripts use the id as `presetViewId` in register.previewPages queries.
7
7
  class PresetView < Logic::BaseModel
8
+ # `fieldConfigurations` is a passarray (leaf) invisible to the default
9
+ # DiffService; LeafDiffService detects it as a full-set replace, which is what
10
+ # makes a columns-only change reach `#as_input` at all (before it, the array was
11
+ # dropped from the flat diff and no mutation was issued). The read-key/shape
12
+ # conversion that change then needs is handled below — the GAP-P1 that comment
13
+ # referred to as a separate pending converter is closed by it.
14
+ # See Diffable::LeafDiffService.
15
+ DIFF_CLASS = Ecoportal::API::Common::GraphQL::Model::Diffable::LeafDiffService
16
+
17
+ # READ key vs WRITE key — and, on update, read SHAPE vs write SHAPE.
18
+ #
19
+ # The API RETURNS the collection as `fieldConfigurations`, a plain list (see the
20
+ # `passarray` below — that stays as-is, consumers read it). The mutations call it
21
+ # `fieldConfigs`, and the two mutations do NOT agree on its shape. Schema-verified
22
+ # against the live introspection dump (20260730):
23
+ #
24
+ # CreatePresetViewInput.fieldConfigs : [FieldConfigurationInput!]!
25
+ # UpdatePresetViewInput.fieldConfigs : FieldConfigurationOneToManyInput!
26
+ # { additions:, deletions:, updates: }
27
+ #
28
+ # Both are NON_NULL with no default value — i.e. REQUIRED on every call — while the
29
+ # `FieldConfigurationOneToManyInput` members each default to `[]`, so `{}` is a
30
+ # valid no-op value for an update that does not touch the columns.
31
+ #
32
+ # Emitting the read key `fieldConfigurations` is an unknown-argument error, and
33
+ # emitting the read LIST under `fieldConfigs` is a type error: either one rejects
34
+ # the WHOLE mutation, a `name`-only edit included.
35
+ #
36
+ # `#as_input` emits the UPDATE shape. `Logic::Mutation#as_input` hands a read model
37
+ # straight to `model.as_input` without telling it which mutation it feeds, and the
38
+ # only model-driven path is `register.preset_view.update`: a read model carries an
39
+ # `id`, which `CreatePresetViewInput` does not even accept. Create is built from a
40
+ # caller-supplied Hash (see Builder::Register::PresetView) whose `fieldConfigs` is
41
+ # already the plain array the schema wants — it needs no conversion.
42
+ READ_KEY = 'fieldConfigurations'.freeze
43
+
44
+ # The fields `FieldConfigurationInput` accepts (same dump). The read nodes also
45
+ # carry `__typename` — the read side is a union, see Fragment::FieldConfiguration —
46
+ # which the input type does NOT accept, so nodes are whitelisted through this list
47
+ # rather than passed along verbatim.
48
+ FIELD_CONFIG_INPUT_KEYS = %i[
49
+ id key name weight dataFieldLabel type showTime valueToShow mode displayUid
50
+ ].freeze
51
+
8
52
  passkey :id
9
53
  passthrough :name, :registerId
10
54
  passboolean :global
11
55
  passarray :fieldConfigurations
12
56
  passthrough :sorter # { key:, direction:, missing: }
57
+
58
+ class << self
59
+ # Turns a raw mutation-input diff into a valid `UpdatePresetViewInput`: drops the
60
+ # read key and sets `fieldConfigs` to the one-to-many delta.
61
+ #
62
+ # Idempotent — the read key is deleted and the delta is recomputed from
63
+ # `doc`/`original_doc`, so a second application yields the same Hash. That is what
64
+ # makes it safe for both seams (`#as_input` and `Input::PresetView::Update
65
+ # .from_model`) to call it on the same Hash.
66
+ #
67
+ # @param model [PresetView] the subject (read) model.
68
+ # @param input [Hash, nil] the mutation input hash so far (nil when nothing in the
69
+ # flat diff changed).
70
+ # @return [Hash, nil] +input+ with a schema-valid `fieldConfigs`, or nil when
71
+ # neither the flat diff nor the columns changed (no mutation to send).
72
+ def build_update_input(model, input)
73
+ return input unless model.respond_to?(:field_configs_delta)
74
+
75
+ delta = model.field_configs_delta
76
+ return input if input.nil? && delta.empty?
77
+
78
+ input ||= {}
79
+ input.delete(:fieldConfigurations)
80
+ input.delete(READ_KEY)
81
+ input[:id] ||= model.id if model.respond_to?(:id)
82
+ input[:fieldConfigs] = delta
83
+ input
84
+ end
85
+
86
+ # @param node [Hash] one `fieldConfigurations` read node.
87
+ # @return [Hash] the node reduced to the keys `FieldConfigurationInput` accepts,
88
+ # symbol-keyed. Keys the read selection did not ask for are simply absent.
89
+ def field_config_input(node)
90
+ FIELD_CONFIG_INPUT_KEYS.each_with_object({}) do |key, out|
91
+ str = key.to_s
92
+ out[key] = node[str] if node.key?(str)
93
+ out[key] = node[key] if node.key?(key)
94
+ end
95
+ end
96
+ end
97
+
98
+ # The `FieldConfigurationOneToManyInput` delta between the server baseline
99
+ # (`original_doc`) and the current `doc`, matched on the config `id`:
100
+ # `additions` = nodes with no known id, `deletions` = baseline ids that are gone,
101
+ # `updates` = known nodes whose (input-visible) content changed. Empty members are
102
+ # omitted, so an untouched collection yields `{}` — the valid no-op.
103
+ #
104
+ # Computed DIRECTLY from the docs rather than from the flat diff, for the same
105
+ # reason as {Logic::Input::IdDiffFields}: `passarray` leaves are dropped by the
106
+ # cascaded DiffService.
107
+ #
108
+ # @return [Hash] `{ additions:, deletions:, updates: }`, empty members omitted.
109
+ def field_configs_delta
110
+ current = field_config_nodes(doc)
111
+ original = field_config_nodes(original_doc)
112
+ known = original.filter_map {|node| node[:id]}
113
+
114
+ additions = current.reject {|node| node[:id] && known.include?(node[:id])}
115
+ updates = (current - additions) - original
116
+ deletions = known - current.filter_map {|node| node[:id]}
117
+
118
+ {}.tap do |delta|
119
+ delta[:additions] = additions unless additions.empty?
120
+ delta[:deletions] = deletions unless deletions.empty?
121
+ delta[:updates] = updates unless updates.empty?
122
+ end
123
+ end
124
+
125
+ # Mutation input for this preset view, shaped for `updatePresetView`.
126
+ # @see .build_update_input
127
+ def as_input(target_class: nil, clientMutationId: '')
128
+ input = self.class.build_update_input(self, super)
129
+ return input if input.nil? || target_class
130
+
131
+ # `super` merges it only when it built the Hash itself; a columns-only change
132
+ # synthesises the Hash here, so keep the wire payload identical either way.
133
+ input[:clientMutationId] = clientMutationId.to_s unless input.key?(:clientMutationId)
134
+ input
135
+ end
136
+
137
+ private
138
+
139
+ # @param source_doc [Hash, nil] `doc` or `original_doc`.
140
+ # @return [Array<Hash>] the read nodes, each reduced to input-accepted keys.
141
+ def field_config_nodes(source_doc)
142
+ return [] if source_doc.nil?
143
+
144
+ Array(source_doc[READ_KEY]).
145
+ grep(Hash).
146
+ map {|node| self.class.field_config_input(node)}
147
+ end
13
148
  end
14
149
  end
15
150
  end
@@ -3,6 +3,11 @@
3
3
  class GraphQL
4
4
  module Base
5
5
  class Register < Logic::BaseModel
6
+ # `filterTags` is a passarray (leaf) that the default DiffService cannot
7
+ # diff; LeafDiffService detects it as a full-set replace. See
8
+ # Diffable::LeafDiffService.
9
+ DIFF_CLASS = Ecoportal::API::Common::GraphQL::Model::Diffable::LeafDiffService
10
+
6
11
  passkey :id
7
12
  passthrough :name, :iconName, :moduleType, :registerStatus
8
13
  passthrough :descriptionText, :descriptionImageUrl
@@ -20,13 +20,22 @@ require_relative 'base/page'
20
20
  require_relative 'base/resource'
21
21
  require_relative 'base/action_category'
22
22
  require_relative 'base/action'
23
+ require_relative 'base/action_event_page'
24
+ require_relative 'base/action_event'
23
25
  require_relative 'base/contractor_entity'
24
26
  require_relative 'base/preview_page'
25
27
  require_relative 'base/delta_result'
28
+ require_relative 'base/authorization_result'
29
+ require_relative 'base/permissions'
30
+ require_relative 'base/activity'
31
+ require_relative 'base/activity_log'
32
+ require_relative 'base/auth_log'
33
+ require_relative 'base/person_member_changelog'
26
34
  require_relative 'base/organization'
27
35
 
28
36
  require_relative 'base/register'
29
37
  require_relative 'base/preset_view'
38
+ require_relative 'base/minimal_dashboard'
30
39
 
31
40
  require_relative 'base/ai_summary_version'
32
41
 
@@ -21,6 +21,12 @@ module Ecoportal
21
21
  destroyMutation.query(**kargs, &block)
22
22
  end
23
23
 
24
+ # updateContractorManagerSettings — auto-approval settings + whether
25
+ # contractor admins may add other admins. Returns the updated Organization.
26
+ def update_manager_settings(**kargs, &block)
27
+ updateManagerSettingsMutation.query(**kargs, &block)
28
+ end
29
+
24
30
  private
25
31
 
26
32
  def createMutation
@@ -34,6 +40,10 @@ module Ecoportal
34
40
  def destroyMutation
35
41
  Mutation::ContractorEntity::Destroy.new(client)
36
42
  end
43
+
44
+ def updateManagerSettingsMutation
45
+ Mutation::ContractorEntity::UpdateManagerSettings.new(client)
46
+ end
37
47
  end
38
48
  end
39
49
  end
@@ -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,6 +40,20 @@ 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
@@ -70,6 +84,10 @@ module Ecoportal
70
84
  Mutation::Template::CreateRelatedPage.new(client)
71
85
  end
72
86
 
87
+ def updateRelatedPageMutation
88
+ Mutation::Template::UpdateRelatedPage.new(client)
89
+ end
90
+
73
91
  def destroyRelatedPageMutation
74
92
  Mutation::Template::DestroyRelatedPage.new(client)
75
93
  end
@@ -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
@@ -15,3 +15,8 @@ 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'
@@ -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