ecoportal-api-graphql 1.3.5 → 1.3.9

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 (499) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-assistance/bridge/CLAUDE.md +338 -0
  3. data/.ai-assistance/bridge/archive/.gitkeep +0 -0
  4. data/.ai-assistance/bridge/archive/oscar-a1b2c3d-gitlab-mcp-doc-update.inbox.md +29 -0
  5. data/.ai-assistance/bridge/archive/oscar-a1b2c3d-gitlab-mcp-doc-update.outbox.md +18 -0
  6. data/.ai-assistance/bridge/archive/oscar-c912c25-gemini-design-review.inbox.md +42 -0
  7. data/.ai-assistance/bridge/archive/oscar-c912c25-gemini-design-review.outbox.md +115 -0
  8. data/.ai-assistance/bridge/context/gemini-review-prompt.txt +48 -0
  9. data/.ai-assistance/bridge/context/gemini-review-response.md +104 -0
  10. data/.ai-assistance/bridge/context/project.md +42 -0
  11. data/.ai-assistance/bridge/inbox/.gitkeep +0 -0
  12. data/.ai-assistance/bridge/outbox/.gitkeep +0 -0
  13. data/.ai-assistance/bridge/outbox/request-for-standards-discovery.md +48 -0
  14. data/.ai-assistance/bridge/queue/.gitkeep +1 -0
  15. data/.ai-assistance/capabilities/CLAUDE.md +27 -0
  16. data/.ai-assistance/capabilities/assumptions-log.md +80 -0
  17. data/.ai-assistance/capabilities/code.md +47 -0
  18. data/.ai-assistance/capabilities/connectors.md +37 -0
  19. data/.ai-assistance/capabilities/cowork.md +55 -0
  20. data/.ai-assistance/code/OVERVIEW.md +155 -0
  21. data/.ai-assistance/code/data_fields.md +242 -0
  22. data/.ai-assistance/code/dependencies.md +151 -0
  23. data/.ai-assistance/code/diff_as_input.md +234 -0
  24. data/.ai-assistance/code/diff_service_deep_dive.md +192 -0
  25. data/.ai-assistance/code/ecoPortal_architecture/00_overview_and_index.md +55 -0
  26. data/.ai-assistance/code/ecoPortal_architecture/01_terminology_dictionary.md +181 -0
  27. data/.ai-assistance/code/ecoPortal_architecture/02_data_model.md +192 -0
  28. data/.ai-assistance/code/ecoPortal_architecture/03_api_layers.md +147 -0
  29. data/.ai-assistance/code/ecoPortal_architecture/04_graphql_queries_mutations.md +277 -0
  30. data/.ai-assistance/code/ecoPortal_architecture/05_page_workflows.md +200 -0
  31. data/.ai-assistance/code/ecoPortal_architecture/06_search_and_filters.md +228 -0
  32. data/.ai-assistance/code/ecoPortal_architecture/07_data_fields.md +197 -0
  33. data/.ai-assistance/code/ecoPortal_architecture/08_stages_sections.md +243 -0
  34. data/.ai-assistance/code/ecoPortal_architecture/09_people_contractors_locations.md +196 -0
  35. data/.ai-assistance/code/ecoPortal_architecture/10_forces_workflow_builder.md +132 -0
  36. data/.ai-assistance/code/ecoPortal_architecture/11_integration_gems.md +187 -0
  37. data/.ai-assistance/code/ecoPortal_architecture/12_ai_documentation_sources_gaps.md +236 -0
  38. data/.ai-assistance/code/ecoPortal_architecture/13_ai_infrastructure.md +183 -0
  39. data/.ai-assistance/code/ecoportal_schema_reference.md +240 -0
  40. data/.ai-assistance/code/graphql_domain_knowledge.md +230 -0
  41. data/.ai-assistance/code/refactoring/datafield-readwrite-shape-asymmetry.md +71 -0
  42. data/.ai-assistance/code/refactoring/opportunities.md +251 -0
  43. data/.ai-assistance/code/schema_analysis.md +321 -0
  44. data/.ai-assistance/code/search_filters.md +868 -0
  45. data/.ai-assistance/code/spec_coverage.md +73 -0
  46. data/.ai-assistance/code/workflow-command-guide.md +438 -0
  47. data/.ai-assistance/code/workflow-space.md +353 -0
  48. data/.ai-assistance/conventions/CLAUDE.md +30 -0
  49. data/.ai-assistance/conventions/code-working-tree-protocol.md +199 -0
  50. data/.ai-assistance/conventions/gitignore-rules.md +42 -0
  51. data/.ai-assistance/conventions/permission-guidance.md +120 -0
  52. data/.ai-assistance/integrations/README.md +70 -0
  53. data/.ai-assistance/integrations/gitkraken-mcp.md +107 -0
  54. data/.ai-assistance/integrations/gitlab-mcp.md +123 -0
  55. data/.ai-assistance/integrations/local-git.md +60 -0
  56. data/.ai-assistance/local_paths.example.md +17 -0
  57. data/.ai-assistance/projects/TODO.md +97 -0
  58. data/.ai-assistance/projects/api-v2-to-graphql-migration/DECISIONS.md +168 -0
  59. data/.ai-assistance/projects/api-v2-to-graphql-migration/INTENT.md +60 -0
  60. data/.ai-assistance/projects/api-v2-to-graphql-migration/TODO.md +267 -0
  61. data/.ai-assistance/projects/api-v2-to-graphql-migration/UPSTREAM.md +53 -0
  62. data/.ai-assistance/projects/api-v2-to-graphql-migration/notes/csv-template-pipeline-design.md +102 -0
  63. data/.ai-assistance/projects/api-v2-to-graphql-migration/notes/cutover-usecase-gap-audit.md +139 -0
  64. data/.ai-assistance/projects/dynamic-model-generation/INTENT.md +93 -0
  65. data/.ai-assistance/projects/eco-helpers-compat/INTENT.md +244 -0
  66. data/.ai-assistance/projects/eco-helpers-compat/MIGRATION_GUIDE.md +266 -0
  67. data/.ai-assistance/projects/eco-helpers-compat/TODO.md +86 -0
  68. data/.ai-assistance/projects/ecoportal-api-v2-doublemodel-review/INTENT.md +101 -0
  69. data/.ai-assistance/projects/graphql-agent/GAP_ANALYSIS.md +177 -0
  70. data/.ai-assistance/projects/ooze-graphql-native-migration/DECISIONS.md +161 -0
  71. data/.ai-assistance/projects/ooze-graphql-native-migration/INTENT.md +125 -0
  72. data/.ai-assistance/projects/ooze-graphql-native-migration/RISKS.md +126 -0
  73. data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +256 -0
  74. data/.ai-assistance/projects/ooze-graphql-native-migration/analysis/2026-06-30-cutover-workflow-deep-review.md +122 -0
  75. data/.ai-assistance/projects/ooze-graphql-native-migration/analysis/2026-07-01-forces-via-workflow-commands-miss-rca.md +148 -0
  76. data/.ai-assistance/projects/page-model/DECISIONS.md +245 -0
  77. data/.ai-assistance/projects/page-model/TODO.md +190 -0
  78. data/.ai-assistance/projects/search-filter-builder/INTENT.md +107 -0
  79. data/.ai-assistance/projects/search-filter-builder/TODO.md +131 -0
  80. data/.ai-assistance/projects/template-maintenance/DESIGN.md +134 -0
  81. data/.ai-assistance/projects/workflow-space/TODO.md +213 -0
  82. data/.ai-assistance/reinstall-claude-desktop-windows.md +136 -0
  83. data/.ai-assistance/scripts/CLAUDE.md +150 -0
  84. data/.ai-assistance/scripts/bridge-init.sh +86 -0
  85. data/.ai-assistance/scripts/bridge-status.sh +44 -0
  86. data/.ai-assistance/scripts/capabilities-check.ts +104 -0
  87. data/.ai-assistance/scripts/check-outbox.sh +43 -0
  88. data/.ai-assistance/scripts/dep_graph.rb +91 -0
  89. data/.ai-assistance/scripts/lock-acquire.sh +103 -0
  90. data/.ai-assistance/scripts/lock-multi.sh +124 -0
  91. data/.ai-assistance/scripts/lock-queue.sh +94 -0
  92. data/.ai-assistance/scripts/setup-mcps.test.ts +188 -0
  93. data/.ai-assistance/scripts/setup-mcps.ts +234 -0
  94. data/.ai-assistance/scripts/task-complete.ts +74 -0
  95. data/.ai-assistance/scripts/task-create.ts +75 -0
  96. data/.ai-assistance/scripts/task-read.ts +125 -0
  97. data/.ai-assistance/scripts/token-logger.js +220 -0
  98. data/.ai-assistance/scripts/token-report.ts +158 -0
  99. data/.ai-assistance/scripts/token-session-start.js +66 -0
  100. data/.ai-assistance/skills/ai-instructions/SKILL.md +48 -0
  101. data/.ai-assistance/skills/code-specs/SKILL.md +69 -0
  102. data/.ai-assistance/skills/corporate-policies/SKILL.md +201 -0
  103. data/.ai-assistance/skills/dep-graph/SKILL.md +139 -0
  104. data/.ai-assistance/skills/ep-ai-manager/SKILL.md +417 -0
  105. data/.ai-assistance/skills/gemini-assist/SKILL.md +63 -0
  106. data/.ai-assistance/skills/gemini-assist/gemini-mcp-server.js +205 -0
  107. data/.ai-assistance/skills/gemini-assist/gemini_ask.py +1 -0
  108. data/.ai-assistance/skills/gemini-assist/gemini_ask.rb +240 -0
  109. data/.ai-assistance/skills/gemini-assist/prompts/cycle_end_review.txt +25 -0
  110. data/.ai-assistance/skills/graphql-schema-analysis/SKILL.md +261 -0
  111. data/.ai-assistance/skills/project-cycle/SKILL.md +177 -0
  112. data/.ai-assistance/skills/refactor/SKILL.md +62 -0
  113. data/.ai-assistance/skills/rubocop/SKILL.md +93 -0
  114. data/.ai-assistance/skills/ruby-scripting/SKILL.md +215 -0
  115. data/.ai-assistance/skills/spec-generation/SKILL.md +72 -0
  116. data/.ai-assistance/standards-version.json +21 -0
  117. data/.ai-assistance/token-budget.json +32 -0
  118. data/.ai-assistance/version.json +39 -0
  119. data/.claude/settings.json +146 -0
  120. data/.env.example +18 -0
  121. data/.gitattributes +15 -0
  122. data/.gitignore +13 -0
  123. data/.rubocop.yml +121 -97
  124. data/CHANGELOG.md +673 -477
  125. data/CLAUDE.md +232 -0
  126. data/Gemfile +30 -6
  127. data/Rakefile +90 -38
  128. data/docs/worklog.md +574 -0
  129. data/ecoportal-api-graphql.gemspec +40 -40
  130. data/lib/ecoportal/api/common/graphql/CLAUDE.md +36 -0
  131. data/lib/ecoportal/api/common/graphql/client.rb +1 -1
  132. data/lib/ecoportal/api/common/graphql/http_client.rb +35 -3
  133. data/lib/ecoportal/api/common/graphql/model/CLAUDE.md +28 -0
  134. data/lib/ecoportal/api/common/graphql/model/as_input.rb +8 -5
  135. data/lib/ecoportal/api/common/graphql/model/diffable/classic_diff_service.rb +3 -1
  136. data/lib/ecoportal/api/common/graphql/model/diffable/diff_service.rb +31 -23
  137. data/lib/ecoportal/api/common/graphql/model/diffable/hash_diff_nesting.rb +54 -1
  138. data/lib/ecoportal/api/graphql/CLAUDE.md +37 -0
  139. data/lib/ecoportal/api/graphql/base/CLAUDE.md +50 -0
  140. data/lib/ecoportal/api/graphql/base/ai_summary_version.rb +17 -0
  141. data/lib/ecoportal/api/graphql/base/delta_result.rb +16 -0
  142. data/lib/ecoportal/api/graphql/base/force/binding.rb +19 -0
  143. data/lib/ecoportal/api/graphql/base/force/binding_collection.rb +62 -0
  144. data/lib/ecoportal/api/graphql/base/force/collection.rb +47 -0
  145. data/lib/ecoportal/api/graphql/base/force.rb +65 -0
  146. data/lib/ecoportal/api/graphql/base/kickstand/job.rb +17 -0
  147. data/lib/ecoportal/api/graphql/base/kickstand/workflow.rb +18 -0
  148. data/lib/ecoportal/api/graphql/base/kickstand.rb +13 -0
  149. data/lib/ecoportal/api/graphql/base/page/basic.rb +38 -0
  150. data/lib/ecoportal/api/graphql/base/page/data_field/actions_list.rb +9 -0
  151. data/lib/ecoportal/api/graphql/base/page/data_field/ai_summary.rb +18 -0
  152. data/lib/ecoportal/api/graphql/base/page/data_field/checklist.rb +29 -0
  153. data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +112 -0
  154. data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -0
  155. data/lib/ecoportal/api/graphql/base/page/data_field/cross_reference.rb +54 -0
  156. data/lib/ecoportal/api/graphql/base/page/data_field/date_field.rb +23 -0
  157. data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -0
  158. data/lib/ecoportal/api/graphql/base/page/data_field/gauge.rb +19 -0
  159. data/lib/ecoportal/api/graphql/base/page/data_field/geo.rb +24 -0
  160. data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -0
  161. data/lib/ecoportal/api/graphql/base/page/data_field/law.rb +8 -0
  162. data/lib/ecoportal/api/graphql/base/page/data_field/mailbox.rb +9 -0
  163. data/lib/ecoportal/api/graphql/base/page/data_field/number.rb +20 -0
  164. data/lib/ecoportal/api/graphql/base/page/data_field/people.rb +35 -0
  165. data/lib/ecoportal/api/graphql/base/page/data_field/plain_text.rb +17 -0
  166. data/lib/ecoportal/api/graphql/base/page/data_field/rich_text.rb +26 -0
  167. data/lib/ecoportal/api/graphql/base/page/data_field/select.rb +41 -0
  168. data/lib/ecoportal/api/graphql/base/page/data_field/signature.rb +9 -0
  169. data/lib/ecoportal/api/graphql/base/page/data_field/smart_fill.rb +17 -0
  170. data/lib/ecoportal/api/graphql/base/page/data_field/table.rb +9 -0
  171. data/lib/ecoportal/api/graphql/base/page/data_field/tag_field.rb +21 -0
  172. data/lib/ecoportal/api/graphql/base/page/data_field.rb +137 -12
  173. data/lib/ecoportal/api/graphql/base/page/phased/stage.rb +68 -14
  174. data/lib/ecoportal/api/graphql/base/page/phased.rb +1 -0
  175. data/lib/ecoportal/api/graphql/base/page/section.rb +36 -0
  176. data/lib/ecoportal/api/graphql/base/page/section_collection.rb +79 -0
  177. data/lib/ecoportal/api/graphql/base/page.rb +2 -0
  178. data/lib/ecoportal/api/graphql/base/pages_workflow/action_type_selection.rb +17 -0
  179. data/lib/ecoportal/api/graphql/base/pages_workflow/callback_type.rb +28 -0
  180. data/lib/ecoportal/api/graphql/base/pages_workflow/command_change.rb +17 -0
  181. data/lib/ecoportal/api/graphql/base/pages_workflow/command_change_message.rb +15 -0
  182. data/lib/ecoportal/api/graphql/base/pages_workflow/command_es_change.rb +17 -0
  183. data/lib/ecoportal/api/graphql/base/pages_workflow/command_interface.rb +36 -0
  184. data/lib/ecoportal/api/graphql/base/pages_workflow/email_config.rb +18 -0
  185. data/lib/ecoportal/api/graphql/base/pages_workflow/escalation_level.rb +19 -0
  186. data/lib/ecoportal/api/graphql/base/pages_workflow/in_system_config.rb +16 -0
  187. data/lib/ecoportal/api/graphql/base/pages_workflow/mailbox_field_selection.rb +17 -0
  188. data/lib/ecoportal/api/graphql/base/pages_workflow/operation_interface.rb +39 -0
  189. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/assign_to.rb +27 -0
  190. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/create_page.rb +21 -0
  191. data/lib/ecoportal/api/graphql/base/pages_workflow/operations/send_notification.rb +30 -0
  192. data/lib/ecoportal/api/graphql/base/pages_workflow/people_field_selection.rb +17 -0
  193. data/lib/ecoportal/api/graphql/base/pages_workflow/recipient_config.rb +34 -0
  194. data/lib/ecoportal/api/graphql/base/pages_workflow/register_field.rb +17 -0
  195. data/lib/ecoportal/api/graphql/base/pages_workflow/task_config_selection.rb +17 -0
  196. data/lib/ecoportal/api/graphql/base/pages_workflow/time_delay_config.rb +16 -0
  197. data/lib/ecoportal/api/graphql/base/pages_workflow/trigger_interface.rb +26 -0
  198. data/lib/ecoportal/api/graphql/base/pages_workflow/triggers/conditional_logic.rb +17 -0
  199. data/lib/ecoportal/api/graphql/base/pages_workflow/user_selection.rb +16 -0
  200. data/lib/ecoportal/api/graphql/base/pages_workflow.rb +35 -0
  201. data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -0
  202. data/lib/ecoportal/api/graphql/base/preview_page.rb +23 -0
  203. data/lib/ecoportal/api/graphql/base/register.rb +18 -0
  204. data/lib/ecoportal/api/graphql/base.rb +11 -0
  205. data/lib/ecoportal/api/graphql/builder/CLAUDE.md +65 -0
  206. data/lib/ecoportal/api/graphql/builder/kickstand.rb +73 -0
  207. data/lib/ecoportal/api/graphql/builder/page.rb +210 -41
  208. data/lib/ecoportal/api/graphql/builder/register/preset_view.rb +84 -0
  209. data/lib/ecoportal/api/graphql/builder/register.rb +27 -19
  210. data/lib/ecoportal/api/graphql/builder/template.rb +80 -0
  211. data/lib/ecoportal/api/graphql/builder.rb +2 -0
  212. data/lib/ecoportal/api/graphql/compat/filter_translator.rb +107 -0
  213. data/lib/ecoportal/api/graphql/compat/page_reference.rb +23 -0
  214. data/lib/ecoportal/api/graphql/compat/pages.rb +212 -0
  215. data/lib/ecoportal/api/graphql/compat/registers.rb +84 -0
  216. data/lib/ecoportal/api/graphql/compat/response.rb +35 -0
  217. data/lib/ecoportal/api/graphql/compat/search_results.rb +33 -0
  218. data/lib/ecoportal/api/graphql/compat/stage_collection.rb +70 -0
  219. data/lib/ecoportal/api/graphql/compat/stage_view.rb +76 -0
  220. data/lib/ecoportal/api/graphql/compat.rb +17 -0
  221. data/lib/ecoportal/api/graphql/concerns/data_field_access.rb +71 -0
  222. data/lib/ecoportal/api/graphql/concerns/deprecation.rb +20 -0
  223. data/lib/ecoportal/api/graphql/concerns/fragment_definitions.rb +21 -28
  224. data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -0
  225. data/lib/ecoportal/api/graphql/concerns/snake_camel_access.rb +60 -0
  226. data/lib/ecoportal/api/graphql/concerns.rb +4 -0
  227. data/lib/ecoportal/api/graphql/connection/page.rb +11 -0
  228. data/lib/ecoportal/api/graphql/connection/pages_workflow_command.rb +13 -0
  229. data/lib/ecoportal/api/graphql/connection/preset_view.rb +11 -0
  230. data/lib/ecoportal/api/graphql/connection/preview_page.rb +11 -0
  231. data/lib/ecoportal/api/graphql/connection.rb +4 -0
  232. data/lib/ecoportal/api/graphql/file_upload/client.rb +181 -0
  233. data/lib/ecoportal/api/graphql/file_upload.rb +10 -0
  234. data/lib/ecoportal/api/graphql/fragment/action.rb +1 -1
  235. data/lib/ecoportal/api/graphql/fragment/action_category.rb +1 -1
  236. data/lib/ecoportal/api/graphql/fragment/contractor_entity.rb +1 -1
  237. data/lib/ecoportal/api/graphql/fragment/force.rb +30 -0
  238. data/lib/ecoportal/api/graphql/fragment/location_draft.rb +2 -2
  239. data/lib/ecoportal/api/graphql/fragment/location_node.rb +1 -1
  240. data/lib/ecoportal/api/graphql/fragment/locations_error.rb +1 -1
  241. data/lib/ecoportal/api/graphql/fragment/page.rb +85 -0
  242. data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +395 -0
  243. data/lib/ecoportal/api/graphql/fragment/pages.rb +15 -0
  244. data/lib/ecoportal/api/graphql/fragment/pages_workflow.rb +172 -0
  245. data/lib/ecoportal/api/graphql/fragment/pagination.rb +1 -1
  246. data/lib/ecoportal/api/graphql/fragment.rb +37 -27
  247. data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +25 -0
  248. data/lib/ecoportal/api/graphql/input/delta_input.rb +16 -0
  249. data/lib/ecoportal/api/graphql/input/page/archive.rb +14 -0
  250. data/lib/ecoportal/api/graphql/input/page/build_from_template.rb +13 -0
  251. data/lib/ecoportal/api/graphql/input/page/create_draft.rb +13 -0
  252. data/lib/ecoportal/api/graphql/input/page/create_from_template.rb +18 -0
  253. data/lib/ecoportal/api/graphql/input/page/delete_draft.rb +13 -0
  254. data/lib/ecoportal/api/graphql/input/page/publish_draft.rb +13 -0
  255. data/lib/ecoportal/api/graphql/input/page/review_task.rb +14 -0
  256. data/lib/ecoportal/api/graphql/input/page/unarchive.rb +14 -0
  257. data/lib/ecoportal/api/graphql/input/page/update.rb +140 -0
  258. data/lib/ecoportal/api/graphql/input/page.rb +26 -0
  259. data/lib/ecoportal/api/graphql/input/preset_view/create.rb +18 -0
  260. data/lib/ecoportal/api/graphql/input/preset_view/permission.rb +16 -0
  261. data/lib/ecoportal/api/graphql/input/preset_view/update.rb +16 -0
  262. data/lib/ecoportal/api/graphql/input/preset_view.rb +14 -0
  263. data/lib/ecoportal/api/graphql/input/register/create.rb +18 -0
  264. data/lib/ecoportal/api/graphql/input/register/update.rb +15 -0
  265. data/lib/ecoportal/api/graphql/input/register.rb +13 -0
  266. data/lib/ecoportal/api/graphql/input/search_conf/ai_generator.rb +234 -0
  267. data/lib/ecoportal/api/graphql/input/search_conf.rb +367 -0
  268. data/lib/ecoportal/api/graphql/input/variable_binding.rb +20 -0
  269. data/lib/ecoportal/api/graphql/input/workflow_command/add_action_tag.rb +18 -0
  270. data/lib/ecoportal/api/graphql/input/workflow_command/add_binding.rb +18 -0
  271. data/lib/ecoportal/api/graphql/input/workflow_command/add_comment_tagging_user_group.rb +18 -0
  272. data/lib/ecoportal/api/graphql/input/workflow_command/add_default_direct_strategy_user.rb +18 -0
  273. data/lib/ecoportal/api/graphql/input/workflow_command/add_default_strategy.rb +18 -0
  274. data/lib/ecoportal/api/graphql/input/workflow_command/add_direct_strategy_user.rb +18 -0
  275. data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +18 -0
  276. data/lib/ecoportal/api/graphql/input/workflow_command/add_force.rb +18 -0
  277. data/lib/ecoportal/api/graphql/input/workflow_command/add_gauge_field_stop.rb +19 -0
  278. data/lib/ecoportal/api/graphql/input/workflow_command/add_linked_field_config.rb +23 -0
  279. data/lib/ecoportal/api/graphql/input/workflow_command/add_linked_helper.rb +18 -0
  280. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation.rb +18 -0
  281. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation_direct_strategy_user.rb +18 -0
  282. data/lib/ecoportal/api/graphql/input/workflow_command/add_operation_strategy.rb +18 -0
  283. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_action_type.rb +18 -0
  284. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_filter.rb +18 -0
  285. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_people_field.rb +18 -0
  286. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_task_config.rb +18 -0
  287. data/lib/ecoportal/api/graphql/input/workflow_command/add_recipient_user.rb +18 -0
  288. data/lib/ecoportal/api/graphql/input/workflow_command/add_scheduled_callback.rb +23 -0
  289. data/lib/ecoportal/api/graphql/input/workflow_command/add_scheduled_callback_action.rb +18 -0
  290. data/lib/ecoportal/api/graphql/input/workflow_command/add_section.rb +18 -0
  291. data/lib/ecoportal/api/graphql/input/workflow_command/add_select_field_option.rb +19 -0
  292. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage.rb +18 -0
  293. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage_section.rb +18 -0
  294. data/lib/ecoportal/api/graphql/input/workflow_command/add_stage_tag.rb +18 -0
  295. data/lib/ecoportal/api/graphql/input/workflow_command/add_strategy.rb +18 -0
  296. data/lib/ecoportal/api/graphql/input/workflow_command/add_task.rb +18 -0
  297. data/lib/ecoportal/api/graphql/input/workflow_command/add_task_assignment_user_group.rb +18 -0
  298. data/lib/ecoportal/api/graphql/input/workflow_command/add_workflow_callback.rb +18 -0
  299. data/lib/ecoportal/api/graphql/input/workflow_command/collapse_section.rb +18 -0
  300. data/lib/ecoportal/api/graphql/input/workflow_command/edit_binding.rb +18 -0
  301. data/lib/ecoportal/api/graphql/input/workflow_command/edit_creator_permissions.rb +18 -0
  302. data/lib/ecoportal/api/graphql/input/workflow_command/edit_default_strategy.rb +18 -0
  303. data/lib/ecoportal/api/graphql/input/workflow_command/edit_field_configuration.rb +21 -0
  304. data/lib/ecoportal/api/graphql/input/workflow_command/edit_force.rb +18 -0
  305. data/lib/ecoportal/api/graphql/input/workflow_command/edit_gauge_field_stop.rb +19 -0
  306. data/lib/ecoportal/api/graphql/input/workflow_command/edit_linked_field_config.rb +19 -0
  307. data/lib/ecoportal/api/graphql/input/workflow_command/edit_linked_helper.rb +18 -0
  308. data/lib/ecoportal/api/graphql/input/workflow_command/edit_operation.rb +18 -0
  309. data/lib/ecoportal/api/graphql/input/workflow_command/edit_operation_strategy.rb +18 -0
  310. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page.rb +28 -0
  311. data/lib/ecoportal/api/graphql/input/workflow_command/edit_page_creator_permissions.rb +18 -0
  312. data/lib/ecoportal/api/graphql/input/workflow_command/edit_reminder.rb +18 -0
  313. data/lib/ecoportal/api/graphql/input/workflow_command/edit_required_sign_offs.rb +18 -0
  314. data/lib/ecoportal/api/graphql/input/workflow_command/edit_restrict_comment_tagging.rb +18 -0
  315. data/lib/ecoportal/api/graphql/input/workflow_command/edit_restrict_task_assignment.rb +18 -0
  316. data/lib/ecoportal/api/graphql/input/workflow_command/edit_scheduled_callback.rb +22 -0
  317. data/lib/ecoportal/api/graphql/input/workflow_command/edit_section_header.rb +18 -0
  318. data/lib/ecoportal/api/graphql/input/workflow_command/edit_select_field_option.rb +19 -0
  319. data/lib/ecoportal/api/graphql/input/workflow_command/edit_stage.rb +18 -0
  320. data/lib/ecoportal/api/graphql/input/workflow_command/edit_strategy.rb +18 -0
  321. data/lib/ecoportal/api/graphql/input/workflow_command/edit_task_due.rb +18 -0
  322. data/lib/ecoportal/api/graphql/input/workflow_command/edit_trigger.rb +18 -0
  323. data/lib/ecoportal/api/graphql/input/workflow_command/expand_section.rb +18 -0
  324. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/contractor_entities.rb +24 -0
  325. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/cross_reference.rb +23 -0
  326. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/date.rb +20 -0
  327. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/gauge.rb +20 -0
  328. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/image_gallery.rb +20 -0
  329. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/location_field.rb +24 -0
  330. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/people.rb +24 -0
  331. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/plain_text.rb +20 -0
  332. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/rich_text.rb +20 -0
  333. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/select.rb +20 -0
  334. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/signature.rb +20 -0
  335. data/lib/ecoportal/api/graphql/input/workflow_command/field_config/table.rb +25 -0
  336. data/lib/ecoportal/api/graphql/input/workflow_command/move_field.rb +18 -0
  337. data/lib/ecoportal/api/graphql/input/workflow_command/move_stage.rb +18 -0
  338. data/lib/ecoportal/api/graphql/input/workflow_command/remove_action_tag.rb +18 -0
  339. data/lib/ecoportal/api/graphql/input/workflow_command/remove_binding.rb +18 -0
  340. data/lib/ecoportal/api/graphql/input/workflow_command/remove_callback.rb +18 -0
  341. data/lib/ecoportal/api/graphql/input/workflow_command/remove_comment_tagging_user_group.rb +18 -0
  342. data/lib/ecoportal/api/graphql/input/workflow_command/remove_default_direct_strategy_user.rb +18 -0
  343. data/lib/ecoportal/api/graphql/input/workflow_command/remove_default_strategy.rb +18 -0
  344. data/lib/ecoportal/api/graphql/input/workflow_command/remove_direct_strategy_user.rb +18 -0
  345. data/lib/ecoportal/api/graphql/input/workflow_command/remove_field.rb +18 -0
  346. data/lib/ecoportal/api/graphql/input/workflow_command/remove_force.rb +18 -0
  347. data/lib/ecoportal/api/graphql/input/workflow_command/remove_gauge_field_stop.rb +17 -0
  348. data/lib/ecoportal/api/graphql/input/workflow_command/remove_linked_field_config.rb +17 -0
  349. data/lib/ecoportal/api/graphql/input/workflow_command/remove_linked_helper.rb +18 -0
  350. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation.rb +18 -0
  351. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation_direct_strategy_user.rb +18 -0
  352. data/lib/ecoportal/api/graphql/input/workflow_command/remove_operation_strategy.rb +18 -0
  353. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_action_type.rb +18 -0
  354. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_filter.rb +18 -0
  355. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_people_field.rb +18 -0
  356. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_task_config.rb +18 -0
  357. data/lib/ecoportal/api/graphql/input/workflow_command/remove_recipient_user.rb +18 -0
  358. data/lib/ecoportal/api/graphql/input/workflow_command/remove_scheduled_callback.rb +18 -0
  359. data/lib/ecoportal/api/graphql/input/workflow_command/remove_section.rb +18 -0
  360. data/lib/ecoportal/api/graphql/input/workflow_command/remove_select_field_option.rb +17 -0
  361. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage.rb +18 -0
  362. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage_section.rb +18 -0
  363. data/lib/ecoportal/api/graphql/input/workflow_command/remove_stage_tag.rb +18 -0
  364. data/lib/ecoportal/api/graphql/input/workflow_command/remove_strategy.rb +18 -0
  365. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task.rb +18 -0
  366. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_assignment_user_group.rb +18 -0
  367. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_due.rb +18 -0
  368. data/lib/ecoportal/api/graphql/input/workflow_command/remove_task_priority_level.rb +18 -0
  369. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_forces.rb +18 -0
  370. data/lib/ecoportal/api/graphql/input/workflow_command/reorder_section.rb +18 -0
  371. data/lib/ecoportal/api/graphql/input/workflow_command.rb +251 -0
  372. data/lib/ecoportal/api/graphql/input.rb +8 -0
  373. data/lib/ecoportal/api/graphql/interface/base_page.rb +100 -58
  374. data/lib/ecoportal/api/graphql/interface/location_structure/nodes.rb +27 -28
  375. data/lib/ecoportal/api/graphql/logic/base_model.rb +2 -0
  376. data/lib/ecoportal/api/graphql/logic/base_query.rb +45 -2
  377. data/lib/ecoportal/api/graphql/logic/input.rb +15 -0
  378. data/lib/ecoportal/api/graphql/model/ai_summary_version.rb +10 -0
  379. data/lib/ecoportal/api/graphql/model/organization.rb +65 -55
  380. data/lib/ecoportal/api/graphql/model/page/basic.rb +14 -0
  381. data/lib/ecoportal/api/graphql/model/page/phased.rb +82 -20
  382. data/lib/ecoportal/api/graphql/model/page.rb +1 -0
  383. data/lib/ecoportal/api/graphql/model/page_union.rb +21 -0
  384. data/lib/ecoportal/api/graphql/model/pages_workflow.rb +20 -0
  385. data/lib/ecoportal/api/graphql/model/preset_view.rb +10 -0
  386. data/lib/ecoportal/api/graphql/model/preview_page.rb +10 -0
  387. data/lib/ecoportal/api/graphql/model/register.rb +10 -0
  388. data/lib/ecoportal/api/graphql/model.rb +8 -0
  389. data/lib/ecoportal/api/graphql/mutation/ai_summary/generate.rb +45 -0
  390. data/lib/ecoportal/api/graphql/mutation/ai_summary/submit_feedback.rb +40 -0
  391. data/lib/ecoportal/api/graphql/mutation/ai_summary.rb +13 -0
  392. data/lib/ecoportal/api/graphql/mutation/kickstand/bulk_update_jobs.rb +43 -0
  393. data/lib/ecoportal/api/graphql/mutation/kickstand/bulk_update_workflows.rb +43 -0
  394. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_job.rb +39 -0
  395. data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +39 -0
  396. data/lib/ecoportal/api/graphql/mutation/kickstand/start_job.rb +39 -0
  397. data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +39 -0
  398. data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +39 -0
  399. data/lib/ecoportal/api/graphql/mutation/kickstand.rb +18 -0
  400. data/lib/ecoportal/api/graphql/mutation/location_structure/apply_commands.rb +3 -3
  401. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/add_commands.rb +2 -2
  402. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/create.rb +2 -2
  403. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/drop_bad_commands.rb +3 -3
  404. data/lib/ecoportal/api/graphql/mutation/location_structure/draft/publish.rb +3 -3
  405. data/lib/ecoportal/api/graphql/mutation/page/approve_review_task.rb +40 -0
  406. data/lib/ecoportal/api/graphql/mutation/page/archive.rb +40 -0
  407. data/lib/ecoportal/api/graphql/mutation/page/batch_update_review_task.rb +40 -0
  408. data/lib/ecoportal/api/graphql/mutation/page/build_from_template.rb +50 -0
  409. data/lib/ecoportal/api/graphql/mutation/page/create_draft.rb +40 -0
  410. data/lib/ecoportal/api/graphql/mutation/page/create_from_template.rb +43 -0
  411. data/lib/ecoportal/api/graphql/mutation/page/delete_draft.rb +40 -0
  412. data/lib/ecoportal/api/graphql/mutation/page/execute_force_commands.rb +69 -0
  413. data/lib/ecoportal/api/graphql/mutation/page/execute_workflow_commands.rb +51 -0
  414. data/lib/ecoportal/api/graphql/mutation/page/publish_draft.rb +40 -0
  415. data/lib/ecoportal/api/graphql/mutation/page/reject_review_task.rb +40 -0
  416. data/lib/ecoportal/api/graphql/mutation/page/restart_review_task.rb +40 -0
  417. data/lib/ecoportal/api/graphql/mutation/page/unarchive.rb +40 -0
  418. data/lib/ecoportal/api/graphql/mutation/page/undo_review_task.rb +40 -0
  419. data/lib/ecoportal/api/graphql/mutation/page/update.rb +40 -0
  420. data/lib/ecoportal/api/graphql/mutation/page/update_variable_bindings.rb +44 -0
  421. data/lib/ecoportal/api/graphql/mutation/page.rb +28 -0
  422. data/lib/ecoportal/api/graphql/mutation/preset_view/create.rb +35 -0
  423. data/lib/ecoportal/api/graphql/mutation/preset_view/destroy.rb +35 -0
  424. data/lib/ecoportal/api/graphql/mutation/preset_view/permission.rb +37 -0
  425. data/lib/ecoportal/api/graphql/mutation/preset_view/update.rb +35 -0
  426. data/lib/ecoportal/api/graphql/mutation/preset_view.rb +15 -0
  427. data/lib/ecoportal/api/graphql/mutation/register/create.rb +35 -0
  428. data/lib/ecoportal/api/graphql/mutation/register/destroy.rb +35 -0
  429. data/lib/ecoportal/api/graphql/mutation/register/update.rb +35 -0
  430. data/lib/ecoportal/api/graphql/mutation/register.rb +14 -0
  431. data/lib/ecoportal/api/graphql/mutation/smart_fill/generate.rb +36 -0
  432. data/lib/ecoportal/api/graphql/mutation/smart_fill/submit_feedback.rb +40 -0
  433. data/lib/ecoportal/api/graphql/mutation/smart_fill.rb +13 -0
  434. data/lib/ecoportal/api/graphql/mutation/template/create.rb +39 -0
  435. data/lib/ecoportal/api/graphql/mutation/template/create_related_page.rb +46 -0
  436. data/lib/ecoportal/api/graphql/mutation/template/destroy_related_page.rb +43 -0
  437. data/lib/ecoportal/api/graphql/mutation/template/publish.rb +39 -0
  438. data/lib/ecoportal/api/graphql/mutation/template/unpublish.rb +39 -0
  439. data/lib/ecoportal/api/graphql/mutation/template/update.rb +43 -0
  440. data/lib/ecoportal/api/graphql/mutation/template/update_information.rb +43 -0
  441. data/lib/ecoportal/api/graphql/mutation/template.rb +18 -0
  442. data/lib/ecoportal/api/graphql/mutation.rb +8 -0
  443. data/lib/ecoportal/api/graphql/payload/ai_summary_generate.rb +12 -0
  444. data/lib/ecoportal/api/graphql/payload/execute_workflow_commands.rb +36 -0
  445. data/lib/ecoportal/api/graphql/payload/force_commands.rb +31 -0
  446. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_jobs.rb +36 -0
  447. data/lib/ecoportal/api/graphql/payload/kickstand/bulk_update_workflows.rb +36 -0
  448. data/lib/ecoportal/api/graphql/payload/kickstand/job.rb +13 -0
  449. data/lib/ecoportal/api/graphql/payload/kickstand/workflow.rb +13 -0
  450. data/lib/ecoportal/api/graphql/payload/kickstand.rb +15 -0
  451. data/lib/ecoportal/api/graphql/payload/location_structure/draft/create.rb +33 -34
  452. data/lib/ecoportal/api/graphql/payload/ok_payload.rb +21 -0
  453. data/lib/ecoportal/api/graphql/payload/page/archive.rb +13 -0
  454. data/lib/ecoportal/api/graphql/payload/page/build_from_template.rb +13 -0
  455. data/lib/ecoportal/api/graphql/payload/page/create_from_template.rb +13 -0
  456. data/lib/ecoportal/api/graphql/payload/page/draft.rb +13 -0
  457. data/lib/ecoportal/api/graphql/payload/page/review_task.rb +13 -0
  458. data/lib/ecoportal/api/graphql/payload/page/unarchive.rb +13 -0
  459. data/lib/ecoportal/api/graphql/payload/page/update.rb +13 -0
  460. data/lib/ecoportal/api/graphql/payload/page/update_variable_bindings.rb +13 -0
  461. data/lib/ecoportal/api/graphql/payload/page.rb +19 -0
  462. data/lib/ecoportal/api/graphql/payload/preset_view.rb +11 -0
  463. data/lib/ecoportal/api/graphql/payload/register.rb +11 -0
  464. data/lib/ecoportal/api/graphql/payload/template/create.rb +13 -0
  465. data/lib/ecoportal/api/graphql/payload/template/create_related_page.rb +13 -0
  466. data/lib/ecoportal/api/graphql/payload/template/destroy_related_page.rb +13 -0
  467. data/lib/ecoportal/api/graphql/payload/template/publish.rb +13 -0
  468. data/lib/ecoportal/api/graphql/payload/template/unpublish.rb +13 -0
  469. data/lib/ecoportal/api/graphql/payload/template/update.rb +13 -0
  470. data/lib/ecoportal/api/graphql/payload/template/update_information.rb +13 -0
  471. data/lib/ecoportal/api/graphql/payload/template.rb +18 -0
  472. data/lib/ecoportal/api/graphql/payload.rb +11 -0
  473. data/lib/ecoportal/api/graphql/query/action.rb +1 -1
  474. data/lib/ecoportal/api/graphql/query/action_categories.rb +1 -1
  475. data/lib/ecoportal/api/graphql/query/actions.rb +2 -2
  476. data/lib/ecoportal/api/graphql/query/contractor_entities.rb +1 -1
  477. data/lib/ecoportal/api/graphql/query/file_upload_signature.rb +76 -0
  478. data/lib/ecoportal/api/graphql/query/location_structure/draft.rb +2 -2
  479. data/lib/ecoportal/api/graphql/query/location_structure.rb +1 -1
  480. data/lib/ecoportal/api/graphql/query/location_structures.rb +1 -1
  481. data/lib/ecoportal/api/graphql/query/page.rb +45 -0
  482. data/lib/ecoportal/api/graphql/query/page_delta.rb +47 -0
  483. data/lib/ecoportal/api/graphql/query/page_with_forces.rb +43 -0
  484. data/lib/ecoportal/api/graphql/query/pages.rb +59 -0
  485. data/lib/ecoportal/api/graphql/query/pages_workflow_commands.rb +59 -0
  486. data/lib/ecoportal/api/graphql/query/register_preset_views.rb +78 -0
  487. data/lib/ecoportal/api/graphql/query/register_preview_pages.rb +83 -0
  488. data/lib/ecoportal/api/graphql/query/templates.rb +53 -0
  489. data/lib/ecoportal/api/graphql/query.rb +11 -0
  490. data/lib/ecoportal/api/graphql.rb +60 -2
  491. data/lib/ecoportal/api/graphql_version.rb +5 -5
  492. data/scripts/auto-worker-scheduler.sh +386 -0
  493. data/tests/contractor_entity_create.rb +19 -19
  494. data/tests/contractor_entity_udpate.rb +20 -20
  495. data/tests/dump_page_model.rb +74 -0
  496. data/tests/loc_structure_get.rb +1 -2
  497. data/tests/loc_structure_update.rb +51 -51
  498. data/tests/loc_structures_get.rb +15 -15
  499. metadata +436 -5
