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
@@ -0,0 +1,38 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # A single "System Access Log" entry, returned by
6
+ # `currentOrganization.activityLogs` (ActivityLogUnionConnection). Read-only.
7
+ #
8
+ # `ActivityLogUnion` is a UNION over 9 members:
9
+ # DashboardAccess, DashboardCreate, DashboardDelete, DashboardUpdate,
10
+ # DeniedDashboardAccess, DeniedPageAccess, FileDownload, PageAccess,
11
+ # ScimIntegrationAccess.
12
+ # This models the field set COMMON to ALL members and passes the concrete
13
+ # `__typename` through. The Dashboard* members additionally expose
14
+ # `parentResource*` + `resource` (see Model::ActivityLog); page/file/scim
15
+ # members do not, so those live on the Model subclass as nullable embeds.
16
+ #
17
+ # Enum-valued fields are exposed as plain strings:
18
+ # - resourceType: ResourceTypesEnum
19
+ class ActivityLog < Logic::BaseModel
20
+ read_only!
21
+
22
+ passkey :id
23
+ # Concrete member type (e.g. 'DashboardAccess', 'PageAccess', 'FileDownload').
24
+ passthrough :__typename
25
+ passthrough :activityType
26
+ passthrough :source
27
+ passthrough :userType
28
+ passthrough :accountStatus
29
+ passthrough :becomingEmail
30
+ # content: Hash! — free-form activity payload; passed through as a Hash.
31
+ passthrough :content
32
+ passthrough :resourceId, :resourceName, :resourceType
33
+ embeds_one :createdAt, klass: Base::DateTime
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,14 +2,19 @@
2
2
  module API
3
3
  class GraphQL
4
4
  module Base
5
- # Represents one generation version of an AiSummary field.
6
- # Returned by generateAiSummary — tracks the prompt, output, token usage, and status.
5
+ # One generation version of an AI page summary — the `item` of `generatePageSummary`.
6
+ #
7
+ # ★ Corrected 2026-07-30 against the live schema. The previous declarations (`prompt`,
8
+ # `inputTokens`, `outputTokens`, `totalTokens`, `resourceType`, `ratings`) do not exist on
9
+ # `AiSummaryVersion` and always read nil. The real fields are: aiSummary, alreadyRated,
10
+ # createdAt, dataFieldLogs, generationErrors, id, pagePatchVer, status.
11
+ #
12
+ # `dataFieldLogs` / `generationErrors` / `createdAt` are left unmodelled until their own
13
+ # shapes are verified — select and model them deliberately rather than by assumption.
7
14
  class AiSummaryVersion < Logic::BaseModel
8
- passkey :id
9
- passthrough :aiSummary, :status, :prompt
10
- passthrough :inputTokens, :outputTokens, :totalTokens
11
- passthrough :resourceType # "field" | "stage" | "page"
12
- passarray :ratings # [[user_id, score], ...]
15
+ passkey :id
16
+ passthrough :aiSummary, :status, :pagePatchVer
17
+ passboolean :alreadyRated
13
18
  end
14
19
  end
15
20
  end
@@ -0,0 +1,24 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # System access log entry (SCIM / security auditing), returned by
6
+ # `Organization.authLogs`. Read-only.
7
+ #
8
+ # Enum-valued fields are exposed as plain strings:
9
+ # - event: 'LOGIN' | 'LOGOUT' | 'PASSWORD_CHANGE' | 'PASSWORD_RESET_REQUEST'
10
+ # - loginMethod: 'PASSWORD' | 'SAML' | 'MAGIC_LINK' | 'REDIRECT' | 'OAUTH'
11
+ # - source: 'WEB' | 'MOBILE'
12
+ class AuthLog < Logic::BaseModel
13
+ read_only!
14
+
15
+ passkey :id
16
+ passthrough :event, :source
17
+ passthrough :loginMethod
18
+ passthrough :host, :ipAddress, :referrer, :userAgent
19
+ embeds_one :createdAt, klass: Base::DateTime
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # AuthorizationResult — the result of a permission / ability check (a `canXXX` field).
6
+ # value: Boolean! — whether the action is allowed
7
+ # message: String — human-readable reason when denied
8
+ # reasons: FailureReasons — structured reasons (not modelled here; omitted from the
9
+ # :AuthorizationResult fragment to avoid pulling that subtree).
10
+ # Reusable node type behind the schema's many `canXXX: AuthorizationResult` fields.
11
+ class AuthorizationResult < Logic::BaseModel
12
+ read_only!
13
+
14
+ passboolean :value
15
+ passthrough :message
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,7 +3,13 @@ module Ecoportal
3
3
  class GraphQL