@@ -0,0 +1,868 @@
1
+ # Search Filters — Code Spec
2
+
3
+ *Extracted from Insomnia_2026-06-07.yaml — real production queries.*
4
+ *Cross-referenced with Confluence APIDOCS partial filter reference.*
5
+
6
+ ---
7
+
8
+ ## GOTCHA — register search matches on the field's GraphQL DEFINITION key (VERIFIED, 2026-07-02)
9
+
10
+ A register/field filter matches on the field's **definition key** (e.g. `plain_text.zc366f454`),
11
+ NOT on the field `name:`. Under APIv2 a filter could carry `{ name:, key: }` and v2 resolved the
12
+ field **loosely by name**, so a wrong/stale `key:` still worked. **On GraphQL the literal `key:` is
13
+ used** — a wrong key silently matches nothing (0 results), which for an upsert means "not found →
14
+ create" → **duplicate pages every run**. This bit the jamestrong cans-upsert live (2026-07-01): the
15
+ case hardcoded `plain_text.z5573fdc2` while the real `Identifier:` key is `plain_text.zc366f454`,
16
+ so the existence search never matched and dups accumulated. Always take the field key from a known-
17
+ good source (a working register filter URL / introspection), not an APIv2-era hardcode.
18
+
19
+ **Exact vs partial (RESOLVED 2026-07-02):** the right op for an exact-id lookup on a plain_text
20
+ field is **`is_filter`** (whole-value equality on the `.exact` keyword subfield) — NOT `match_filter`
21
+ (analyzed/partial, which missed the exact SKU → 0 results) and NOT `exact_filter` (which does not
22
+ exist for plain_text register fields). `contains_filter` is substring (what the UI exposes). The gem
23
+ now translates a v2-style `type: 'exact_filter'` (or `'is_filter'`) → GraphQL `is_filter` with the
24
+ `membranes.<type>` path derived from the key (`Compat::FilterTranslator#translate_exact_filter`).
25
+ CAVEAT: `is_filter` on plain_text is **case-insensitive** and matches only the **first 64 chars**
26
+ (values lowercased + truncated at index). No case-sensitive / >64-char exact op exists. Prefer keying
27
+ integration upserts on `externalId` (server-enforced unique) over data-field search where possible —
28
+ see `ecoPortal_architecture/02_data_model.md`.
29
+
30
+ ---
31
+
32
+ ## Two Query Modalities
33
+
34
+ ### 1. Org search — `currentOrganization { pages(...) }`
35
+
36
+ ```graphql
37
+ query OrgSearchPages(
38
+ $searchConf: Search,
39
+ $templatesOnly: Boolean,
40
+ $showHiddenData: Boolean,
41
+ $first: Int,
42
+ $last: Int,
43
+ $after: String,
44
+ $before: String
45
+ ) {
46
+ currentOrganization {
47
+ pages(
48
+ searchConf: $searchConf,
49
+ templatesOnly: $templatesOnly,
50
+ showHiddenData: $showHiddenData,
51
+ first: $first,
52
+ last: $last,
53
+ after: $after,
54
+ before: $before
55
+ ) {
56
+ totalCount
57
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
58
+ nodes { ...PageUnion }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ - **Source:** PostgreSQL DB, rendered by API layer
65
+ - **Returns:** `PageUnion` (`BasicPage | PhasedPage`) — full data including `patchVer`,
66
+ field IDs, all typed data field models
67
+ - **Performance:** ~3s/page on render (slow for large result sets)
68
+ - **Archived:** controlled via `searchConf` filters (e.g. `state_filter` or no filter)
69
+ - **Extra args:** `templatesOnly: Boolean`, `showHiddenData: Boolean`
70
+ - **Best for:** Pre-update fetch, pre-create fetch, finding pages when field IDs are needed
71
+
72
+ ### 2. Register search — `currentOrganization { register(id:) { previewPages(...) } }`
73
+
74
+ ```graphql
75
+ query SearchRegisterPages(
76
+ $registerId: ID!,
77
+ $presetId: ID,
78
+ $after: String,
79
+ $search: Search,
80
+ $includeArchived: Boolean = false
81
+ ) {
82
+ currentOrganization {
83
+ register(id: $registerId) {
84
+ previewPages(
85
+ presetViewId: $presetId,
86
+ after: $after,
87
+ searchConf: $search,
88
+ includeArchived: $includeArchived
89
+ ) {
90
+ totalCount
91
+ pageInfo { endCursor hasNextPage }
92
+ nodes { ...PreviewPage }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ - **Source:** Elasticsearch index (fast — sub-second)
100
+ - **Returns:** `PreviewPage` — field preview values only. **No field IDs, no `patchVer`,
101
+ no typed field models.** Cannot be used to build mutation inputs.
102
+ - **Archived:** `includeArchived: Boolean` parameter — defaults to `false`, can be `true`
103
+ - **Extra args:** `presetViewId: ID` — applies a register PageView preset configuration
104
+ - **Field name:** `previewPages`, not `pages`
105
+ - **Best for:** Fast lookup of page IDs by metadata before switching to org search
106
+
107
+ ---
108
+
109
+ ## SearchConf Structure
110
+
111
+ The `searchConf` / `$search` argument type is `Search` — a Hash, not a typed GraphQL
112
+ input model. The top-level structure is:
113
+
114
+ ```json
115
+ {
116
+ "filters": [ <filter_object>, ... ],
117
+ "sorters": { "key": "field_name", "direction": "asc" | "desc" },
118
+ "query": "optional full-text search string"
119
+ }
120
+ ```
121
+
122
+ **Note on `filters`:** Can be an array of filter objects (standard) or a single filter
123
+ object (observed in some older contractor queries). Prefer array form.
124
+
125
+ **Note on `sorters`:** Can be a single sorter object or an array. Key field is `key`
126
+ (not `fieldName`).
127
+
128
+ ---
129
+
130
+ ## Filter Operations
131
+
132
+ All operations are **snake_case strings** in the `operation` field.
133
+
134
+ ### `exact_filter` — equality match
135
+
136
+ ```json
137
+ {
138
+ "operation": "exact_filter",
139
+ "params": {
140
+ "key": "external_id",
141
+ "value": "1223"
142
+ }
143
+ }
144
+ ```
145
+
146
+ Common keys: `external_id`, `state`, `creator_id`, `template_id`
147
+
148
+ ### `date_filter` — date range
149
+
150
+ ```json
151
+ {
152
+ "operation": "date_filter",
153
+ "params": {
154
+ "key": "updated_at",
155
+ "gte": "2025-01-01T00:00:00+00:00",
156
+ "time_zone": "UTC"
157
+ }
158
+ }
159
+ ```
160
+
161
+ Params: `key` (field), `gte` (from), `lte` (to), `time_zone`. Date format: ISO8601.
162
+ Common keys: `updated_at`, `created_at`
163
+
164
+ **Human date mode** — instead of ISO8601 bounds, use a named time window via `mode`:
165
+
166
+ ```json
167
+ {
168
+ "operation": "date_filter",
169
+ "params": {
170
+ "key": "updated_at",
171
+ "mode": "last_month",
172
+ "time_zone": "Pacific/Auckland"
173
+ }
174
+ }
175
+ ```
176
+
177
+ Supported modes (from `HumanDateFilterable` concern in backend):
178
+
179
+ | Category | Modes |
180
+ |---|---|
181
+ | Past relative | `last_year`, `last_quarter`, `last_month`, `last_week` |
182
+ | Past counted | `last_days` (+`days_count`), `last_hours` (+`hours_count`), `last_months` (+`months_count`) |
183
+ | Current period | `current_year`, `current_quarter`, `current_month`, `current_week`, `current_day` |
184
+ | Future relative | `next_year`, `next_quarter`, `next_month`, `next_week` |
185
+ | Future counted | `next_days` (+`days_count`), `next_hours` (+`hours_count`), `next_months` (+`months_count`) |
186
+ | Point-in-time | `exact` (+`days_count`), `less_than` (+`days_count`), `more_than` (+`days_count`), `between` (+`days_count`) |
187
+ | Financial year | `last_financial_year`, `current_financial_year`, `next_financial_year`, `custom_financial_year` (+`year`) |
188
+ | Open-ended | `past`, `future` |
189
+
190
+ Extra params used with some modes: `days_count`, `hours_count`, `months_count`, `year`, `time_zone` (default: `"Pacific/Auckland"`).
191
+
192
+ ### `register_filter` — scope to one or more registers (org search only)
193
+
194
+ ```json
195
+ {
196
+ "operation": "register_filter",
197
+ "params": {
198
+ "ids": ["registerId1", "registerId2"]
199
+ }
200
+ }
201
+ ```
202
+
203
+ Use this in the org search to narrow results to a specific register without switching
204
+ to the register-scoped query. Equivalent to `currentOrganization.pages` filtered by register.
205
+
206
+ ### `and_filter` — explicit AND group
207
+
208
+ ```json
209
+ {
210
+ "operation": "and_filter",
211
+ "params": {
212
+ "filters": [
213
+ { "operation": "exact_filter", "params": { "key": "external_id", "value": "X" } },
214
+ { "operation": "date_filter", "params": { "key": "updated_at", "gte": "2025-01-01" } }
215
+ ]
216
+ }
217
+ }
218
+ ```
219
+
220
+ Top-level `filters` array is implicitly AND. Use `and_filter` explicitly when nesting
221
+ inside an `or_filter`.
222
+
223
+ ### `or_filter` — OR group
224
+
225
+ ```json
226
+ {
227
+ "operation": "or_filter",
228
+ "params": {
229
+ "filters": [
230
+ { "operation": "exact_filter", "params": { "key": "external_id", "value": "3333" } },
231
+ { "operation": "exact_filter", "params": { "key": "external_id", "value": "TMS00323" } }
232
+ ]
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### `one_of_filter` — match any value in a set
238
+
239
+ More concise than an `or_filter` of `exact_filter` calls when matching the same field
240
+ against multiple values.
241
+
242
+ ```json
243
+ {
244
+ "operation": "one_of_filter",
245
+ "params": {
246
+ "key": "external_id",
247
+ "values": ["3333", "TMS00323", "ABC-001"]
248
+ }
249
+ }
250
+ ```
251
+
252
+ Ruby: `SearchConf.new.filter({ operation: 'one_of_filter', params: { key: 'external_id', values: [...] } })`
253
+
254
+ ### `none_of_filter` — exclude all values in a set
255
+
256
+ ```json
257
+ {
258
+ "operation": "none_of_filter",
259
+ "params": {
260
+ "key": "state",
261
+ "values": ["inactive", "complete"]
262
+ }
263
+ }
264
+ ```
265
+
266
+ ### `has_any_filter` — field has any non-null/non-empty value
267
+
268
+ Most common frontend filter. Checks that a field is populated.
269
+
270
+ ```json
271
+ {
272
+ "operation": "has_any_filter",
273
+ "params": {
274
+ "key": "external_id"
275
+ }
276
+ }
277
+ ```
278
+
279
+ ### `match_filter` — full-text / partial text match
280
+
281
+ For text fields where you want contains/partial matching rather than exact equality.
282
+
283
+ ```json
284
+ {
285
+ "operation": "match_filter",
286
+ "params": {
287
+ "key": "name",
288
+ "value": "safety inspection"
289
+ }
290
+ }
291
+ ```
292
+
293
+ ### `exists_filter` / `doesnt_exist_filter` — field presence check
294
+
295
+ ```json
296
+ { "operation": "exists_filter", "params": { "field": "external_id" } }
297
+ { "operation": "doesnt_exist_filter", "params": { "field": "external_id" } }
298
+ ```
299
+
300
+ Note: uses `field` (not `key`) in params.
301
+
302
+ ### `boolean_filter` — boolean field match
303
+
304
+ ```json
305
+ {
306
+ "operation": "boolean_filter",
307
+ "params": {
308
+ "key": "archived",
309
+ "value": true
310
+ }
311
+ }
312
+ ```
313
+
314
+ ### `numeric_range_filter` — numeric range
315
+
316
+ ```json
317
+ {
318
+ "operation": "numeric_range_filter",
319
+ "params": {
320
+ "key": "mould_counter",
321
+ "gte": 5,
322
+ "lte": 100
323
+ }
324
+ }
325
+ ```
326
+
327
+ ### `isnt_exact_filter` — negated exact match
328
+
329
+ ```json
330
+ {
331
+ "operation": "isnt_exact_filter",
332
+ "params": {
333
+ "key": "state",
334
+ "value": "complete"
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### `not_register_filter` — exclude registers
340
+
341
+ ```json
342
+ {
343
+ "operation": "not_register_filter",
344
+ "params": {
345
+ "ids": ["registerId1"]
346
+ }
347
+ }
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Sorters
353
+
354
+ ```json
355
+ { "key": "created_at", "direction": "asc" }
356
+ ```
357
+
358
+ Or as an array:
359
+
360
+ ```json
361
+ [{ "key": "created_at", "direction": "desc" }]
362
+ ```
363
+
364
+ Common keys: `created_at`, `updated_at`
365
+ Directions: `"asc"`, `"desc"`
366
+
367
+ ---
368
+
369
+ ## fieldName Reference — Pages
370
+
371
+ *Extracted from backend filter class analysis (`app/services/new_ep/es/filters/pages/`)
372
+ and frontend `createFilter.ts`. All keys are **snake_case**.*
373
+
374
+ ### Core metadata fields
375
+
376
+ | fieldName | Supported operations | Values / notes |
377
+ |---|---|---|
378
+ | `external_id` | `exact_filter`, `one_of_filter`, `has_any_filter`, `doesnt_exist_filter` | Unique per org |
379
+ | `state` | `exact_filter`, `one_of_filter`, `isnt_exact_filter` | `"active"`, `"inactive"`, `"complete"` |
380
+ | `name` | `match_filter`, `exact_filter` | Use `match_filter` for text/partial search |
381
+ | `creator_id` | `exact_filter`, `one_of_filter` | PersonMember ID string |
382
+ | `created_at` | `date_filter` | ISO8601 or human mode |
383
+ | `updated_at` | `date_filter` | ISO8601 or human mode |
384
+ | `template_id` | `exact_filter`, `one_of_filter` | Source template MongoDB ID |
385
+ | `id` | `exact_filter`, `one_of_filter` | Page MongoDB ID |
386
+
387
+ ### Location fields
388
+
389
+ | fieldName | Supported operations | Notes |
390
+ |---|---|---|
391
+ | `location_id` | `exact_filter`, `one_of_filter` | Specific location node — **no** ancestor traversal |
392
+ | `all_location_ids` | `exact_filter`, `one_of_filter`, `has_any_filter` | Includes ancestor location IDs — broader match |
393
+
394
+ **Prefer `all_location_ids`** when you want "pages assigned to location X or any of its children".
395
+ Use `location_id` only when you need an exact location match with no ancestor roll-up.
396
+
397
+ ### Tag / categorisation fields
398
+
399
+ | fieldName | Supported operations | Notes |
400
+ |---|---|---|
401
+ | `other_tags` | `exact_filter`, `one_of_filter`, `has_any_filter` | Free-form tag strings |
402
+ | `source` | `exact_filter` | Page source classification |
403
+ | `mould_counter` | `exact_filter`, `numeric_range_filter` | Sequence counter |
404
+
405
+ ### Boolean / presence flags
406
+
407
+ | fieldName | Supported operations | Notes |
408
+ |---|---|---|
409
+ | `archived` | `boolean_filter`, `exact_filter` | `true` / `false` |
410
+ | `draft` | `boolean_filter`, `exact_filter` | Draft state |
411
+
412
+ ### Sorter fieldNames (common)
413
+
414
+ | fieldName | Notes |
415
+ |---|---|
416
+ | `created_at` | |
417
+ | `updated_at` | |
418
+ | `name` | Alphabetical |
419
+ | `mould_counter` | Sequence order |
420
+
421
+ ### Operations not currently in `Input::SearchConf`
422
+
423
+ The backend supports these operations confirmed from production frontend code,
424
+ not yet wrapped as `SearchConf::*` value objects. Pass raw hashes via `SearchConf#filter`:
425
+
426
+ | Operation | Key param | Use case |
427
+ |---|---|---|
428
+ | `one_of_filter` | `key`, `values` | Match any of N values for one field |
429
+ | `none_of_filter` | `key`, `values` | Exclude N values |
430
+ | `has_any_filter` | `key` | Field has any non-empty value |
431
+ | `match_filter` | `key`, `value` | Partial/full-text match |
432
+ | `exists_filter` | `field` | Field is present in ES document |
433
+ | `doesnt_exist_filter` | `field` | Field is absent |
434
+ | `boolean_filter` | `key`, `value` | True/false field |
435
+ | `numeric_range_filter` | `key`, `gte`, `lte` | Numeric bounds |
436
+ | `isnt_exact_filter` | `key`, `value` | Negated exact match |
437
+ | `not_register_filter` | `ids` | Exclude pages from registers |
438
+
439
+ Raw hash usage:
440
+ ```ruby
441
+ conf = SearchConf.new.filter(
442
+ { 'operation' => 'one_of_filter', 'params' => { 'key' => 'state', 'values' => ['active', 'complete'] } }
443
+ )
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Parametrization Patterns
449
+
450
+ `SearchConf#with(**substitutions)` returns a cloned `SearchConf` with placeholder
451
+ symbols replaced throughout all nested filters. Use this to build reusable filter
452
+ templates where only target values differ between calls.
453
+
454
+ ### Template for find-by-externalId in any register
455
+
456
+ ```ruby
457
+ FIND_BY_EXT_ID = SearchConf.new
458
+ .filter(SearchConf::Register.new(:REGISTER_ID))
459
+ .filter(SearchConf::Exact.new(:external_id, :EXT_ID))
460
+
461
+ # Instantiate for a specific register + ID
462
+ query_conf = FIND_BY_EXT_ID.with(REGISTER_ID: 'reg_abc', EXT_ID: '1223')
463
+ result = org.pages(searchConf: query_conf.to_h, first: 1)
464
+ ```
465
+
466
+ ### Template for date-range sweep with any field
467
+
468
+ ```ruby
469
+ DATE_SWEEP = SearchConf.new
470
+ .filter(SearchConf::Register.new(:REGISTER_ID))
471
+ .filter(SearchConf::DateRange.new(:updated_at, gte: :FROM_DATE, lte: :TO_DATE))
472
+ .sort(:updated_at, :asc)
473
+
474
+ last_week = DATE_SWEEP.with(
475
+ REGISTER_ID: 'reg_abc',
476
+ FROM_DATE: '2025-06-01T00:00:00+12:00',
477
+ TO_DATE: '2025-06-07T23:59:59+12:00'
478
+ )
479
+ ```
480
+
481
+ ### Template for multi-state search
482
+
483
+ ```ruby
484
+ ACTIVE_IN_REGISTER = SearchConf.new
485
+ .filter(SearchConf::Register.new(:REGISTER_ID))
486
+ .filter(SearchConf::Exact.new(:state, 'active'))
487
+
488
+ # Reuse across registers — only REGISTER_ID substituted
489
+ reg1_results = ACTIVE_IN_REGISTER.with(REGISTER_ID: 'reg_1')
490
+ reg2_results = ACTIVE_IN_REGISTER.with(REGISTER_ID: 'reg_2')
491
+ ```
492
+
493
+ ### Substitution rules
494
+
495
+ - Placeholder: any Ruby symbol passed as a parameter value (e.g. `:REGISTER_ID`)
496
+ - `with(**kv)` traverses all nested filters recursively and replaces matching symbol keys
497
+ - The original `SearchConf` is never mutated — `with` always returns a fresh clone
498
+ - Substitutions that don't match any placeholder are silently ignored
499
+
500
+ ---
501
+
502
+ ## SearchConf Ruby API — Quick Reference
503
+
504
+ `Input::SearchConf` is the Ruby value object that builds `searchConf` hashes.
505
+ All classes live under `Ecoportal::API::GraphQL::Input::SearchConf`.
506
+
507
+ ### Wrapped filter classes (produce correct `to_h` output)
508
+
509
+ | Ruby class | operation string | Constructor |
510
+ |---|---|---|
511
+ | `SearchConf::Exact` | `exact_filter` | `Exact.new(:key, value)` |
512
+ | `SearchConf::DateRange` | `date_filter` | `DateRange.new(:key, gte: iso8601, lte: iso8601, time_zone: 'UTC')` |
513
+ | `SearchConf::Register` | `register_filter` | `Register.new(register_id)` |
514
+ | `SearchConf::And` | `and_filter` | `And.new(filter1, filter2, ...)` |
515
+ | `SearchConf::Or` | `or_filter` | `Or.new(filter1, filter2, ...)` |
516
+
517
+ ### Builder methods on `SearchConf` instance
518
+
519
+ ```ruby
520
+ conf = SearchConf.new
521
+ .filter(SearchConf::Exact.new(:external_id, 'ABC')) # add one filter
522
+ .filter(SearchConf::Register.new('reg_id')) # chain multiple
523
+ .sort(:created_at, :desc) # add sorter
524
+ .sort(:name, :asc) # multiple sorters → array
525
+ .query('search text') # full-text query string
526
+
527
+ conf.to_h # → Hash for the searchConf argument
528
+ conf.as_json # → same (JSON serialisation)
529
+ ```
530
+
531
+ ### Common patterns
532
+
533
+ ```ruby
534
+ # Find by externalId in a register
535
+ SearchConf.new
536
+ .filter(SearchConf::Register.new('REG_ID'))
537
+ .filter(SearchConf::Exact.new(:external_id, 'ABC-123'))
538
+
539
+ # Find active pages updated after a date
540
+ SearchConf.new
541
+ .filter(SearchConf::Register.new('REG_ID'))
542
+ .filter(SearchConf::Exact.new(:state, 'active'))
543
+ .filter(SearchConf::DateRange.new(:updated_at, gte: '2025-01-01T00:00:00Z'))
544
+ .sort(:updated_at, :asc)
545
+
546
+ # Find pages with one of several externalIds (OR logic)
547
+ SearchConf.new
548
+ .filter(SearchConf::Or.new(
549
+ SearchConf::Exact.new(:external_id, 'ABC'),
550
+ SearchConf::Exact.new(:external_id, 'DEF')
551
+ ))
552
+
553
+ # Explicit AND (nesting inside OR — top-level array is already AND)
554
+ SearchConf.new
555
+ .filter(SearchConf::Or.new(
556
+ SearchConf::And.new(
557
+ SearchConf::Exact.new(:state, 'active'),
558
+ SearchConf::Register.new('REG_1')
559
+ ),
560
+ SearchConf::And.new(
561
+ SearchConf::Exact.new(:state, 'complete'),
562
+ SearchConf::Register.new('REG_2')
563
+ )
564
+ ))
565
+
566
+ # Reusable template with #with (symbol placeholders)
567
+ TEMPLATE = SearchConf.new
568
+ .filter(SearchConf::Register.new(:REG_ID))
569
+ .filter(SearchConf::Exact.new(:external_id, :EXT_ID))
570
+
571
+ conf = TEMPLATE.with(REG_ID: 'reg_abc', EXT_ID: 'ABC-123')
572
+ ```
573
+
574
+ ### For operations NOT yet wrapped (pass raw hash)
575
+
576
+ ```ruby
577
+ # one_of_filter — match any of N values
578
+ SearchConf.new.filter(
579
+ { 'operation' => 'one_of_filter',
580
+ 'params' => { 'key' => 'state', 'values' => ['active', 'complete'] } }
581
+ )
582
+
583
+ # has_any_filter — field is populated
584
+ SearchConf.new.filter(
585
+ { 'operation' => 'has_any_filter', 'params' => { 'key' => 'external_id' } }
586
+ )
587
+
588
+ # date_filter with human mode
589
+ SearchConf.new.filter(
590
+ { 'operation' => 'date_filter',
591
+ 'params' => { 'key' => 'updated_at', 'mode' => 'last_month', 'time_zone' => 'Pacific/Auckland' } }
592
+ )
593
+ ```
594
+
595
+ All three patterns work: `SearchConf` filters accept filter objects (wrapped classes),
596
+ raw hashes (unwrapped operations), or a mix. `normalize_variables` in `Logic::BaseQuery`
597
+ calls `.to_h` automatically so you never need to call `.to_h` explicitly at the call site.
598
+
599
+ ---
600
+
601
+ ## Production Examples
602
+
603
+ ### Find page by externalId (org search)
604
+
605
+ ```json
606
+ {
607
+ "showHiddenData": true,
608
+ "templatesOnly": false,
609
+ "searchConf": {
610
+ "filters": [
611
+ {
612
+ "operation": "exact_filter",
613
+ "params": { "key": "external_id", "value": "1223" }
614
+ }
615
+ ]
616
+ },
617
+ "first": 10
618
+ }
619
+ ```
620
+
621
+ ### Find pages in a register updated after a date (org search)
622
+
623
+ ```json
624
+ {
625
+ "showHiddenData": true,
626
+ "templatesOnly": false,
627
+ "searchConf": {
628
+ "filters": [
629
+ {
630
+ "operation": "register_filter",
631
+ "params": { "ids": ["REGISTER_ID"] }
632
+ },
633
+ {
634
+ "operation": "date_filter",
635
+ "params": { "key": "updated_at", "gte": "2025-01-01", "time_zone": "UTC" }
636
+ }
637
+ ],
638
+ "sorters": { "key": "created_at", "direction": "asc" }
639
+ },
640
+ "first": 5
641
+ }
642
+ ```
643
+
644
+ ### Find pages by one of several externalIds (register search — OR logic)
645
+
646
+ ```json
647
+ {
648
+ "registerId": "REGISTER_ID",
649
+ "includeArchived": true,
650
+ "search": {
651
+ "sorters": { "key": "created_at", "direction": "desc" },
652
+ "filters": [
653
+ {
654
+ "operation": "or_filter",
655
+ "params": {
656
+ "filters": [
657
+ { "operation": "exact_filter", "params": { "key": "external_id", "value": "3333" } },
658
+ { "operation": "exact_filter", "params": { "key": "external_id", "value": "TMS00323" } }
659
+ ]
660
+ }
661
+ }
662
+ ]
663
+ }
664
+ }
665
+ ```
666
+
667
+ ### Contractor search by externalId (same filter pattern, different query)
668
+
669
+ ```json
670
+ {
671
+ "searchConf": {
672
+ "filters": [
673
+ {
674
+ "operation": "exact_filter",
675
+ "params": { "key": "external_id", "value": "9429041280072" }
676
+ }
677
+ ]
678
+ }
679
+ }
680
+ ```
681
+
682
+ ### Find pages by state (org search)
683
+
684
+ Filter by page lifecycle state. The `state` key is snake_case; values are lowercase strings.
685
+
686
+ ```json
687
+ {
688
+ "searchConf": {
689
+ "filters": [
690
+ {
691
+ "operation": "exact_filter",
692
+ "params": { "key": "state", "value": "active" }
693
+ }
694
+ ]
695
+ }
696
+ }
697
+ ```
698
+
699
+ **Common state values:** `"active"`, `"inactive"`, `"complete"`
700
+
701
+ **Find archived pages** — archived pages are a separate flag (`archived: true`), not a state.
702
+ To include archived pages in register search, use `includeArchived: true` on the
703
+ `previewPages` query. For org search, archived pages appear in results by default
704
+ (no separate flag needed — use a state filter if you want to restrict to one state).
705
+
706
+ **Ruby (SearchConf):**
707
+ ```ruby
708
+ SearchConf.new.filter(SearchConf::Exact.new(:state, 'active'))
709
+ ```
710
+
711
+ ### Find pages by location ID (org search)
712
+
713
+ Filter pages associated with a specific location node. The filter key is `location_id`
714
+ (confirmed from Confluence APIDOCS filter reference).
715
+
716
+ ```json
717
+ {
718
+ "searchConf": {
719
+ "filters": [
720
+ {
721
+ "operation": "exact_filter",
722
+ "params": { "key": "location_id", "value": "LOCATION_NODE_ID" }
723
+ }
724
+ ]
725
+ }
726
+ }
727
+ ```
728
+
729
+ **Note:** `location_id` targets a single specific node in the location hierarchy.
730
+ To filter by multiple locations, use an `or_filter` wrapping multiple `exact_filter` entries,
731
+ or check whether `in_filter` (listed in Confluence APIDOCS but not yet confirmed via Insomnia)
732
+ is supported.
733
+
734
+ **Ruby (SearchConf):**
735
+ ```ruby
736
+ SearchConf.new.filter(SearchConf::Exact.new(:location_id, 'NODE_ID'))
737
+ ```
738
+
739
+ ---
740
+
741
+ ## The Required Pattern: Find Before Mutate
742
+
743
+ There is no `fetch page by externalId` endpoint. externalId is unique per organisation
744
+ but there is no direct lookup — you must search. The standard workflow:
745
+
746
+ ```ruby
747
+ # Step 1: find the page ID via org search (register-scoped for speed)
748
+ conf = SearchConf.new
749
+ .filter(SearchConf::Register.new('REGISTER_ID')) # scope to register
750
+ .filter(SearchConf::Exact.new(:external_id, 'ABC-123'))
751
+
752
+ result = org.pages(searchConf: conf.to_h, first: 1)
753
+ page = result.nodes.first
754
+ # page is a PageUnion (BasicPage or PhasedPage) with full data including patchVer + field IDs
755
+
756
+ # Step 2: build mutation input from the fetched page
757
+ input = Input::Page::Update.from_model(page)
758
+ return if input.nil? # no changes
759
+
760
+ # Step 3: update
761
+ payload = api.page.update(input: input)
762
+ ```
763
+
764
+ Why `register_filter` in org search (not register search)?
765
+ - Register search (`previewPages`) is fast/ES but returns `PreviewPage` — no field IDs, no `patchVer`
766
+ - Org search with `register_filter` gives the full `PageUnion` from DB — slow but complete
767
+ - For "does this page exist?" checks, register search is fine. For "update this page", always use org search.
768
+
769
+ The `register_filter` narrows the org search to one register, which dramatically reduces
770
+ result set size and improves response time despite the ~3s/page DB render cost.
771
+
772
+ ---
773
+
774
+ ## Filter Rules — Silent Failure Traps
775
+
776
+ **snake_case is mandatory.** Operation names must be snake_case strings (e.g. `exact_filter`,
777
+ `and_filter`). If CamelCase is used (e.g. `ExactFilter`), the server returns **no error and
778
+ no results** — it silently ignores the filter. This is one of the hardest bugs to diagnose.
779
+
780
+ **`filters` should always be an Array.** The server normalises a single object to an array
781
+ internally, so a bare object happens to work in some endpoints, but always use the array
782
+ form for consistency and forward-compatibility.
783
+
784
+ ---
785
+
786
+ ## Important Observations
787
+
788
+ ### `ArchivePage` — compound mutation pattern
789
+
790
+ Archiving a page in production runs **two mutations in a single GraphQL request**:
791
+
792
+ ```graphql
793
+ mutation ArchivePage($id: ID!, $blank_external_id: Boolean = true) {
794
+ updatePage(input: { id: $id, page: { externalId: null } })
795
+ @include(if: $blank_external_id) {
796
+ item { ...corePageData }
797
+ }
798
+ archivePage(input: { id: $id }) {
799
+ item { ...corePageData }
800
+ errors { details fullMessages }
801
+ }
802
+ }
803
+ ```
804
+
805
+ The `externalId` is blanked first (to prevent duplicate externalId conflicts if the page
806
+ is later unarchived and recreated with the same ID), then the archive mutation runs.
807
+ The `@include(if: $blank_external_id)` directive makes the blank optional.
808
+
809
+ **Implication for `Mutation::Page::Archive`:** May need to support the combined pattern,
810
+ or at minimum document that callers should blank `externalId` before archiving if they
811
+ manage externalIds.
812
+
813
+ ### Two build patterns
814
+
815
+ **`Build - Model`** — older pattern, own inline fragment definitions, all 20 data field
816
+ types, no `$content`/`$fields` variables. Gives raw field structure.
817
+
818
+ **`Build - Standard`** — uses the `CommonPageUnion` fragment with
819
+ `$fields/$content/$only_content` variables. This is the production standard and the
820
+ pattern the gem's `Fragment::Pages::CommonPageUnion` is based on.
821
+
822
+ ### `typo: extact_filter`
823
+
824
+ One contractor query uses `"operation": "extact_filter"` (typo). The correct name is
825
+ `exact_filter`. The server appears to tolerate this — no results were returned rather
826
+ than an error. Worth noting for the filter builder's validation.
827
+
828
+ ---
829
+
830
+ ## Filter Builder — Operation Constants
831
+
832
+ *Confirmed from backend source (`app/services/new_ep/es/filters/global/`) and frontend
833
+ `createFilter.ts`. All are valid snake_case operation strings.*
834
+
835
+ ```ruby
836
+ # Core — wrapped by Input::SearchConf::* value objects
837
+ EXACT_FILTER = 'exact_filter'
838
+ DATE_FILTER = 'date_filter'
839
+ REGISTER_FILTER = 'register_filter'
840
+ AND_FILTER = 'and_filter'
841
+ OR_FILTER = 'or_filter'
842
+
843
+ # Confirmed from backend + frontend — pass as raw hash via SearchConf#filter
844
+ ONE_OF_FILTER = 'one_of_filter' # match any of N values
845
+ NONE_OF_FILTER = 'none_of_filter' # exclude all of N values
846
+ ALL_OF_FILTER = 'all_of_filter' # match all values (rarely needed)
847
+ HAS_ANY_FILTER = 'has_any_filter' # field has any non-empty value
848
+ MATCH_FILTER = 'match_filter' # partial text match
849
+ EXISTS_FILTER = 'exists_filter' # field present in ES document
850
+ DOESNT_EXIST_FILTER = 'doesnt_exist_filter' # field absent
851
+ BOOLEAN_FILTER = 'boolean_filter' # true/false field
852
+ NUMERIC_RANGE_FILTER = 'numeric_range_filter' # numeric gte/lte
853
+ ISNT_EXACT_FILTER = 'isnt_exact_filter' # negated exact match
854
+ NOT_REGISTER_FILTER = 'not_register_filter' # exclude registers
855
+
856
+ # Backend-confirmed, frontend-usage only (not typically used in scripts)
857
+ ARCHIVED_FILTER = 'archived_filter'
858
+ UNARCHIVED_FILTER = 'unarchived_filter'
859
+ NESTED_FILTER = 'nested_filter' # ES nested object query
860
+ ID_FILTER = 'id_filter'
861
+ PREFIX_FILTER = 'prefix_filter'
862
+ ```
863
+
864
+ **Previously noted as "not confirmed"** — status now resolved:
865
+ - `exists_filter` — **confirmed** (see above)
866
+ - `in_filter` — **not a real operation**; use `one_of_filter` instead
867
+ - `range_filter` — **not a real operation**; use `date_filter` or `numeric_range_filter`
868
+ - `ne_filter` — **not a real operation**; use `isnt_exact_filter` instead