4
4
  module Base
5
5
  class ContractorEntity
6
+ # Convenience mutators over the read-model `associatedPeopleIds` / `leadContractorIds`
7
+ # arrays. They push/delete plain ids so that `Input::ContractorEntity::Update.from_model`
8
+ # can later emit the IdDiff (additions/removals) against `original_doc`.
6
9
  module MemberChanges
10
+ # Associates one or more people. When `lead: true`, also marks them as leads.
11
+ # @param values [Array<String, Hash, Person, Enumerable>] ids (or things resolvable to ids).
12
+ # @param lead [Boolean] whether the associated people are also lead contractors.
7
13
  def associate(*values, lead: false)
8
14
  ids = to_person_ids(*values)
9
15
 
@@ -15,7 +21,9 @@ module Ecoportal
15
21
  end
16
22
  end
17
23
 
18
- def dissassociate(*values)
24
+ # Disassociates one or more people (also removing them as leads).
25
+ # @param values [Array<String, Hash, Person, Enumerable>] ids (or resolvable to ids).
26
+ def disassociate(*values)
19
27
  ids = to_person_ids(*values)
20
28
 
21
29
  associatedPeopleIds.tap do |associated|
@@ -23,7 +31,10 @@ module Ecoportal
23
31
  associated.delete!(*ids)
24
32
  end
25
33
  end
34
+ # @deprecated Backwards-compatible alias for the original misspelling.
35
+ alias_method :dissassociate, :disassociate
26
36
 
37
+ # Marks one or more people as lead contractors (associating them if not already).
27
38
  def lead!(*values)
28
39
  ids = to_person_ids(*values)
29
40
 
@@ -33,6 +44,7 @@ module Ecoportal
33
44
  end
34
45
  end
35
46
 
47
+ # Removes one or more people from the lead contractors list.
36
48
  def unlead!(*values)
37
49
  ids = to_person_ids(*values)
38
50
 
@@ -44,7 +56,7 @@ module Ecoportal
44
56
  private
45
57
 
46
58
  def to_person_ids(*values)
47
- to_person_id(values.flatten)
59
+ Array(to_person_id(values.flatten)).flatten.compact
48
60
  end
49
61
 
50
62
  def to_person_id(value)
@@ -54,9 +66,9 @@ module Ecoportal
54
66
  when Ecoportal::API::V1::Person
55
67
  value.id
56
68
  when Hash
57
- to_person_id(['id'])
58
- when Enumarable
59
- value.map {|val| to_person_id(val)}.compact
69
+ value['id'] || value[:id]
70
+ when Enumerable
71
+ value.map { |val| to_person_id(val) }.compact
60
72
  end
61
73
  end
62
74
  end
@@ -2,11 +2,19 @@ module Ecoportal
2
2
  module API
3
3
  class GraphQL
4
4
  module Base
5
+ # Verified against the live schema 2026-07-30: activeFileAttachment, activeName,
6
+ # activeVersion, archive, canDownload, canShow, draft, fileAttachments, fileSize, id,
7
+ # locations, name, otherTags, tags, token.
8
+ #
9
+ # ★ `activeVersionId` was declared here and does NOT exist on the type (it is
10
+ # `activeVersion`) — it always read nil. `otherTags` was missing, which matters because
11
+ # `UploadInput.otherTags` is the only way to tag a container.
5
12
  class FileContainer < Logic::BaseModel
6
- passkey :id
7
- passthrough :name, :activeName
8
- passthrough :fileSize, :token, :activeVersionId
9
- passarray :tags
13
+ passkey :id
14
+ passthrough :name, :activeName, :activeVersion
15
+ passthrough :fileSize, :token
16
+ passboolean :draft
17
+ passarray :tags, :otherTags
10
18
  end
11
19
  end
12
20
  end
@@ -8,6 +8,8 @@ module Ecoportal
8
8
  passkey :id
9
9
  passthrough :name
10
10
  passboolean :active
11
+ # Whether this classification was AI-generated (2026-07 schema).
12
+ passboolean :aiGenerated, read_only: true
11
13
  end
12
14
  end
13
15
  end
@@ -11,6 +11,9 @@ module Ecoportal
11
11
  passkey :id
12
12
  passthrough :name, :weight
13
13
  passboolean :archived, :published, :visitorManagementEnabled
14
+ # AI-generated structure support (2026-07 schema).
15
+ passthrough :aiPrompt, read_only: true
16
+ passboolean :generating, read_only: true
14
17
  embeds_one :updatedAt, klass: Base::DateTime
15
18
 
16
19
  embeds_many :drafts, klass: :draft_class, order_key: :createdAt
@@ -0,0 +1,25 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # Lightweight dashboard descriptor returned by the `dashboardSearch` connection.
6
+ # Read-only. For the full dashboard (components, filters, permissions) the
7
+ # `Dashboard` type is not yet modelled — see docs/worklog.md.
8
+ #
9
+ # Enum-valued fields are exposed as plain strings:
10
+ # - permissionType: 'GLOBAL' | 'RESTRICTED' | 'PRIVATE'
11
+ # - parentResourceType: DashboardParentResource (REGISTER, REPORTING, TASKS, ...)
12
+ class MinimalDashboard < Logic::BaseModel
13
+ read_only!
14
+
15
+ passkey :id
16
+ passthrough :name
17
+ passthrough :parentResource, :parentResourceId, :parentResourceType
18
+ passthrough :permissionType
19
+ passthrough :weight
20
+ passboolean :global
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -8,6 +8,8 @@ module Ecoportal
8
8
  passkey :id
9
9
  passthrough :name, :companyName
10
10
  passthrough :logoUrl, read_only: true
11
+ # Whether contractor admins may add other contractor admins (2026-07 schema).
12
+ passboolean :allowContractorAdminsToAddAdmins, read_only: true
11
13
  end
12
14
  end
13
15
  end
@@ -1,12 +1,15 @@
1
- module Ecoportal::API::GraphQL::Base::Page
1
+ module Ecoportal::API::GraphQL::Base::Page
2
2
  class DataField
3
3
  # Contractor entity selector field.
4
4
  # Stores references to ContractorEntity records.
5
5
  class ContractorEntities < DataField
6
- passarray :contractors # each: { id:, name: }
6
+ passarray :contractors # staged writes; the READ response uses `contractorEntities`
7
7
 
8
+ # Current contractor-entity ids. The GraphQL response carries them under
9
+ # `contractorEntities` (CommonPageUnion's contractorEntitiesField), NOT `contractors`
10
+ # — so read from there. After a contractor_entity_ids= write, prefer the staged value.
8
11
  def contractor_entity_ids
9
- Array(contractors).map { |c| c.is_a?(Hash) ? c['id'] : c }.compact
12
+ contractor_ids_from(doc)
10
13
  end
11
14
 
12
15
  def contractor_entity_ids=(ids)
@@ -18,11 +21,31 @@
18
21
  self.contractor_entity_ids = ids
19
22
  end
20
23
 
24
+ # Dirty only when the effective id SET actually changed — same phantom-dirty class as
25
+ # CrossReference (READ one key, WRITE another): the default leaf diff flags the field
26
+ # the moment the `contractors` key appears, even for a no-op restatement or a clear on
27
+ # an already-empty field.
28
+ def dirty?
29
+ contractor_ids_from(doc).sort != contractor_ids_from(original_doc).sort
30
+ end
31
+
21
32
  def as_input
22
33
  return nil unless dirty?
23
34
 
24
35
  { contractorEntities: { id: id, contractorEntityIds: contractor_entity_ids } }
25
36
  end
37
+
38
+ private
39
+
40
+ # Prefer the staged `contractors` (write shape), fall back to `contractorEntities`
41
+ # (read shape). Shape-agnostic for both `doc` and `original_doc`, so the comparison
42
+ # stays correct after `consolidate!` copies the staged shape into the baseline.
43
+ def contractor_ids_from(source_doc)
44
+ return [] unless source_doc
45
+
46
+ src = source_doc.key?('contractors') ? source_doc['contractors'] : source_doc['contractorEntities']
47
+ Array(src).map { |c| c.is_a?(Hash) ? c['id'] : c }.compact
48
+ end
26
49
  end
27
50
  end
28
51
  end
@@ -1,24 +1,63 @@
1
- module Ecoportal::API::GraphQL::Base::Page
1
+ module Ecoportal::API::GraphQL::Base::Page
2
2
  class DataField
3
3
  # File attachment field.
4
- # Writing requires a prior file upload to the REST upload endpoint
5
- # to obtain fileContainerIds see the file-upload TODO in projects/TODO.md.
6
- # The file_container_ids= setter can be used once IDs are available.
4
+ # READ: the fragment selects `items { id fileContainer { id ... } }` — the container id
5
+ # lives at items[].fileContainer.id, NOT in a `fileContainers` array (live-verified;
6
+ # the previous reader keyed on a doc key the API never returns).
7
+ # WRITE: live `FileInput` accepts `items: [ItemInput]` ({id, fileContainerId, position}),
8
+ # NOT `fileContainerIds`. The backend applies `items` as FULL-REPLACE — any existing item
9
+ # whose id is not echoed back is destroyed — so `as_input` sends kept items as `{id:}`
10
+ # and new containers as `{fileContainerId:}`.
11
+ # `items` is deliberately a plain doc reader, not `passarray`: materialising an ArrayModel
12
+ # writes `items: []` into the doc of a field read without $content, and a phantom-dirty
13
+ # empty `items` here doesn't no-op — it DELETES every attachment on the next update.
14
+ # Container ids come from a prior upload — see FileUpload::Client#upload_all.
7
15
  class FileField < DataField
8
- passarray :fileContainers # each: { id:, fileName:, ... }
16
+ # each: { 'id' =>, 'fileContainer' => { 'id' =>, 'activeFileAttachment' => {...} } }
17
+ def items
18
+ Array(doc['items'])
19
+ end
9
20
 
10
21
  def file_container_ids
11
- Array(fileContainers).map { |c| c.is_a?(Hash) ? c['id'] : c }.compact
22
+ items.map do |item|
23
+ item.is_a?(Hash) ? item.dig('fileContainer', 'id') : item
24
+ end.compact
12
25
  end
13
26
 
27
+ # Full-replace: containers already on the field keep their item identity;
28
+ # ids not present in `ids` are removed server-side by omission.
14
29
  def file_container_ids=(ids)
15
- doc['fileContainers'] = Array(ids).map { |id_val| { 'id' => id_val } }
30
+ kept = {}
31
+ items.each do |item|
32
+ next unless item.is_a?(Hash) && (fc_id = item.dig('fileContainer', 'id'))
33
+
34
+ kept[fc_id] ||= item['id']
35
+ end
36
+ doc['items'] = Array(ids).map do |fc_id|
37
+ new_item = { 'fileContainer' => { 'id' => fc_id } }
38
+ new_item['id'] = kept[fc_id] if kept[fc_id]
39
+ new_item
40
+ end
16
41
  end
17
42
 
18
43
  def as_input
19
44
  return nil unless dirty?
20
45
 
21
- { file: { id: id, fileContainerIds: file_container_ids } }
46
+ { file: { id: id, items: items_input } }
47
+ end
48
+
49
+ private
50
+
51
+ def items_input
52
+ items.map do |item|
53
+ next unless item.is_a?(Hash)
54
+
55
+ if (item_id = item['id'])
56
+ { id: item_id }
57
+ else
58
+ { fileContainerId: item.dig('fileContainer', 'id') }
59
+ end
60
+ end.compact
22
61
  end
23
62
  end
24
63
  end
@@ -1,8 +1,11 @@
1
1
  module Ecoportal::API::GraphQL::Base::Page
2
2
  class DataField
3
3
  # Image gallery field.
4
- # Writing requires a prior file upload to the REST upload endpoint
5
- # to obtain fileContainerIds — see the file-upload TODO in projects/TODO.md.
4
+ # WRITE PATH BROKEN (schema-invalid, live-verified 2026-08): `ImageGalleryInput` has no
5
+ # `fileContainerIds`it takes `images: [ImageInput]`, attaching via `sourceId`, which is
6
+ # an `Enzyme::ImageReference::TempImage` (a DIFFERENT upload pipeline than file containers).
7
+ # Fixing this needs its own design — see the DataField shape-asymmetry review project.
8
+ # The reader is equally stale: live responses carry `images`, not `fileContainers`.
6
9
  class ImageGallery < DataField
7
10
  passarray :fileContainers # each: { id:, fileName:, url:, ... }
8
11
 
@@ -4,20 +4,16 @@ module Ecoportal::API::GraphQL::Base::Page
4
4
  passarray :peopleIds
5
5
 
6
6
  def people_ids
7
- ids = Array(peopleIds)
8
- return ids unless ids.empty?
9
-
10
- # Fallback: the read fragment populates `peopleIds`, but if a response ever returns it
11
- # null/empty while the expanded `people { id }` nodes ARE present (a partial / non-
12
- # $content read), seed from the nodes. The backend write replaces peopleIds wholesale,
13
- # so a `people_ids << x` append must start from the existing set or it would WIPE the
14
- # field. Mirrors CrossReference's references.nodes fallback. Read key == write key
15
- # (peopleIds), so this only widens the read; it never creates phantom-dirty.
16
- Array(doc['people']).map { |p| p.is_a?(Hash) ? p['id'] : p }.compact
7
+ people_ids_from(doc)
17
8
  end
18
9
 
19
10
  def people_ids=(ids)
20
- doc['peopleIds'] = Array(ids).compact
11
+ # An explicit write makes `peopleIds` authoritative for THIS instance: without the
12
+ # flag, a deliberate clear (`people_ids = []`) on a `peopleIds: null` baseline would
13
+ # fall back to the `people` nodes on the doc side too, compare equal, and be silently
14
+ # dropped. The flag confines the fallback to un-written docs.
15
+ @people_ids_written = true
16
+ doc['peopleIds'] = Array(ids).compact
21
17
  end
22
18
 
23
19
  # v2 compat: scripts may set via people_ids= or direct assignment
@@ -25,10 +21,52 @@ module Ecoportal::API::GraphQL::Base::Page
25
21
  self.people_ids = ids
26
22
  end
27
23
 
24
+ # Dirty only when the effective people-id SET actually changed.
25
+ #
26
+ # The read response can carry `peopleIds: null` while still populating the expanded
27
+ # `people { id }` (and `cachedPeople`) nodes (a partial / non-$content read). The
28
+ # default key-level leaf diff baselines on `original_doc['peopleIds']` (null), so
29
+ # writing the SAME id set as already attached looks like a brand-new key and flags
30
+ # a change — a phantom-dirty update that sends `peopleIds` for a no-op. Compare id
31
+ # SETS (falling back to `people[].id` on both sides) instead. Mirrors CrossReference's
32
+ # `references.nodes` fallback / `dirty?` override for the identical disease.
33
+ def dirty?
34
+ effective_people_ids.sort != people_ids_from(original_doc).sort
35
+ end
36
+
28
37
  def as_input
29
38
  return nil unless dirty?
30
39
 
31
- { people: { id: id, peopleIds: Array(peopleIds) } }
40
+ { people: { id: id, peopleIds: effective_people_ids } }
41
+ end
42
+
43
+ private
44
+
45
+ # The id set the client currently means: after an explicit `people_ids=` write the raw
46
+ # `peopleIds` doc key is authoritative (so `[]` means CLEAR, no fallback); before any
47
+ # write it is the shape-agnostic read (fallback to `people[].id` nodes). Keeping
48
+ # `as_input` on the same source as `dirty?` also prevents an explicit clear from
49
+ # re-serialising fallback-seeded node ids.
50
+ def effective_people_ids
51
+ return Array(doc['peopleIds']).compact if @people_ids_written
52
+
53
+ people_ids_from(doc)
54
+ end
55
+
56
+ # Extract the people id set from a doc snapshot. `original_doc` is the server baseline
57
+ # (the diff prev_doc — see Diffable::ClassicDiffService), `doc` carries client changes.
58
+ # Prefer `peopleIds` when it is present and non-empty (the write shape, and the normal
59
+ # read shape); fall back to the expanded `people[].id` nodes (the read-only lookup
60
+ # shape) when `peopleIds` is absent/null/empty — so the comparison is shape-agnostic on
61
+ # both sides and stays correct after `consolidate!` copies `doc` into `original_doc`.
62
+ # `people` nodes are a lookup only, never written.
63
+ def people_ids_from(source_doc)
64
+ return [] unless source_doc
65
+
66
+ ids = Array(source_doc['peopleIds']).compact
67
+ return ids unless ids.empty?
68
+
69
+ Array(source_doc['people']).map { |p| p.is_a?(Hash) ? p['id'] : p }.compact
32
70
  end
33
71
  end
34
72
  end
@@ -10,24 +10,12 @@ module Ecoportal
10
10
  #
11
11
  # A DataField's array attributes (`options`, `peopleIds`, `contractors`,
12
12
  # `pages`, `fileContainers`, `items`, ...) are declared with `passarray`,
13
- # which registers them as *cascaded* attributes. The default `DiffService`
14
- # strips cascaded keys from the flat classic diff (`doc_with_non_cascaded_attributes`)
15
- # on the assumption that a later `diff_reduce` pass re-adds them by cascading
16
- # into nested `GraphQL::Model` children. DataField arrays are plain hashes /
17
- # strings, not `GraphQL::Model` objects, so that re-add never happens and the
18
- # array change becomes invisible to the diff (scalar `passthrough` fields are
19
- # unaffected). For a leaf model we want every doc key diffed inline, so this
20
- # service keeps cascaded keys in place.
21
- class LeafDiffService < Ecoportal::API::Common::GraphQL::Model::Diffable::DiffService
22
- private
23
-
24
- # Keep all doc keys: leaf-field arrays are data, not nested models.
25
- def doc_with_non_cascaded_attributes(json)
26
- json
27
- end
28
- end
29
-
30
- DIFF_CLASS = LeafDiffService
13
+ # which registers them as *cascaded* attributes invisible to the default
14
+ # `DiffService` (see `Diffable::LeafDiffService` for the full explanation).
15
+ # A DataField is a pure leaf model (no nested `GraphQL::Model` children), so
16
+ # it diffs `flat: true` the promoted shared `LeafDiffService` keeps the
17
+ # array doc keys inline.
18
+ DIFF_CLASS = Ecoportal::API::Common::GraphQL::Model::Diffable::LeafDiffService
31
19
  # Map GraphQL __typename values to subclass names.
32
20
  TYPE_MAP = {
33
21
  'PlainText' => 'PlainText',
@@ -55,6 +43,12 @@ module Ecoportal
55
43
 
56
44
  passkey :id
57
45
  passthrough :label, :deindex, :genomeSignature
46
+ # `read_only: true` is load-bearing, not documentation: `description` is readable on
47
+ # `DataFieldsInterface` but ABSENT from `DataFieldInput` and every per-type member of
48
+ # it, so a writable accessor would make the field dirty and push a key `updatePage`
49
+ # cannot accept. The only writer is the `editFieldConfiguration` workflow command
50
+ # (`Input::WorkflowCommand::EditFieldConfiguration`).
51
+ passthrough :description, read_only: true
58
52
  # __typename is stored under the 'type' key after mapping
59
53
  passthrough :type
60
54
 
@@ -0,0 +1,15 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # Permissions ? the org-level ability set (Organization.permissions).
6
+ # Every field is a `canXXX: AuthorizationResult!` permission check. Read-only.
7
+ # The concrete embeds_one declarations live on Model::Permissions (they reference
8
+ # Model::AuthorizationResult).
9
+ class Permissions < Logic::BaseModel
10
+ read_only!
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ module Ecoportal
2
+ module API
3
+ class GraphQL
4
+ module Base
5
+ # A single audit entry in a person member's changelog history, returned by
6
+ # `PersonMember.changelogs` / `Organization.personMemberChangelogs`. Read-only.
7
+ #
8
+ # Enum-valued fields are exposed as plain strings:
9
+ # - operation / accountOperation: 'CREATE' | 'UPDATE' | 'DESTROY'
10
+ class PersonMemberChangelog < Logic::BaseModel
11
+ read_only!
12
+
13
+ passkey :id
14
+ passthrough :operation, :accountOperation
15
+ passthrough :personMemberId, :apiKeyPrefix
16
+ passboolean :changedByApi
17
+ passarray :humanReadableDifferences
18
+ embeds_one :createdAt, klass: Base::DateTime
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end