has_helpers 5.0.2

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 (748) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +38 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +10 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.tekton/Chart.yaml +14 -0
  8. data/.tekton/templates/pipelinerun.yaml +44 -0
  9. data/.tekton/values-main.yaml +53 -0
  10. data/.tekton/values-pr.yaml +40 -0
  11. data/.tekton/values.yaml +17 -0
  12. data/.tool-versions +1 -0
  13. data/CHANGELOG.md +204 -0
  14. data/Gemfile +25 -0
  15. data/README.md +76 -0
  16. data/Rakefile +46 -0
  17. data/app/api/has_helpers/rack_api.rb +116 -0
  18. data/app/api/has_helpers/search_api.rb +173 -0
  19. data/app/assets/stylesheets/has_helpers/base.scss +5 -0
  20. data/app/assets/stylesheets/has_helpers/color_palette.scss +33 -0
  21. data/app/assets/stylesheets/has_helpers/color_palettes/default.scss +11 -0
  22. data/app/assets/stylesheets/has_helpers/components/dataset.scss +43 -0
  23. data/app/assets/stylesheets/has_helpers/mixins.scss +149 -0
  24. data/app/commands/has_helpers/cards/available_cards_command.rb +98 -0
  25. data/app/commands/has_helpers/cards/card_data_command.rb +1062 -0
  26. data/app/commands/reports/base_options.rb +79 -0
  27. data/app/commands/reports/belongs_to_options.rb +178 -0
  28. data/app/commands/reports/constant_options.rb +66 -0
  29. data/app/commands/reports/external_graphql_client.rb +116 -0
  30. data/app/commands/reports/fill_dates.rb +65 -0
  31. data/app/commands/reports/filter_option_generator.rb +186 -0
  32. data/app/commands/reports/generator.rb +310 -0
  33. data/app/commands/reports/has_many_options.rb +105 -0
  34. data/app/commands/reports/importer.rb +132 -0
  35. data/app/commands/reports/nested_attribute_options.rb +134 -0
  36. data/app/constants/color.rb +9 -0
  37. data/app/constants/file_format.rb +12 -0
  38. data/app/constants/has_helpers/cards/card_custom_field.rb +12 -0
  39. data/app/constants/has_helpers/cards/card_data_type.rb +11 -0
  40. data/app/constants/has_helpers/cards/card_identifier.rb +10 -0
  41. data/app/constants/has_helpers/cards/card_layout_type.rb +11 -0
  42. data/app/constants/has_helpers/cards/card_variant_type.rb +12 -0
  43. data/app/constants/has_helpers/link_type.rb +10 -0
  44. data/app/constants/has_helpers/notification_alert_status.rb +14 -0
  45. data/app/constants/has_helpers/notification_alert_type.rb +10 -0
  46. data/app/constants/has_helpers/operation.rb +11 -0
  47. data/app/constants/has_helpers/resource_type.rb +25 -0
  48. data/app/constants/icon.rb +22 -0
  49. data/app/constants/logical_operator.rb +7 -0
  50. data/app/constants/option.rb +10 -0
  51. data/app/constants/time_zone.rb +12 -0
  52. data/app/constructors/has_helpers/base_constructor.rb +62 -0
  53. data/app/controllers/concerns/has_helpers/service_trust/authenticated_as_service.rb +51 -0
  54. data/app/controllers/concerns/has_helpers/service_trust/graphql_authentication.rb +57 -0
  55. data/app/controllers/has_helpers/controller.rb +63 -0
  56. data/app/controllers/has_helpers/factory_controller.rb +261 -0
  57. data/app/controllers/has_helpers/graphql_extensions.rb +83 -0
  58. data/app/controllers/has_helpers/health_check_controller.rb +46 -0
  59. data/app/controllers/has_helpers/search_extensions.rb +92 -0
  60. data/app/controllers/has_helpers/service_trust/jwks_controller.rb +20 -0
  61. data/app/controllers/has_helpers/xlsx_export.rb +115 -0
  62. data/app/controllers/search_extensions.rb +5 -0
  63. data/app/graphql/has_helpers/base_type.rb +13 -0
  64. data/app/graphql/has_helpers/base_union.rb +9 -0
  65. data/app/graphql/has_helpers/enums/resource_enum.rb +24 -0
  66. data/app/graphql/has_helpers/middleware/application_access.rb +26 -0
  67. data/app/graphql/has_helpers/mutations/base.rb +27 -0
  68. data/app/graphql/has_helpers/mutations/external_data_sync.rb +72 -0
  69. data/app/graphql/has_helpers/mutations/mark_individual_alert_read.rb +31 -0
  70. data/app/graphql/has_helpers/mutations/mark_notifications_read.rb +50 -0
  71. data/app/graphql/has_helpers/mutations/toggle_favorite.rb +39 -0
  72. data/app/graphql/has_helpers/resolvers/app_nav.rb +107 -0
  73. data/app/graphql/has_helpers/resolvers/base.rb +27 -0
  74. data/app/graphql/has_helpers/resolvers/cards/available_cards.rb +31 -0
  75. data/app/graphql/has_helpers/resolvers/cards/card_data.rb +74 -0
  76. data/app/graphql/has_helpers/resolvers/cards/hover_card_data.rb +153 -0
  77. data/app/graphql/has_helpers/resolvers/favorite.rb +62 -0
  78. data/app/graphql/has_helpers/resolvers/internal/roles.rb +54 -0
  79. data/app/graphql/has_helpers/resolvers/is_favorite.rb +24 -0
  80. data/app/graphql/has_helpers/resolvers/list_notification_alerts.rb +47 -0
  81. data/app/graphql/has_helpers/resolvers/lookup.rb +36 -0
  82. data/app/graphql/has_helpers/resolvers/recently_viewed.rb +54 -0
  83. data/app/graphql/has_helpers/resolvers/resource_tab.rb +22 -0
  84. data/app/graphql/has_helpers/resolvers/search.rb +18 -0
  85. data/app/graphql/has_helpers/resolvers/search_query.rb +38 -0
  86. data/app/graphql/has_helpers/resolvers/user_application_order.rb +35 -0
  87. data/app/graphql/has_helpers/resources/organization_option_resource.rb +28 -0
  88. data/app/graphql/has_helpers/resources/organization_resource_resource.rb +18 -0
  89. data/app/graphql/has_helpers/resources/resource_group_resource.rb +10 -0
  90. data/app/graphql/has_helpers/resources/resource_resource.rb +21 -0
  91. data/app/graphql/has_helpers/resources/user_role_resource.rb +9 -0
  92. data/app/graphql/has_helpers/types/application_order_type.rb +18 -0
  93. data/app/graphql/has_helpers/types/cards/available_card_type.rb +20 -0
  94. data/app/graphql/has_helpers/types/cards/card_badge_type.rb +12 -0
  95. data/app/graphql/has_helpers/types/cards/card_config_type.rb +18 -0
  96. data/app/graphql/has_helpers/types/cards/card_data_type.rb +13 -0
  97. data/app/graphql/has_helpers/types/cards/card_entry_field_type.rb +21 -0
  98. data/app/graphql/has_helpers/types/cards/card_entry_type.rb +12 -0
  99. data/app/graphql/has_helpers/types/cards/card_field_config_type.rb +17 -0
  100. data/app/graphql/has_helpers/types/layouts/app_nav/link_section_type.rb +17 -0
  101. data/app/graphql/has_helpers/types/layouts/app_nav/link_type.rb +20 -0
  102. data/app/graphql/has_helpers/types/layouts/app_nav/sub_link_type.rb +17 -0
  103. data/app/graphql/has_helpers/types/layouts/app_nav/type.rb +16 -0
  104. data/app/graphql/has_helpers/types/lookup.rb +16 -0
  105. data/app/graphql/has_helpers/types/resource_tab/type.rb +15 -0
  106. data/app/graphql/has_helpers/types/role_type.rb +13 -0
  107. data/app/graphql/has_helpers/types/search/detail_badge_type.rb +15 -0
  108. data/app/graphql/has_helpers/types/search/detail_item_type.rb +14 -0
  109. data/app/graphql/has_helpers/types/search/detail_type.rb +16 -0
  110. data/app/graphql/has_helpers/types/search/filter_operation_enum.rb +19 -0
  111. data/app/graphql/has_helpers/types/search/input_type.rb +16 -0
  112. data/app/graphql/has_helpers/types/search/result_type.rb +72 -0
  113. data/app/graphql/has_helpers/types/search/search_filter_input_type.rb +17 -0
  114. data/app/graphql/has_helpers/types/search/type.rb +17 -0
  115. data/app/importers/has_helpers/cards/importer.rb +1223 -0
  116. data/app/lib/has_helpers/application_helper.rb +21 -0
  117. data/app/lib/has_helpers/dn/processor.rb +55 -0
  118. data/app/lib/has_helpers/dn/refresh.rb +13 -0
  119. data/app/lib/has_helpers/dn/utils/backfill_utils.rb +80 -0
  120. data/app/lib/has_helpers/dn/utils/query_utils.rb +216 -0
  121. data/app/lib/has_helpers/feature.rb +88 -0
  122. data/app/lib/has_helpers/hq_exception.rb +18 -0
  123. data/app/lib/has_helpers/index.rb +890 -0
  124. data/app/lib/has_helpers/organization_merge.rb +128 -0
  125. data/app/lib/has_helpers/query/elasticsearch.rb +20 -0
  126. data/app/lib/has_helpers/query/provider.rb +105 -0
  127. data/app/lib/has_helpers/query/sql.rb +133 -0
  128. data/app/lib/has_helpers/query.rb +14 -0
  129. data/app/lib/has_helpers/search/elasticsearch.rb +41 -0
  130. data/app/lib/has_helpers/search/elasticsearch_shared.rb +70 -0
  131. data/app/lib/has_helpers/search/provider.rb +149 -0
  132. data/app/lib/has_helpers/search/search_utils.rb +74 -0
  133. data/app/lib/has_helpers/search/sql_search.rb +219 -0
  134. data/app/lib/has_helpers/search.rb +46 -0
  135. data/app/models/ability.rb +233 -0
  136. data/app/models/has_helpers/application.rb +47 -0
  137. data/app/models/has_helpers/application_record.rb +7 -0
  138. data/app/models/has_helpers/cards/card_builder/base.rb +74 -0
  139. data/app/models/has_helpers/cards/card_builder/card_definition_builder.rb +486 -0
  140. data/app/models/has_helpers/cards/card_builder/definition_context.rb +82 -0
  141. data/app/models/has_helpers/cards/card_builder/field_builder.rb +142 -0
  142. data/app/models/has_helpers/cards/card_builder/resource_builder.rb +34 -0
  143. data/app/models/has_helpers/cards/card_definition.rb +41 -0
  144. data/app/models/has_helpers/cards/card_definition_resource.rb +19 -0
  145. data/app/models/has_helpers/cards/card_field.rb +39 -0
  146. data/app/models/has_helpers/cards/registry.rb +115 -0
  147. data/app/models/has_helpers/cards/role_card_field.rb +28 -0
  148. data/app/models/has_helpers/cards/role_resource_card.rb +41 -0
  149. data/app/models/has_helpers/dn_utils.rb +61 -0
  150. data/app/models/has_helpers/favorite.rb +20 -0
  151. data/app/models/has_helpers/master_record.rb +18 -0
  152. data/app/models/has_helpers/notification_alert.rb +52 -0
  153. data/app/models/has_helpers/organization.rb +16 -0
  154. data/app/models/has_helpers/organization_link.rb +92 -0
  155. data/app/models/has_helpers/organization_methods.rb +62 -0
  156. data/app/models/has_helpers/organization_option.rb +31 -0
  157. data/app/models/has_helpers/organization_resource.rb +45 -0
  158. data/app/models/has_helpers/pending_db_operation.rb +31 -0
  159. data/app/models/has_helpers/recently_viewed.rb +34 -0
  160. data/app/models/has_helpers/reindex_queue.rb +43 -0
  161. data/app/models/has_helpers/resource.rb +38 -0
  162. data/app/models/has_helpers/resource_group.rb +15 -0
  163. data/app/models/has_helpers/restriction.rb +15 -0
  164. data/app/models/has_helpers/restriction_operation.rb +13 -0
  165. data/app/models/has_helpers/role.rb +20 -0
  166. data/app/models/has_helpers/service_trust/audit_log.rb +20 -0
  167. data/app/models/has_helpers/service_trust/key.rb +56 -0
  168. data/app/models/has_helpers/stat.rb +13 -0
  169. data/app/models/has_helpers/user.rb +18 -0
  170. data/app/models/has_helpers/user_methods.rb +184 -0
  171. data/app/models/has_helpers/user_organization.rb +33 -0
  172. data/app/models/has_helpers/user_role.rb +27 -0
  173. data/app/models/has_helpers/user_session.rb +19 -0
  174. data/app/presenters/has_helpers/barcode_base_presenter.rb +113 -0
  175. data/app/presenters/has_helpers/base_presenter.rb +100 -0
  176. data/app/presenters/has_helpers/cacheable_item.rb +28 -0
  177. data/app/presenters/has_helpers/item_presenter.rb +52 -0
  178. data/app/presenters/has_helpers/items_presenter.rb +33 -0
  179. data/app/presenters/has_helpers/resource/action_presenter.rb +8 -0
  180. data/app/presenters/has_helpers/resource/cell_presenter.rb +52 -0
  181. data/app/presenters/has_helpers/resource/email_presenter.rb +34 -0
  182. data/app/presenters/has_helpers/resource/form_presenter.rb +76 -0
  183. data/app/presenters/has_helpers/resource/info_collection_presenter.rb +33 -0
  184. data/app/presenters/has_helpers/resource/info_presenter.rb +34 -0
  185. data/app/presenters/has_helpers/resource/list_presenter.rb +120 -0
  186. data/app/presenters/has_helpers/resource/note_presenter.rb +14 -0
  187. data/app/presenters/has_helpers/resource/row_presenter.rb +25 -0
  188. data/app/presenters/has_helpers/resource/tag_presenter.rb +31 -0
  189. data/app/presenters/has_helpers/search_presenter.rb +230 -0
  190. data/app/triggers/has_helpers/erase_denormalized_belongs_to_column_trigger.rb +85 -0
  191. data/app/triggers/has_helpers/limit_favorites.rb +97 -0
  192. data/app/utils/has_helpers/redis_configurable.rb +79 -0
  193. data/app/utils/has_helpers/sync_utils.rb +234 -0
  194. data/app/views/has_helpers/actions/_action.html.erb +31 -0
  195. data/app/views/has_helpers/alerts/_alert.html.erb +3 -0
  196. data/app/views/has_helpers/barcodes.html.erb +36 -0
  197. data/app/views/has_helpers/containers/_container.html.erb +3 -0
  198. data/app/views/has_helpers/email_notifier/system.html.erb +8 -0
  199. data/app/views/has_helpers/email_notifier/system.text.erb +7 -0
  200. data/app/views/has_helpers/enumerables/_enumerable.html.erb +1 -0
  201. data/app/views/has_helpers/icons/_icon.html.erb +1 -0
  202. data/app/views/has_helpers/render_objects/_render_object.html.erb +1 -0
  203. data/app/views/has_helpers/services/_service.html.erb +1 -0
  204. data/app/views/has_helpers/text_nodes/_text_node.html.erb +4 -0
  205. data/app/views/has_helpers/ui_attributes/_html_attrs.html.erb +1 -0
  206. data/app/workers/elasticsearch_process_queue.rb +11 -0
  207. data/app/workers/elasticsearch_reindex.rb +11 -0
  208. data/app/workers/elasticsearch_reindex_scheduler.rb +35 -0
  209. data/app/workers/good_job/configurable_backfill.rb +64 -0
  210. data/app/workers/good_job/cron_denormalization_stats_manager.rb +39 -0
  211. data/app/workers/good_job/denormalization_stats_worker.rb +57 -0
  212. data/app/workers/good_job/dn_update_worker.rb +30 -0
  213. data/app/workers/good_job/dn_worker.rb +47 -0
  214. data/app/workers/good_job/elasticsearch_old_index_cleaner_worker.rb +113 -0
  215. data/app/workers/good_job/elasticsearch_on_finish_reindex.rb +26 -0
  216. data/app/workers/good_job/elasticsearch_process_batch.rb +78 -0
  217. data/app/workers/good_job/elasticsearch_process_change_logs_master_worker.rb +27 -0
  218. data/app/workers/good_job/elasticsearch_process_change_logs_worker.rb +155 -0
  219. data/app/workers/good_job/elasticsearch_process_queue.rb +50 -0
  220. data/app/workers/good_job/elasticsearch_reindex.rb +31 -0
  221. data/app/workers/has_helpers/cards/orphan_card_sweeper.rb +28 -0
  222. data/app/workers/has_helpers/db_operations/clean_stats_worker.rb +42 -0
  223. data/app/workers/has_helpers/db_operations/helper_worker.rb +131 -0
  224. data/app/workers/has_helpers/db_operations/main_worker.rb +164 -0
  225. data/app/workers/has_helpers/db_operations/operations.rb +134 -0
  226. data/app/workers/has_helpers/db_operations/update_operations_stats_resume_worker.rb +35 -0
  227. data/app/workers/has_helpers/default_worker.rb +17 -0
  228. data/app/workers/has_helpers/good_job/notification_alert_cleanup_worker.rb +43 -0
  229. data/app/workers/has_helpers/service_trust/audit_log_cleanup_worker.rb +19 -0
  230. data/app/workers/sidekiq/elasticsearch_old_index_cleaner_worker.rb +112 -0
  231. data/app/workers/sidekiq/elasticsearch_process_batch.rb +87 -0
  232. data/app/workers/sidekiq/elasticsearch_process_queue.rb +40 -0
  233. data/app/workers/sidekiq/elasticsearch_reindex.rb +53 -0
  234. data/config/initializers/action_view.rb +7 -0
  235. data/config/initializers/inflections.rb +24 -0
  236. data/config/initializers/marginalia.rb +8 -0
  237. data/config/initializers/mime_types.rb +6 -0
  238. data/config/initializers/opentelemetry.rb +12 -0
  239. data/config/initializers/redis_conf.rb +40 -0
  240. data/config/initializers/searchkick.rb +9 -0
  241. data/config/initializers/will_paginate.rb +39 -0
  242. data/config/redis.yml +12 -0
  243. data/config/routes.rb +27 -0
  244. data/config/service_trust_routes.rb +5 -0
  245. data/config.ru +9 -0
  246. data/db/migrate/20230127194725_create_organization_table.rb +25 -0
  247. data/db/migrate/20230127194726_validate_organization_foreign_keys.rb +7 -0
  248. data/db/migrate/20230206141413_create_security_models.rb +42 -0
  249. data/db/migrate/20230206141513_create_restriction_model.rb +20 -0
  250. data/db/migrate/20230206142613_add_security_indexes.rb +14 -0
  251. data/db/migrate/20230206143513_security_model_constants_seed.rb +8 -0
  252. data/db/migrate/20230206161513_add_organization_to_security_models.rb +13 -0
  253. data/db/migrate/20230209121019_create_search_group_models.rb +36 -0
  254. data/db/migrate/20230209122919_add_search_group_indexes.rb +21 -0
  255. data/db/migrate/20230209181832_add_search_group_resources_type_constraint_trigger.rb +61 -0
  256. data/db/migrate/20230313220337_create_table_user_sessions.rb +31 -0
  257. data/db/migrate/20230517141106_update_resource_types.rb +12 -0
  258. data/db/migrate/20230518194611_add_resources_contraints.rb +17 -0
  259. data/db/migrate/20230518194642_create_nested_resource_model.rb +21 -0
  260. data/db/migrate/20230518194645_validate_security_models_constraints.rb +23 -0
  261. data/db/migrate/20230608125037_add_inactive_field_to_user_sessions.rb +7 -0
  262. data/db/migrate/20230919191525_add_parent_id_to_resources.rb +8 -0
  263. data/db/migrate/20230919191535_validate_parent_id_fk_resources.rb +7 -0
  264. data/db/migrate/20230919210456_drop_nested_resources.rb +7 -0
  265. data/db/migrate/20231222135620_add_user_role.rb +19 -0
  266. data/db/migrate/20240108172021_create_favorites_table.rb +20 -0
  267. data/db/migrate/20240108190209_create_organization_link_table_if_doesnt_exists.rb +31 -0
  268. data/db/migrate/20240109211445_validate_fks_org_links.rb +9 -0
  269. data/db/migrate/20240110135440_create_recently_vieweds.rb +20 -0
  270. data/db/migrate/20240112185312_link_type_options_seed.rb +7 -0
  271. data/db/migrate/20240116173201_organization_link_resource_tab_constraints.rb +19 -0
  272. data/db/migrate/20240131160949_add_user_organizations_table.rb +32 -0
  273. data/db/migrate/20240212132519_add_organization_in_user_sessions.rb +13 -0
  274. data/db/migrate/20240227135052_add_user_organization_composite_fks.rb +32 -0
  275. data/db/migrate/20240312193501_update_organization_links.rb +11 -0
  276. data/db/migrate/20240426185902_add_user_organizations_current_organization_column.rb +12 -0
  277. data/db/migrate/20240429214601_add_primary_trigger.rb +10 -0
  278. data/db/migrate/20241112131819_add_json_file_format_seed.rb +7 -0
  279. data/db/migrate/20250520133417_create_restriction_operations.rb +18 -0
  280. data/db/migrate/20250520133420_validate_create_restriction_operations.rb +8 -0
  281. data/db/migrate/20250520140642_add_indexes_to_restrictions.rb +9 -0
  282. data/db/migrate/20250529130741_create_notification_alerts_table.rb +28 -0
  283. data/db/migrate/20250530223332_create_data_migrations_table.rb +19 -0
  284. data/db/migrate/20250605121622_seed_notification_alert_status.rb +7 -0
  285. data/db/migrate/20250605131439_seed_notification_alert_types.rb +7 -0
  286. data/db/migrate/20250606173128_update_notification_alert_length_constraint.rb +18 -0
  287. data/db/migrate/20250606174043_validate_notification_alert_length_constraints.rb +8 -0
  288. data/db/migrate/20250624183000_add_data_import_alert_type.rb +11 -0
  289. data/db/migrate/20250722183935_create_organization_options_table.rb +38 -0
  290. data/db/migrate/20250722192724_validate_organization_options_fks_and_constraints.rb +32 -0
  291. data/db/migrate/20250722192936_add_indexes_to_organization_options_table.rb +15 -0
  292. data/db/migrate/20250915132448_add_column_restrictions_hash.rb +7 -0
  293. data/db/migrate/20250917143211_add_column_permissions_hash.rb +7 -0
  294. data/db/migrate/20251001145950_add_trust_this_device.rb +7 -0
  295. data/db/migrate/20251021130835_add_backend_urls_to_applications_table.rb +13 -0
  296. data/db/migrate/20251021172715_validate_backend_urls_constraints_in_applications.rb +9 -0
  297. data/db/migrate/20251030200020_create_card_definitions.rb +23 -0
  298. data/db/migrate/20251030201012_create_card_definition_resources.rb +24 -0
  299. data/db/migrate/20251030201236_create_card_fields.rb +34 -0
  300. data/db/migrate/20251103194317_create_role_resource_cards.rb +37 -0
  301. data/db/migrate/20251103194426_create_role_card_fields.rb +34 -0
  302. data/db/migrate/20251107173218_validate_columns_from_cards.rb +17 -0
  303. data/db/migrate/20251111173212_add_column_application_array.rb +7 -0
  304. data/db/migrate/20251112203326_add_card_types.rb +10 -0
  305. data/db/migrate/20251115120000_add_virtual_cards_support.rb +28 -0
  306. data/db/migrate/20251202231256_add_org_scoped_fk_role_resource_cards_to_roles.rb +14 -0
  307. data/db/migrate/20260105134504_add_accessor_field_resource_type.rb +7 -0
  308. data/db/migrate/20260115123000_add_default_visibility_to_card_definitions.rb +9 -0
  309. data/db/migrate/20260120175037_is_new_admin.rb +9 -0
  310. data/db/migrate/20260122392008_add_stats_table.rb +17 -0
  311. data/db/migrate/20260414204718_add_index_to_stats_table.rb +10 -0
  312. data/db/migrate/20260509120000_create_service_trust_keys.rb +25 -0
  313. data/db/migrate/20260509120001_create_service_trust_audit_logs.rb +26 -0
  314. data/db/migrate/20260511120000_add_direction_to_service_trust_audit_logs.rb +17 -0
  315. data/db/migrate/20260511121000_remove_role_fk_from_role_resource_cards.rb +19 -0
  316. data/db/migrate/20260511121001_add_disabled_at_to_role_resource_cards.rb +10 -0
  317. data/db/migrate/20260605120100_seed_hover_card_options.rb +8 -0
  318. data/db/migrate/20260608120000_seed_badge_list_card_data_type.rb +7 -0
  319. data/db/migrate/20260727000000_add_two_factor_trust_id_to_user_sessions.rb +11 -0
  320. data/db/seeds.rb +14 -0
  321. data/docs/Elasticsearch.md +23 -0
  322. data/docs/Search_API.md +126 -0
  323. data/docs/cards_role_proxy.md +107 -0
  324. data/docs/factory_api.md +135 -0
  325. data/has-helpers-pr-289-pipeline.yaml +16 -0
  326. data/has-helpers-pr-298-pipeline.yaml +15 -0
  327. data/has_helpers.gemspec +68 -0
  328. data/lib/active_record/migration/active_migration_patch.rb +123 -0
  329. data/lib/generators/has_helpers/install/USAGE +8 -0
  330. data/lib/generators/has_helpers/install/install_generator.rb +22 -0
  331. data/lib/generators/has_helpers/install/templates/migrate/has_helpers_initial_setup.rb +26 -0
  332. data/lib/has_helpers/active_record/base.rb +50 -0
  333. data/lib/has_helpers/active_record/date_range.rb +228 -0
  334. data/lib/has_helpers/active_record/nested_resource.rb +58 -0
  335. data/lib/has_helpers/active_record/validation.rb +63 -0
  336. data/lib/has_helpers/active_record.rb +520 -0
  337. data/lib/has_helpers/app/command.rb +107 -0
  338. data/lib/has_helpers/app.rb +10 -0
  339. data/lib/has_helpers/assets_precompile_helper.rb +20 -0
  340. data/lib/has_helpers/attributes.rb +196 -0
  341. data/lib/has_helpers/aws.rb +67 -0
  342. data/lib/has_helpers/cards/cache.rb +49 -0
  343. data/lib/has_helpers/cards/config.rb +42 -0
  344. data/lib/has_helpers/cards/remote_role.rb +27 -0
  345. data/lib/has_helpers/cards/remote_roles.rb +75 -0
  346. data/lib/has_helpers/cards/roles.rb +55 -0
  347. data/lib/has_helpers/cards.rb +34 -0
  348. data/lib/has_helpers/concerns/hydrate_concern.rb +76 -0
  349. data/lib/has_helpers/concerns/importable_accessors.rb +25 -0
  350. data/lib/has_helpers/concerns/restrictions_parseable.rb +51 -0
  351. data/lib/has_helpers/config.rb +11 -0
  352. data/lib/has_helpers/constant.rb +578 -0
  353. data/lib/has_helpers/constraints.rb +10 -0
  354. data/lib/has_helpers/core_ext/array.rb +28 -0
  355. data/lib/has_helpers/core_ext/date/infinity.rb +25 -0
  356. data/lib/has_helpers/core_ext/date.rb +39 -0
  357. data/lib/has_helpers/core_ext/enumerable.rb +20 -0
  358. data/lib/has_helpers/core_ext/false_class.rb +14 -0
  359. data/lib/has_helpers/core_ext/float.rb +18 -0
  360. data/lib/has_helpers/core_ext/hash.rb +71 -0
  361. data/lib/has_helpers/core_ext/integer.rb +18 -0
  362. data/lib/has_helpers/core_ext/nil_class.rb +30 -0
  363. data/lib/has_helpers/core_ext/object.rb +60 -0
  364. data/lib/has_helpers/core_ext/proc.rb +16 -0
  365. data/lib/has_helpers/core_ext/string.rb +81 -0
  366. data/lib/has_helpers/core_ext/symbol.rb +17 -0
  367. data/lib/has_helpers/core_ext/true_class.rb +14 -0
  368. data/lib/has_helpers/cursor.rb +46 -0
  369. data/lib/has_helpers/data_migration/base.rb +293 -0
  370. data/lib/has_helpers/data_migration/large_worker.rb +20 -0
  371. data/lib/has_helpers/data_migration/master_worker.rb +33 -0
  372. data/lib/has_helpers/data_migration/migrator.rb +83 -0
  373. data/lib/has_helpers/data_migration/model.rb +18 -0
  374. data/lib/has_helpers/data_migration/small_worker.rb +20 -0
  375. data/lib/has_helpers/data_migration.rb +8 -0
  376. data/lib/has_helpers/engine.rb +47 -0
  377. data/lib/has_helpers/extensions.rb +17 -0
  378. data/lib/has_helpers/factory_api.rb +47 -0
  379. data/lib/has_helpers/factory_scenario/definition.rb +33 -0
  380. data/lib/has_helpers/factory_scenario/dsl.rb +80 -0
  381. data/lib/has_helpers/factory_scenario/factory_bot_override.rb +53 -0
  382. data/lib/has_helpers/factory_scenario/registry.rb +31 -0
  383. data/lib/has_helpers/factory_scenario/runner.rb +285 -0
  384. data/lib/has_helpers/factory_scenario.rb +27 -0
  385. data/lib/has_helpers/fixed_expiration_time_cache_session_store.rb +20 -0
  386. data/lib/has_helpers/flattener/association.rb +140 -0
  387. data/lib/has_helpers/flattener/attribute.rb +42 -0
  388. data/lib/has_helpers/flattener/base.rb +428 -0
  389. data/lib/has_helpers/flattener/column/types.rb +71 -0
  390. data/lib/has_helpers/flattener/column.rb +97 -0
  391. data/lib/has_helpers/flattener/pivot.rb +49 -0
  392. data/lib/has_helpers/flattener/reset.rb +33 -0
  393. data/lib/has_helpers/flattener/resource.rb +114 -0
  394. data/lib/has_helpers/flattener/resource_generator.rb +21 -0
  395. data/lib/has_helpers/flattener/utils.rb +14 -0
  396. data/lib/has_helpers/flattener.rb +53 -0
  397. data/lib/has_helpers/hash_accessors.rb +62 -0
  398. data/lib/has_helpers/helpers/application.rb +291 -0
  399. data/lib/has_helpers/index/config.rb +15 -0
  400. data/lib/has_helpers/lazy_config.rb +61 -0
  401. data/lib/has_helpers/resource_importer.rb +260 -0
  402. data/lib/has_helpers/resources.rb +127 -0
  403. data/lib/has_helpers/rspec/constants.rb +61 -0
  404. data/lib/has_helpers/rspec/factories.rb +11 -0
  405. data/lib/has_helpers/rspec.rb +22 -0
  406. data/lib/has_helpers/sentry/graphql_trace.rb +37 -0
  407. data/lib/has_helpers/serializer.rb +41 -0
  408. data/lib/has_helpers/service_trust/audit.rb +45 -0
  409. data/lib/has_helpers/service_trust/authorization.rb +34 -0
  410. data/lib/has_helpers/service_trust/bearer_token.rb +23 -0
  411. data/lib/has_helpers/service_trust/capability.rb +32 -0
  412. data/lib/has_helpers/service_trust/client/response.rb +13 -0
  413. data/lib/has_helpers/service_trust/client/retry.rb +35 -0
  414. data/lib/has_helpers/service_trust/client.rb +128 -0
  415. data/lib/has_helpers/service_trust/configuration.rb +76 -0
  416. data/lib/has_helpers/service_trust/engine.rb +10 -0
  417. data/lib/has_helpers/service_trust/errors.rb +32 -0
  418. data/lib/has_helpers/service_trust/graphql/authorizable.rb +72 -0
  419. data/lib/has_helpers/service_trust/graphql_client.rb +54 -0
  420. data/lib/has_helpers/service_trust/jwks_client.rb +133 -0
  421. data/lib/has_helpers/service_trust/jwt_codec.rb +45 -0
  422. data/lib/has_helpers/service_trust/key_manager.rb +160 -0
  423. data/lib/has_helpers/service_trust/rate_limiter.rb +27 -0
  424. data/lib/has_helpers/service_trust/replay_cache.rb +26 -0
  425. data/lib/has_helpers/service_trust/signer.rb +47 -0
  426. data/lib/has_helpers/service_trust/test.rb +60 -0
  427. data/lib/has_helpers/service_trust/verifier.rb +85 -0
  428. data/lib/has_helpers/service_trust/verify_middleware.rb +55 -0
  429. data/lib/has_helpers/service_trust.rb +51 -0
  430. data/lib/has_helpers/test_helpers/factory_data/unit_addresses.rb +42 -0
  431. data/lib/has_helpers/test_helpers/factory_data/unit_advisors.rb +272 -0
  432. data/lib/has_helpers/test_helpers/factory_data/unit_emails.rb +37 -0
  433. data/lib/has_helpers/test_helpers/factory_data/unit_organizations.rb +48 -0
  434. data/lib/has_helpers/test_helpers/factory_data/unit_people.rb +34 -0
  435. data/lib/has_helpers/test_helpers/factory_data/unit_phones.rb +41 -0
  436. data/lib/has_helpers/test_helpers/factory_data/unit_states.rb +26 -0
  437. data/lib/has_helpers/test_helpers/factory_data/unit_webs.rb +31 -0
  438. data/lib/has_helpers/test_helpers/shoulda_helpers.rb +249 -0
  439. data/lib/has_helpers/ui_attributes.rb +39 -0
  440. data/lib/has_helpers/utils/action.rb +87 -0
  441. data/lib/has_helpers/utils/active_record_utils.rb +40 -0
  442. data/lib/has_helpers/utils/alert.rb +42 -0
  443. data/lib/has_helpers/utils/container.rb +46 -0
  444. data/lib/has_helpers/utils/dataset.rb +34 -0
  445. data/lib/has_helpers/utils/email_notifier.rb +15 -0
  446. data/lib/has_helpers/utils/icon.rb +66 -0
  447. data/lib/has_helpers/utils/input.rb +45 -0
  448. data/lib/has_helpers/utils/log_notifier.rb +79 -0
  449. data/lib/has_helpers/utils/render_object.rb +32 -0
  450. data/lib/has_helpers/utils/s3_utils.rb +37 -0
  451. data/lib/has_helpers/utils/service.rb +87 -0
  452. data/lib/has_helpers/utils/slack_conf.rb +31 -0
  453. data/lib/has_helpers/utils/slack_notifier.rb +42 -0
  454. data/lib/has_helpers/utils/text_node.rb +34 -0
  455. data/lib/has_helpers/utils/user_restrictions.rb +266 -0
  456. data/lib/has_helpers/utils.rb +19 -0
  457. data/lib/has_helpers/version.rb +6 -0
  458. data/lib/has_helpers/web_socket_publisher.rb +20 -0
  459. data/lib/has_helpers/webhook.rb +17 -0
  460. data/lib/has_helpers.rb +63 -0
  461. data/lib/hq/resource.rb +55 -0
  462. data/lib/hq/structure/inserts.rb +176 -0
  463. data/lib/hq/structure.rb +115 -0
  464. data/lib/tasks/has_helpers/service_trust.rake +39 -0
  465. data/script/rails +10 -0
  466. data/sorbet/config +5 -0
  467. data/sorbet/rbi/gems/actionpack.rbi +1361 -0
  468. data/sorbet/rbi/gems/activemodel.rbi +755 -0
  469. data/sorbet/rbi/gems/activerecord.rbi +4713 -0
  470. data/sorbet/rbi/gems/activesupport.rbi +1970 -0
  471. data/sorbet/rbi/gems/barby.rbi +129 -0
  472. data/sorbet/rbi/gems/better_errors.rbi +155 -0
  473. data/sorbet/rbi/gems/binding_of_caller.rbi +22 -0
  474. data/sorbet/rbi/gems/byebug.rbi +19 -0
  475. data/sorbet/rbi/gems/chronic.rbi +480 -0
  476. data/sorbet/rbi/gems/chunky_png.rbi +510 -0
  477. data/sorbet/rbi/gems/climate_control.rbi +45 -0
  478. data/sorbet/rbi/gems/coderay.rbi +91 -0
  479. data/sorbet/rbi/gems/concurrent-ruby.rbi +1587 -0
  480. data/sorbet/rbi/gems/debug_inspector.rbi +18 -0
  481. data/sorbet/rbi/gems/erubi.rbi +26 -0
  482. data/sorbet/rbi/gems/holidays.rbi +362 -0
  483. data/sorbet/rbi/gems/i18n.rbi +132 -0
  484. data/sorbet/rbi/gems/method_source.rbi +63 -0
  485. data/sorbet/rbi/gems/mime-types-data.rbi +16 -0
  486. data/sorbet/rbi/gems/mime-types.rbi +217 -0
  487. data/sorbet/rbi/gems/mimemagic.rbi +36 -0
  488. data/sorbet/rbi/gems/minitest.rbi +282 -0
  489. data/sorbet/rbi/gems/mustache.rbi +151 -0
  490. data/sorbet/rbi/gems/paperclip.rbi +644 -0
  491. data/sorbet/rbi/gems/parallel.rbi +81 -0
  492. data/sorbet/rbi/gems/parallel_tests.rbi +27 -0
  493. data/sorbet/rbi/gems/pry-rails.rbi +71 -0
  494. data/sorbet/rbi/gems/pry.rbi +1964 -0
  495. data/sorbet/rbi/gems/rack.rbi +228 -0
  496. data/sorbet/rbi/gems/railties.rbi +364 -0
  497. data/sorbet/rbi/gems/rspec-core.rbi +1704 -0
  498. data/sorbet/rbi/gems/rspec-expectations.rbi +389 -0
  499. data/sorbet/rbi/gems/rspec-support.rbi +206 -0
  500. data/sorbet/rbi/gems/rspec.rbi +14 -0
  501. data/sorbet/rbi/gems/spreadsheet_architect.rbi +10 -0
  502. data/sorbet/rbi/gems/sprockets.rbi +659 -0
  503. data/sorbet/rbi/gems/terrapin.rbi +115 -0
  504. data/sorbet/rbi/gems/testhq.rbi +14 -0
  505. data/sorbet/rbi/gems/thor.rbi +482 -0
  506. data/sorbet/rbi/gems/thread_safe.rbi +81 -0
  507. data/sorbet/rbi/gems/tzinfo.rbi +407 -0
  508. data/sorbet/rbi/gems/zeitwerk.rbi +131 -0
  509. data/sorbet/rbi/hidden-definitions/errors.txt +35111 -0
  510. data/sorbet/rbi/hidden-definitions/hidden.rbi +59974 -0
  511. data/sorbet/rbi/sorbet-typed/lib/actionmailer/all/actionmailer.rbi +13 -0
  512. data/sorbet/rbi/sorbet-typed/lib/actionpack/all/actionpack.rbi +855 -0
  513. data/sorbet/rbi/sorbet-typed/lib/actionview/all/actionview.rbi +314 -0
  514. data/sorbet/rbi/sorbet-typed/lib/activemodel/all/activemodel.rbi +423 -0
  515. data/sorbet/rbi/sorbet-typed/lib/activerecord/>=5.2/activerecord.rbi +17 -0
  516. data/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1415 -0
  517. data/sorbet/rbi/sorbet-typed/lib/activerecord/~>6.0.0/activerecord.rbi +475 -0
  518. data/sorbet/rbi/sorbet-typed/lib/activestorage/>=6.0.0/activestorage.rbi +19 -0
  519. data/sorbet/rbi/sorbet-typed/lib/activestorage/all/activestorage.rbi +173 -0
  520. data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6.0.0.rc1/activesupport.rbi +23 -0
  521. data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +625 -0
  522. data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +8684 -0
  523. data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +99 -0
  524. data/sorbet/rbi/sorbet-typed/lib/railties/all/railties.rbi +25 -0
  525. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +254 -0
  526. data/sorbet/rbi/sorbet-typed/lib/ruby/all/gem.rbi +4222 -0
  527. data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +111 -0
  528. data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +543 -0
  529. data/sorbet/rbi/todo.rbi +244 -0
  530. data/spec/commands/cards/available_cards_command_spec.rb +90 -0
  531. data/spec/commands/cards/card_data_command_spec.rb +1048 -0
  532. data/spec/constructors/has_helpers/base_constructor_spec.rb +49 -0
  533. data/spec/controllers/has_helpers/controller_spec.rb +71 -0
  534. data/spec/controllers/has_helpers/xlsx_export_spec.rb +96 -0
  535. data/spec/factories/advisors.rb +20 -0
  536. data/spec/factories/anonymous_cowards.rb +5 -0
  537. data/spec/factories/applications.rb +5 -0
  538. data/spec/factories/appointments.rb +6 -0
  539. data/spec/factories/attachment.rb +15 -0
  540. data/spec/factories/audits.rb +5 -0
  541. data/spec/factories/basic_schedules.rb +12 -0
  542. data/spec/factories/cards/card_definition_resources.rb +7 -0
  543. data/spec/factories/cards/card_definitions.rb +11 -0
  544. data/spec/factories/cards/card_fields.rb +22 -0
  545. data/spec/factories/cards/role_card_fields.rb +17 -0
  546. data/spec/factories/cards/role_resource_cards.rb +12 -0
  547. data/spec/factories/constant_definition.rb +7 -0
  548. data/spec/factories/contracts.rb +10 -0
  549. data/spec/factories/data_migration_models.rb +9 -0
  550. data/spec/factories/demographics.rb +22 -0
  551. data/spec/factories/events.rb +21 -0
  552. data/spec/factories/firms.rb +21 -0
  553. data/spec/factories/imports.rb +9 -0
  554. data/spec/factories/meetups.rb +13 -0
  555. data/spec/factories/organizations.rb +13 -0
  556. data/spec/factories/policies.rb +7 -0
  557. data/spec/factories/policy_notes.rb +9 -0
  558. data/spec/factories/read_only_values.rb +7 -0
  559. data/spec/factories/resources.rb +7 -0
  560. data/spec/factories/roles.rb +11 -0
  561. data/spec/factories/schedules.rb +13 -0
  562. data/spec/factories/shared/flattener/attributes.rb +10 -0
  563. data/spec/factories/shared/flattener/columns.rb +6 -0
  564. data/spec/factories/shared/flattener/resources.rb +5 -0
  565. data/spec/factories/templates.rb +12 -0
  566. data/spec/factories/user_organizations.rb +23 -0
  567. data/spec/factories/user_session.rb +11 -0
  568. data/spec/factories/users.rb +55 -0
  569. data/spec/fixtures/sample_config.yml +3 -0
  570. data/spec/graphql/has_helpers/mutations/external_data_sync_spec.rb +101 -0
  571. data/spec/graphql/has_helpers/resolvers/cards/available_cards_spec.rb +36 -0
  572. data/spec/graphql/has_helpers/resolvers/cards/card_data_spec.rb +42 -0
  573. data/spec/graphql/has_helpers/resolvers/cards/hover_card_data_spec.rb +59 -0
  574. data/spec/has_helpers/cards/cache_spec.rb +58 -0
  575. data/spec/has_helpers/cards/config_spec.rb +68 -0
  576. data/spec/has_helpers/cards/orphan_card_sweeper_spec.rb +97 -0
  577. data/spec/has_helpers/cards/remote_role_spec.rb +17 -0
  578. data/spec/has_helpers/cards/remote_roles_spec.rb +97 -0
  579. data/spec/has_helpers/cards/roles_spec.rb +52 -0
  580. data/spec/has_helpers/cards/smoke_spec.rb +168 -0
  581. data/spec/has_helpers/cards/support.rb +52 -0
  582. data/spec/has_helpers/resolvers/internal/roles_spec.rb +73 -0
  583. data/spec/has_helpers/service_trust/audit_log_cleanup_worker_spec.rb +33 -0
  584. data/spec/has_helpers/service_trust/audit_spec.rb +70 -0
  585. data/spec/has_helpers/service_trust/authenticated_as_service_spec.rb +65 -0
  586. data/spec/has_helpers/service_trust/authorization_spec.rb +37 -0
  587. data/spec/has_helpers/service_trust/capability_spec.rb +35 -0
  588. data/spec/has_helpers/service_trust/client_spec.rb +129 -0
  589. data/spec/has_helpers/service_trust/configuration_spec.rb +35 -0
  590. data/spec/has_helpers/service_trust/engine_spec.rb +13 -0
  591. data/spec/has_helpers/service_trust/errors_spec.rb +23 -0
  592. data/spec/has_helpers/service_trust/graphql_authentication_spec.rb +59 -0
  593. data/spec/has_helpers/service_trust/graphql_authorizable_spec.rb +91 -0
  594. data/spec/has_helpers/service_trust/graphql_client_spec.rb +47 -0
  595. data/spec/has_helpers/service_trust/jwks_client_spec.rb +85 -0
  596. data/spec/has_helpers/service_trust/jwks_controller_spec.rb +47 -0
  597. data/spec/has_helpers/service_trust/jwt_codec_spec.rb +30 -0
  598. data/spec/has_helpers/service_trust/key_manager_spec.rb +80 -0
  599. data/spec/has_helpers/service_trust/key_spec.rb +52 -0
  600. data/spec/has_helpers/service_trust/rake_tasks_spec.rb +58 -0
  601. data/spec/has_helpers/service_trust/rate_limiter_spec.rb +31 -0
  602. data/spec/has_helpers/service_trust/replay_cache_spec.rb +29 -0
  603. data/spec/has_helpers/service_trust/signer_verifier_spec.rb +159 -0
  604. data/spec/has_helpers/service_trust/smoke_spec.rb +216 -0
  605. data/spec/has_helpers/service_trust/support.rb +121 -0
  606. data/spec/has_helpers/service_trust/test_module_spec.rb +39 -0
  607. data/spec/has_helpers/service_trust/verify_middleware_spec.rb +82 -0
  608. data/spec/helpers/application_spec.rb +155 -0
  609. data/spec/importers/cards/importer_spec.rb +861 -0
  610. data/spec/internal/app/assets/config/manifest.js +0 -0
  611. data/spec/internal/app/constants/cat_egory.rb +33 -0
  612. data/spec/internal/app/constants/color.rb +19 -0
  613. data/spec/internal/app/constants/exclude_group_nil_constant.rb +5 -0
  614. data/spec/internal/app/constants/foo.rb +6 -0
  615. data/spec/internal/app/constants/fruit.rb +3 -0
  616. data/spec/internal/app/constants/generic_group_example.rb +12 -0
  617. data/spec/internal/app/constants/my_constant.rb +4 -0
  618. data/spec/internal/app/constants/product.rb +3 -0
  619. data/spec/internal/app/constants/shirt_size.rb +18 -0
  620. data/spec/internal/app/constants/simple_option.rb +14 -0
  621. data/spec/internal/app/constants/status.rb +5 -0
  622. data/spec/internal/app/constructors/organization_constructor.rb +4 -0
  623. data/spec/internal/app/controllers/application_controller.rb +27 -0
  624. data/spec/internal/app/controllers/search_extensions.rb +3 -0
  625. data/spec/internal/app/indices/advisor_index.rb +35 -0
  626. data/spec/internal/app/indices/contract_index.rb +52 -0
  627. data/spec/internal/app/indices/firm_index.rb +16 -0
  628. data/spec/internal/app/indices/policy_index.rb +20 -0
  629. data/spec/internal/app/models/advisor.rb +24 -0
  630. data/spec/internal/app/models/agency.rb +3 -0
  631. data/spec/internal/app/models/anonymous_coward.rb +5 -0
  632. data/spec/internal/app/models/another_event.rb +2 -0
  633. data/spec/internal/app/models/appointment.rb +3 -0
  634. data/spec/internal/app/models/attachment.rb +10 -0
  635. data/spec/internal/app/models/basic_schedule.rb +6 -0
  636. data/spec/internal/app/models/beneficiary.rb +6 -0
  637. data/spec/internal/app/models/client.rb +5 -0
  638. data/spec/internal/app/models/contract.rb +9 -0
  639. data/spec/internal/app/models/demographic.rb +4 -0
  640. data/spec/internal/app/models/division.rb +3 -0
  641. data/spec/internal/app/models/event.rb +10 -0
  642. data/spec/internal/app/models/event_template.rb +1 -0
  643. data/spec/internal/app/models/firm.rb +18 -0
  644. data/spec/internal/app/models/has_audits/audit.rb +15 -0
  645. data/spec/internal/app/models/import.rb +7 -0
  646. data/spec/internal/app/models/meetup.rb +8 -0
  647. data/spec/internal/app/models/organization.rb +17 -0
  648. data/spec/internal/app/models/organization_event.rb +5 -0
  649. data/spec/internal/app/models/policy.rb +16 -0
  650. data/spec/internal/app/models/policy_holder.rb +7 -0
  651. data/spec/internal/app/models/policy_note.rb +8 -0
  652. data/spec/internal/app/models/project.rb +3 -0
  653. data/spec/internal/app/models/read_only_value.rb +8 -0
  654. data/spec/internal/app/models/schedule.rb +12 -0
  655. data/spec/internal/app/models/template.rb +9 -0
  656. data/spec/internal/app/models/user.rb +31 -0
  657. data/spec/internal/app/models/user_organization.rb +17 -0
  658. data/spec/internal/app/views/has_lists/lists/_table.html.erb +1 -0
  659. data/spec/internal/config/database-standard.yml +24 -0
  660. data/spec/internal/config/initializers/resources.rb +19 -0
  661. data/spec/internal/config/routes.rb +19 -0
  662. data/spec/internal/config/storage.yml +3 -0
  663. data/spec/internal/db/good_job_schema.rb +141 -0
  664. data/spec/internal/db/schema.rb +334 -0
  665. data/spec/internal/log/.gitignore +1 -0
  666. data/spec/internal/public/favicon.ico +0 -0
  667. data/spec/lib/flattener/base_spec.rb +93 -0
  668. data/spec/lib/flattener/column/types_spec.rb +65 -0
  669. data/spec/lib/has_helpers/active_record/base_spec.rb +53 -0
  670. data/spec/lib/has_helpers/active_record/date_range_spec.rb +498 -0
  671. data/spec/lib/has_helpers/active_record/lookup_model_spec.rb +408 -0
  672. data/spec/lib/has_helpers/active_record/nested_resource_spec.rb +43 -0
  673. data/spec/lib/has_helpers/app/command/reports/generator_specs.rb +887 -0
  674. data/spec/lib/has_helpers/app/command_spec.rb +117 -0
  675. data/spec/lib/has_helpers/app/support/mocks/graphql_reports_helpers.rb +96 -0
  676. data/spec/lib/has_helpers/app/support/mocks/reports_api_mock.rb +47 -0
  677. data/spec/lib/has_helpers/attributes_spec.rb +220 -0
  678. data/spec/lib/has_helpers/aws_spec.rb +126 -0
  679. data/spec/lib/has_helpers/concerns/hydrate_concern_spec.rb +473 -0
  680. data/spec/lib/has_helpers/constant/definition_spec.rb +128 -0
  681. data/spec/lib/has_helpers/constant_spec.rb +432 -0
  682. data/spec/lib/has_helpers/core_ext/array_spec.rb +22 -0
  683. data/spec/lib/has_helpers/core_ext/date/infinity_spec.rb +67 -0
  684. data/spec/lib/has_helpers/core_ext/date_spec.rb +27 -0
  685. data/spec/lib/has_helpers/core_ext/false_class_spec.rb +7 -0
  686. data/spec/lib/has_helpers/core_ext/float_spec.rb +9 -0
  687. data/spec/lib/has_helpers/core_ext/hash_spec.rb +48 -0
  688. data/spec/lib/has_helpers/core_ext/integer_spec.rb +9 -0
  689. data/spec/lib/has_helpers/core_ext/nil_class_spec.rb +17 -0
  690. data/spec/lib/has_helpers/core_ext/object_spec.rb +54 -0
  691. data/spec/lib/has_helpers/core_ext/proc_spec.rb +24 -0
  692. data/spec/lib/has_helpers/core_ext/string_spec.rb +154 -0
  693. data/spec/lib/has_helpers/core_ext/true_class_spec.rb +7 -0
  694. data/spec/lib/has_helpers/data_migration/base_spec.rb +470 -0
  695. data/spec/lib/has_helpers/data_migration/large_worker_spec.rb +34 -0
  696. data/spec/lib/has_helpers/data_migration/master_worker_spec.rb +51 -0
  697. data/spec/lib/has_helpers/data_migration/migrator_spec.rb +113 -0
  698. data/spec/lib/has_helpers/data_migration/model_spec.rb +52 -0
  699. data/spec/lib/has_helpers/data_migration/small_worker_spec.rb +34 -0
  700. data/spec/lib/has_helpers/good_job/notification_alert_cleanup_worker_spec.rb +89 -0
  701. data/spec/lib/has_helpers/hash_accessors_spec.rb +66 -0
  702. data/spec/lib/has_helpers/index_spec.rb +442 -0
  703. data/spec/lib/has_helpers/lazy_config_spec.rb +31 -0
  704. data/spec/lib/has_helpers/organization_merge_spec.rb +88 -0
  705. data/spec/lib/has_helpers/resource_importer_spec.rb +119 -0
  706. data/spec/lib/has_helpers/rspec_spec.rb +16 -0
  707. data/spec/lib/has_helpers/search/provider_spec.rb +26 -0
  708. data/spec/lib/has_helpers/search/search_utils_spec.rb +134 -0
  709. data/spec/lib/has_helpers/sentry/graphql_trace_spec.rb +140 -0
  710. data/spec/lib/has_helpers/ui_attributes_spec.rb +42 -0
  711. data/spec/lib/has_helpers/utils/action_spec.rb +49 -0
  712. data/spec/lib/has_helpers/utils/alert_spec.rb +33 -0
  713. data/spec/lib/has_helpers/utils/dataset_spec.rb +10 -0
  714. data/spec/lib/has_helpers/utils/icon_spec.rb +23 -0
  715. data/spec/lib/has_helpers/utils/input_spec.rb +40 -0
  716. data/spec/lib/has_helpers/utils/log_notifier_spec.rb +25 -0
  717. data/spec/lib/has_helpers/utils/render_object_spec.rb +17 -0
  718. data/spec/lib/has_helpers/utils/s3_utils_spec.rb +78 -0
  719. data/spec/lib/has_helpers/utils/service_spec.rb +79 -0
  720. data/spec/lib/has_helpers/utils/slack_notifier_spec.rb +70 -0
  721. data/spec/lib/has_helpers/utils/user_restrictions_spec.rb +343 -0
  722. data/spec/lib/hq/structure/inserts_spec.rb +152 -0
  723. data/spec/lib/hq/structure_spec.rb +12 -0
  724. data/spec/models/cards/base_spec.rb +149 -0
  725. data/spec/models/cards/card_definition_resource_spec.rb +103 -0
  726. data/spec/models/cards/card_definition_spec.rb +50 -0
  727. data/spec/models/cards/card_field_spec.rb +46 -0
  728. data/spec/models/cards/field_builder_spec.rb +318 -0
  729. data/spec/models/cards/registry_spec.rb +147 -0
  730. data/spec/models/cards/role_card_field_spec.rb +111 -0
  731. data/spec/models/cards/role_resource_card_spec.rb +112 -0
  732. data/spec/models/has_helpers/master_record_spec.rb +20 -0
  733. data/spec/models/has_helpers/user_organization_spec.rb +28 -0
  734. data/spec/models/has_helpers/user_session_spec.rb +16 -0
  735. data/spec/presenters/base_presenter_spec.rb +166 -0
  736. data/spec/rails_helper.rb +119 -0
  737. data/spec/requests/has_helpers/factory_controller_spec.rb +159 -0
  738. data/spec/requests/has_helpers/health_check_controller_spec.rb +37 -0
  739. data/spec/scenarios/empty_setup.rb +6 -0
  740. data/spec/spec_helper.rb +22 -0
  741. data/spec/support/has_helpers/database_helpers.rb +12 -0
  742. data/spec/utils/sync_utils_spec.rb +233 -0
  743. data/spec/workers/goodjob/elasticsearch_process_batch_spec.rb +94 -0
  744. data/spec/workers/goodjob/elasticsearch_process_change_logs_master_worker_spec.rb +40 -0
  745. data/spec/workers/goodjob/elasticsearch_process_change_logs_worker_spec.rb +82 -0
  746. data/spec/workers/goodjob/elasticsearch_process_queue_spec.rb +89 -0
  747. data/spec/workers/goodjob/elasticsearch_reindex_spec.rb +49 -0
  748. metadata +1593 -0
@@ -0,0 +1,1062 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasHelpers
4
+ module Cards
5
+ # Command object that resolves the effective card configuration
6
+ # (card + fields) for a given role/resource/identifier/record combo and
7
+ # returns serialized data for GraphQL or other consumers.
8
+ #
9
+ # Usage:
10
+ # result = HasHelpers::Cards::CardDataCommand.call(
11
+ # role_id: role_id,
12
+ # resource_id: resource_id,
13
+ # identifier_id: identifier_id,
14
+ # record_id: record_id
15
+ # )
16
+ #
17
+ # Usage (optimized API, receives a preloaded record to avoid N+1):
18
+ # result = HasHelpers::Cards::CardDataCommand.call_with_record(
19
+ # role_id: role_id,
20
+ # resource_id: resource, # HasHelpers::Resource instance or id
21
+ # identifier_id: identifier, # HasHelpers::Cards::CardIdentifier instance or id
22
+ # record: record # ActiveRecord instance
23
+ # )
24
+ #
25
+ # result.card # => Hash
26
+ # result.fields # => Array<Hash>
27
+ # result.entries # => Array<Hash>
28
+ class CardDataCommand
29
+ Response = Struct.new(:card, :fields, :entries, keyword_init: true) # rubocop:disable Lint/StructNewOverride
30
+ MAPPER_NOT_APPLIED = Object.new
31
+
32
+ # Public entry point: original API, keeps backward compatibility.
33
+ # It receives record_id and will load the record internally.
34
+ def self.call(role_id:, resource_id:, identifier_id:, record_id:)
35
+ new(
36
+ role_id: role_id,
37
+ resource_id: resource_id,
38
+ identifier_id: identifier_id,
39
+ record_id: record_id
40
+ ).call
41
+ end
42
+
43
+ # Public entry point: optimized variant that receives a preloaded record.
44
+ # This avoids doing resource.klass.find(record_id) for each card.
45
+ def self.call_with_record(role_id:, resource_id:, identifier_id:, record:)
46
+ new(
47
+ role_id: role_id,
48
+ resource_id: resource_id,
49
+ identifier_id: identifier_id,
50
+ record: record
51
+ ).call
52
+ end
53
+
54
+ # Public entry point: hover-only cards live entirely in-memory.
55
+ # Caller supplies a pre-built (unpersisted) CardDefinition + preloaded record;
56
+ # no DB lookup for the definition is performed, and role customization is skipped.
57
+ def self.call_with_definition(resource_id:, card_definition:, record:)
58
+ new(
59
+ role_id: nil,
60
+ resource_id: resource_id,
61
+ identifier_id: card_definition.identifier,
62
+ record: record,
63
+ card_definition: card_definition
64
+ ).call
65
+ end
66
+
67
+ # Constructor supports both styles:
68
+ # - record_id only (classic)
69
+ # - record only (preloaded)
70
+ # - or both, but record takes precedence when present.
71
+ def initialize(role_id:, resource_id:, identifier_id:, record_id: nil, record: nil, card_definition: nil)
72
+ @role = ::HasHelpers::Role.find_by(id: role_id)
73
+ @resource = find_resource(resource_id)
74
+ # Hover-only definitions are passed in-memory; we don't look up the CardIdentifier
75
+ # option in the DB for those (the identifier may just be a plain string).
76
+ @identifier = card_definition ? identifier_id : find_identifier(identifier_id)
77
+ @record_id = record_id
78
+ @record = record
79
+ @card_definition = card_definition
80
+ end
81
+
82
+ def call
83
+ card_definition = @card_definition || find_card_definition!
84
+ registry_card = registry_card_for(card_definition)
85
+ role_resource_card = @card_definition ? nil : find_role_resource_card(card_definition)
86
+
87
+ card_visible = resolve_card_visibility(card_definition, role_resource_card)
88
+ card_position = resolve_card_position(card_definition, role_resource_card)
89
+
90
+ # Build field configs with all metadata in memory.
91
+ field_configs = build_field_configs(card_definition, role_resource_card, registry_card)
92
+
93
+ # If the card is hidden for this role, we only return the card metadata.
94
+ return Response.new(
95
+ card: serialize_card(card_definition, card_visible, card_position),
96
+ fields: [],
97
+ entries: []
98
+ ) unless card_visible
99
+
100
+ # In hover-only mode the parent record IS the target — no relationship navigation.
101
+ relationship = @card_definition ? nil : relationship_for(card_definition, registry_card)
102
+
103
+ targets = target_records(
104
+ load_record,
105
+ relationship,
106
+ scope_for(card_definition, registry_card),
107
+ expansion_scope_for(card_definition, registry_card),
108
+ registry_card
109
+ )
110
+
111
+ Response.new(
112
+ card: serialize_card(card_definition, card_visible, card_position),
113
+ fields: serialize_fields(field_configs),
114
+ entries: build_entries(targets, field_configs, registry_card)
115
+ )
116
+ end
117
+
118
+ private
119
+
120
+ attr_reader :role, :resource, :identifier, :record_id
121
+
122
+ # Basics lookups
123
+ def find_resource(resource_or_id)
124
+ return resource_or_id if resource_or_id.is_a?(::HasHelpers::Resource)
125
+
126
+ ::HasHelpers::Resource.find(resource_or_id)
127
+ end
128
+
129
+ def find_identifier(identifier_or_id)
130
+ return identifier_or_id if identifier_or_id.is_a?(::HasHelpers::Cards::CardIdentifier)
131
+
132
+ ::HasHelpers::Cards::CardIdentifier.find(identifier_or_id)
133
+ end
134
+
135
+ def find_card_definition!
136
+ ::HasHelpers::Cards::CardDefinition.
137
+ joins(:card_definition_resources).
138
+ where(identifier_id: identifier.id, card_definition_resources: { resource_id: resource.id }).
139
+ first! # unique per identifier/resource
140
+ end
141
+
142
+ def find_role_resource_card(card_definition)
143
+ ::HasHelpers::Cards::RoleResourceCard.find_by(
144
+ role_id: role&.id,
145
+ resource_id: resource.id,
146
+ card_definition_id: card_definition.id,
147
+ organization_id: role&.organization_id
148
+ )
149
+ end
150
+
151
+ # Visibility resolution
152
+ def resolve_card_visibility(card_definition, role_resource_card)
153
+ return !!card_definition.default_visibility unless role_resource_card
154
+
155
+ # bi_state_boolean columns default to true, but nil means "inherit"
156
+ role_resource_card.is_visible.nil? ? true : role_resource_card.is_visible
157
+ end
158
+
159
+ def resolve_card_position(card_definition, role_resource_card)
160
+ role_resource_card&.position || card_definition.position
161
+ end
162
+
163
+ # Configuration of fields (meta)
164
+ def build_field_configs(card_definition, role_resource_card, _registry_card)
165
+ overrides = role_resource_card ? role_resource_card.role_card_fields.index_by(&:card_field_id) : {}
166
+ registry_map = registry_field_map_for(card_definition)
167
+
168
+ fields_scope =
169
+ if card_definition.new_record?
170
+ card_definition.card_fields
171
+ else
172
+ card_definition.card_fields.includes(:field_resource, :data_type)
173
+ end
174
+
175
+ fields_scope.
176
+ map do |field|
177
+ override = overrides[field.id]
178
+ visible = resolve_field_visibility(field, override)
179
+ position = override&.position || field.default_position
180
+ registry_meta = registry_map[field_identity(field)]
181
+
182
+ next unless visible
183
+
184
+ source_meta = registry_meta && registry_meta[:source]
185
+
186
+ {
187
+ card_field: field,
188
+ key: effective_field_key(field),
189
+ label: effective_field_label(field, registry_meta),
190
+ data_type: field.data_type.name.underscore.to_sym,
191
+ required: !!field.is_required,
192
+ position: position || 0,
193
+ mapper: registry_meta && registry_meta[:mapper],
194
+ kind: registry_meta && registry_meta[:kind] ? registry_meta[:kind].to_sym : inferred_field_kind(field),
195
+ source: source_meta,
196
+ hover_card: registry_meta && registry_meta[:hover_card],
197
+ hover_card_source: registry_meta && registry_meta[:hover_card_source],
198
+ hover_record_source: registry_meta && registry_meta[:hover_record_source],
199
+ hover_resource: registry_meta && registry_meta[:hover_resource],
200
+ hover_resource_source: registry_meta && registry_meta[:hover_resource_source],
201
+ icon: registry_meta && registry_meta[:icon],
202
+ badge_color: resolve_badge_color_source(field, registry_meta),
203
+ url_resolver: resolve_url_resolver_source(field, registry_meta),
204
+ custom_label: registry_meta && !!registry_meta[:custom_label],
205
+ custom_label_key: registry_meta && registry_meta[:custom_label_key].presence
206
+ }
207
+ end.
208
+ compact.
209
+ sort_by { |cfg| [cfg[:position], cfg[:key].to_s] }
210
+ end
211
+
212
+ def resolve_field_visibility(field, override)
213
+ return override.is_visible unless override&.is_visible.nil?
214
+
215
+ # default_visibility is bi_state_boolean, nil means visible by default
216
+ field.default_visibility != false
217
+ end
218
+
219
+ # Identity for mapping with the registry and with overrides
220
+ # [:custom, "DEMOGRAPHICS_FULL_NAME"]
221
+ # [:resource, "age"]
222
+ def field_identity(field)
223
+ if field.respond_to?(:field_resource) && field.field_resource.present?
224
+ [:resource, field.field_resource&.name.to_s]
225
+ elsif field.respond_to?(:virtual_ref) && field.virtual_ref.present?
226
+ [:virtual, field.virtual_ref.to_s]
227
+ else
228
+ [:custom, normalized_custom_key(field)]
229
+ end
230
+ end
231
+
232
+ def effective_field_key(field)
233
+ has_resource =
234
+ if field.respond_to?(:field_resource_id)
235
+ field.field_resource_id.present?
236
+ elsif field.respond_to?(:field_resource)
237
+ field.field_resource.present?
238
+ else
239
+ false
240
+ end
241
+
242
+ if has_resource
243
+ field.field_resource&.name&.to_sym
244
+ elsif field.respond_to?(:virtual_ref) && field.virtual_ref.present?
245
+ field.virtual_ref.to_sym
246
+ else
247
+ normalized_custom_key(field)&.to_sym
248
+ end
249
+ end
250
+
251
+ def effective_field_label(field, registry_meta = nil)
252
+ return field.field_custom_name if field.field_custom_name.present?
253
+ return registry_meta[:name] if registry_meta && registry_meta[:name].present?
254
+
255
+ resource = field.field_resource
256
+ label = resource&.alias_name.presence || resource&.name
257
+ label ? label.to_s.humanize.titleize : nil
258
+ end
259
+
260
+ def inferred_field_kind(field)
261
+ has_resource =
262
+ if field.respond_to?(:field_resource_id)
263
+ field.field_resource_id.present?
264
+ elsif field.respond_to?(:field_resource)
265
+ field.field_resource.present?
266
+ else
267
+ false
268
+ end
269
+
270
+ return :resource if has_resource
271
+ return :virtual if field.respond_to?(:virtual_ref) && field.virtual_ref.present?
272
+
273
+ :custom
274
+ end
275
+
276
+ def normalized_custom_key(field)
277
+ raw = field.respond_to?(:field_custom_name) ? field.field_custom_name : nil
278
+ return nil if raw.blank?
279
+
280
+ raw.to_s.strip.gsub(/\s+/, "_").underscore
281
+ end
282
+
283
+ # Accepts a CardIdentifier option (DB-backed, has an id) or an unsaved/in-memory
284
+ # one used by hover-only cards (no id — fall back to name).
285
+ def serialize_identifier(identifier_value)
286
+ return nil if identifier_value.nil?
287
+ if identifier_value.respond_to?(:id) && identifier_value.id
288
+ identifier_value.id
289
+ elsif identifier_value.respond_to?(:name) && identifier_value.name
290
+ identifier_value.name.to_s
291
+ else
292
+ identifier_value.to_s
293
+ end
294
+ end
295
+
296
+ # Serialize card metadata
297
+ def serialize_card(card_definition, visible, position)
298
+ {
299
+ id: card_definition.id,
300
+ name: card_definition.name,
301
+ identifier: serialize_identifier(card_definition.identifier),
302
+ layout: card_definition.layout_type.name.downcase.to_sym,
303
+ variant: card_definition.variant&.name&.downcase&.to_sym,
304
+ description: card_definition.description,
305
+ position: position,
306
+ visible: visible
307
+ }
308
+ end
309
+
310
+ def serialize_fields(field_configs)
311
+ field_configs.map do |cfg|
312
+ {
313
+ id: cfg[:card_field].id,
314
+ key: cfg[:key],
315
+ label: cfg[:label],
316
+ data_type: cfg[:data_type],
317
+ required: cfg[:required],
318
+ position: cfg[:position],
319
+ custom_label_key: cfg[:custom_label_key]
320
+ }
321
+ end
322
+ end
323
+
324
+ def build_entries(targets, field_configs, registry_card)
325
+ targets.flat_map do |target|
326
+ data_sources = data_source_payloads(target, registry_card)
327
+
328
+ # If any data source returns an Array, treat each element as a separate entry.
329
+ _array_ds_key, array_ds_values = data_sources.find { |_k, v| v.is_a?(Array) }
330
+
331
+ if array_ds_values
332
+ array_ds_values.map do |element|
333
+ entry_sources = data_sources.transform_values do |v|
334
+ v.is_a?(Array) ? element : v
335
+ end
336
+
337
+ {
338
+ record_id: safe_id(target),
339
+ fields: render_fields(target, field_configs, entry_sources, registry_card)
340
+ }
341
+ end
342
+ else
343
+ {
344
+ record_id: safe_id(target),
345
+ fields: render_fields(target, field_configs, data_sources, registry_card)
346
+ }
347
+ end
348
+ end
349
+ end
350
+
351
+ def data_source_payloads(target, registry_card)
352
+ return {} unless registry_card && registry_card[:data_sources].present?
353
+
354
+ @data_source_cache ||= {}
355
+ cache_key = [registry_card[:identifier], target.object_id].join(":")
356
+ return @data_source_cache[cache_key] if @data_source_cache.key?(cache_key)
357
+
358
+ payloads = {}
359
+ registry_card[:data_sources].each do |ds|
360
+ resolver = ds[:resolver]
361
+ next unless resolver.respond_to?(:call)
362
+
363
+ payloads[ds[:name].to_sym] =
364
+ if resolver.arity == 2
365
+ resolver.call(target, data_source_context)
366
+ else
367
+ resolver.call(target)
368
+ end
369
+ end
370
+
371
+ @data_source_cache[cache_key] = payloads
372
+ end
373
+
374
+ def render_fields(target, field_configs, data_sources, registry_card)
375
+ field_configs.map do |cfg|
376
+ value, url, badge_color_override = resolve_field_value_and_url(target, cfg, data_sources, registry_card)
377
+
378
+ custom_label =
379
+ if cfg[:kind].to_sym == :virtual && cfg[:custom_label] && cfg[:custom_label_key].present?
380
+ dig_any(data_sources, cfg[:custom_label_key])
381
+ end
382
+
383
+ field_hash = {
384
+ id: cfg[:card_field].id,
385
+ label: cfg[:label],
386
+ value: value
387
+ }
388
+
389
+ field_hash[:icon] = cfg[:icon] if cfg[:icon].present?
390
+
391
+ # Emit url for link fields and for hover_card fields (so the trigger can also
392
+ # act as a clickable link to the target record).
393
+ field_hash[:url] = url if link_field?(cfg[:card_field]) || hover_card_field?(cfg)
394
+ if badge_field?(cfg[:card_field])
395
+ field_hash[:badge_color] = badge_color_override || resolve_badge_color(cfg, value)
396
+ end
397
+
398
+ if hover_card_field?(cfg) || cfg[:hover_card].present? || cfg[:hover_card_source].present?
399
+ hover_meta = resolve_hover_metadata(target, cfg, data_sources, value)
400
+ field_hash[:hover_card_identifier] = hover_meta[:identifier]
401
+ field_hash[:hover_target_resource] = hover_meta[:resource_name]
402
+ field_hash[:hover_target_record_id] = hover_meta[:record_id]
403
+ elsif link_field?(cfg[:card_field]) && hover_config_blank?(cfg)
404
+ # Link fields with no explicit hover config: infer a hover card by
405
+ # convention when an <Resource>InfoCard hover-only card exists in
406
+ # the registry. Falls back to a plain link when nothing matches.
407
+ inferred = infer_link_hover_metadata(cfg, value, raw_value_for_link(target, cfg, data_sources))
408
+ if inferred
409
+ field_hash[:hover_card_identifier] = inferred[:identifier]
410
+ field_hash[:hover_target_resource] = inferred[:resource_name]
411
+ field_hash[:hover_target_record_id] = inferred[:record_id]
412
+ end
413
+ end
414
+
415
+ if badge_list_field?(cfg)
416
+ field_hash[:value] = nil
417
+ field_hash[:badges] = resolve_badge_list(value)
418
+ end
419
+
420
+ field_hash[:custom_label] = custom_label.to_s if custom_label.present?
421
+
422
+ field_hash
423
+ end
424
+ end
425
+
426
+ def resolve_field_value_and_url(target, cfg, data_sources, registry_card)
427
+ return [nil, nil, nil] unless target
428
+
429
+ field = cfg[:card_field]
430
+ attr_name = field_attribute_name(field)
431
+ raw_value = resolve_raw_field_value(target, cfg, attr_name, data_sources, registry_card)
432
+
433
+ hash_payload =
434
+ case raw_value
435
+ when Hash
436
+ raw_value.symbolize_keys
437
+ when ActionController::Parameters
438
+ # Parameters may be unpermitted; we explicitly opt into a safe hash for serialization.
439
+ raw_value.to_unsafe_h.symbolize_keys
440
+ end
441
+
442
+ value_from_hash = hash_payload&.key?(:value) ? hash_payload[:value] : raw_value
443
+ value = jsonify_value(option_name_or_value(value_from_hash))
444
+
445
+ url_override = hash_payload&.[](:url) || hash_payload&.[](:href)
446
+ badge_override = hash_payload&.[](:badge_color) || hash_payload&.[](:badgeColor)
447
+
448
+ url = url_override || resolve_url(field, cfg, raw_value, value, attr_name)
449
+
450
+ [value, url, badge_override]
451
+ end
452
+
453
+ def field_attribute_name(field)
454
+ if field.field_resource
455
+ field.field_resource.alias_name.presence || field.field_resource.name
456
+ elsif field.respond_to?(:virtual_ref) && field.virtual_ref.present?
457
+ field.virtual_ref
458
+ elsif field.field_custom_name.present?
459
+ field.field_custom_name.to_s.strip.gsub(/\s+/, "_").underscore
460
+ end
461
+ end
462
+
463
+ def resolve_raw_field_value(target, cfg, attr_name, data_sources, _registry_card)
464
+ return nil unless target
465
+
466
+ source_value = resolve_source_value(target, cfg, data_sources)
467
+ return source_value unless source_value.equal?(MAPPER_NOT_APPLIED)
468
+
469
+ mapped_value = resolve_mapper_value(target, cfg[:mapper])
470
+ return mapped_value unless mapped_value.equal?(MAPPER_NOT_APPLIED)
471
+
472
+ # 2) Fallback: direct reading from the model
473
+ field = cfg[:card_field]
474
+ # byebug if field.data_type_id == HasHelpers::Cards::CardDataType::LINK.id
475
+ if field.field_resource && attr_name.present?
476
+ attr_str = attr_name.to_s
477
+
478
+ # For *_id, try to use the association first
479
+ if attr_str.end_with?("_id")
480
+ assoc_method = attr_str.delete_suffix("_id")
481
+ if target.respond_to?(assoc_method)
482
+ associated = target.public_send(assoc_method)
483
+ return associated if associated
484
+ end
485
+
486
+ return target.public_send(attr_str) if target.respond_to?(attr_str)
487
+ else
488
+ if target.respond_to?(attr_str)
489
+ return target.public_send(attr_str)
490
+ end
491
+ end
492
+ elsif field.field_custom_name.present? && attr_name.present?
493
+ return target.public_send(attr_name) if target.respond_to?(attr_name)
494
+
495
+ # Fallbacks: use cfg key variants in case attr_name normalization differs
496
+ if cfg[:key]
497
+ key_str = cfg[:key].to_s
498
+ return target.public_send(key_str) if target.respond_to?(key_str)
499
+ key_underscored = key_str.underscore
500
+ return target.public_send(key_underscored) if target.respond_to?(key_underscored)
501
+ end
502
+ end
503
+
504
+ nil
505
+ end
506
+
507
+ def resolve_mapper_value(target, mapper)
508
+ return MAPPER_NOT_APPLIED if mapper.blank?
509
+
510
+ mapper_class =
511
+ mapper.to_s.safe_constantize ||
512
+ ::HasHelpers::ApplicationHelper.translate_klass(mapper.to_s)
513
+ if mapper_class.respond_to?(:call)
514
+ mapper_class.call(target)
515
+ elsif mapper_class && mapper_class.instance_methods(false).include?(:call)
516
+ mapper_instance =
517
+ begin
518
+ mapper_class.new(target)
519
+ rescue ArgumentError
520
+ mapper_class.new
521
+ end
522
+
523
+ call_method = mapper_instance.method(:call)
524
+ call_method.arity.zero? ? mapper_instance.call : mapper_instance.call(target)
525
+ else
526
+ MAPPER_NOT_APPLIED
527
+ end
528
+ end
529
+
530
+ def resolve_source_value(target, cfg, data_sources)
531
+ source = cfg[:source]
532
+ return MAPPER_NOT_APPLIED if source.blank?
533
+
534
+ if source[:from] && data_sources
535
+ payload = data_sources[source[:from].to_sym]
536
+ if payload
537
+ return payload if source[:key].blank?
538
+ if payload.respond_to?(:key?)
539
+ return payload[source[:key].to_sym] if payload.key?(source[:key].to_sym)
540
+ return payload[source[:key].to_s] if payload.key?(source[:key].to_s)
541
+ elsif payload.respond_to?(:[])
542
+ return payload[source[:key].to_sym]
543
+ end
544
+ end
545
+ end
546
+
547
+ if source[:mapper]
548
+ mapped = resolve_mapper_value(target, source[:mapper])
549
+ return mapped unless mapped.equal?(MAPPER_NOT_APPLIED)
550
+ end
551
+
552
+ proc_source = source[:proc]
553
+ if proc_source.respond_to?(:call)
554
+ return proc_source.arity == 1 ? proc_source.call(target) : proc_source.call(target, data_source_context)
555
+ end
556
+
557
+ MAPPER_NOT_APPLIED
558
+ end
559
+
560
+ def link_field?(field)
561
+ return false unless field&.data_type_id
562
+
563
+ link_id = ::HasHelpers::Cards::CardDataType::LINK.id
564
+ title_id = ::HasHelpers::Cards::CardDataType::TITLE.id
565
+
566
+ field.data_type_id == link_id || field.data_type_id == title_id
567
+ end
568
+
569
+ def resolve_url(field, cfg, raw_value, value, attr_name)
570
+ return nil unless link_field?(field) || hover_card_field?(cfg)
571
+
572
+ if cfg[:url_resolver].respond_to?(:call)
573
+ return cfg[:url_resolver].call(cfg, field, raw_value, value)
574
+ end
575
+
576
+ value.present? ? build_link_url(raw_value, attr_name, field) : nil
577
+ end
578
+
579
+ def badge_field?(field)
580
+ field.data_type_id == ::HasHelpers::Cards::CardDataType::BADGE.id
581
+ end
582
+
583
+ def hover_card_field?(cfg)
584
+ cfg[:data_type].to_sym == :hover_card
585
+ end
586
+
587
+ # Default record-id source for a hover_card field with no explicit hover_record_source.
588
+ # Reads the raw column off `target` (e.g. `target.carrier_id`).
589
+ def default_hover_record_value(target, cfg)
590
+ return nil unless target
591
+
592
+ field = cfg[:card_field]
593
+ attr_name =
594
+ if field.respond_to?(:field_resource) && field.field_resource
595
+ field.field_resource.name.to_s
596
+ elsif field.respond_to?(:virtual_ref) && field.virtual_ref.present?
597
+ field.virtual_ref.to_s
598
+ end
599
+ return nil if attr_name.blank?
600
+ return nil unless target.respond_to?(attr_name)
601
+
602
+ target.public_send(attr_name)
603
+ end
604
+
605
+ BADGE_THEMES = %w[default yellow green red redBackground blue purple sky pink orange].freeze
606
+
607
+ def badge_list_field?(cfg)
608
+ cfg[:data_type].to_sym == :badge_list
609
+ end
610
+
611
+ def resolve_badge_list(raw_value)
612
+ return [] unless raw_value.is_a?(Array)
613
+
614
+ raw_value.map do |entry|
615
+ hash =
616
+ case entry
617
+ when Hash then entry.symbolize_keys
618
+ when ActionController::Parameters then entry.to_unsafe_h.symbolize_keys
619
+ else next nil
620
+ end
621
+ label = hash[:label].to_s
622
+ next nil if label.empty?
623
+
624
+ color = hash[:color].to_s
625
+ { label: label, color: BADGE_THEMES.include?(color) ? color : "default" }
626
+ end.compact
627
+ end
628
+
629
+ def resolve_hover_metadata(target, cfg, data_sources, _field_value = nil)
630
+ null_hover_metadata = { identifier: nil, resource_name: nil, record_id: nil }
631
+ resolved_cfg = resolve_dynamic_hover_keys(target, cfg, data_sources)
632
+ resource_name, hover_identifier = canonicalize_hover_target(resolved_cfg)
633
+ return null_hover_metadata unless hover_identifier
634
+
635
+ # If the DSL provides an explicit `hover_record_source`, resolve through it.
636
+ # Otherwise, fall back to reading the raw column off the target (e.g. for
637
+ # `field :carrier_id, data_type: HOVER_CARD` we read `target.carrier_id`).
638
+ hover_value =
639
+ if resolved_cfg[:hover_record_source].present?
640
+ resolve_source_value(
641
+ target,
642
+ resolved_cfg.merge(source: resolved_cfg[:hover_record_source]),
643
+ data_sources
644
+ )
645
+ else
646
+ default_hover_record_value(target, resolved_cfg)
647
+ end
648
+
649
+ return null_hover_metadata if hover_value.nil? || hover_value.equal?(MAPPER_NOT_APPLIED)
650
+
651
+ payload =
652
+ case hover_value
653
+ when Hash
654
+ hover_value.symbolize_keys
655
+ when ActionController::Parameters
656
+ hover_value.to_unsafe_h.symbolize_keys
657
+ end
658
+
659
+ record_id =
660
+ if payload
661
+ payload[:record_id] || payload[:id]
662
+ elsif hover_value.respond_to?(:id)
663
+ hover_value.id
664
+ else
665
+ hover_value
666
+ end
667
+
668
+ # Payload may carry an explicit resource_name (e.g. polymorphic case
669
+ # where the data_source returns { resource_name:, record_id: }). Prefer
670
+ # that over the cfg-derived value when present.
671
+ resolved_resource_name = (payload && payload[:resource_name].presence) || resource_name
672
+
673
+ {
674
+ identifier: hover_identifier,
675
+ resource_name: resolved_resource_name,
676
+ record_id: record_id
677
+ }
678
+ end
679
+
680
+ # If the DSL provides `hover_card_source` / `hover_resource_source`, resolve those
681
+ # against the data sources and substitute into a copy of the cfg so downstream
682
+ # logic can treat them as if they were static `hover_card` / `hover_resource`.
683
+ def resolve_dynamic_hover_keys(target, cfg, data_sources)
684
+ hover_card = cfg[:hover_card]
685
+ hover_resource = cfg[:hover_resource]
686
+
687
+ if cfg[:hover_card_source].present?
688
+ dynamic = resolve_source_value(
689
+ target,
690
+ cfg.merge(source: cfg[:hover_card_source]),
691
+ data_sources
692
+ )
693
+ hover_card = dynamic.to_s if dynamic.present? && !dynamic.equal?(MAPPER_NOT_APPLIED)
694
+ end
695
+
696
+ if cfg[:hover_resource_source].present?
697
+ dynamic = resolve_source_value(
698
+ target,
699
+ cfg.merge(source: cfg[:hover_resource_source]),
700
+ data_sources
701
+ )
702
+ hover_resource = dynamic.to_s if dynamic.present? && !dynamic.equal?(MAPPER_NOT_APPLIED)
703
+ end
704
+
705
+ cfg.merge(hover_card: hover_card, hover_resource: hover_resource)
706
+ end
707
+
708
+ # True when the dev provided no hover_* config at all on this field, so
709
+ # we're free to infer one from convention.
710
+ def hover_config_blank?(cfg)
711
+ cfg[:hover_card].blank? &&
712
+ cfg[:hover_card_source].blank? &&
713
+ cfg[:hover_resource].blank? &&
714
+ cfg[:hover_resource_source].blank?
715
+ end
716
+
717
+ # Re-derive raw_value cheaply for inference. (We don't have it in scope
718
+ # from resolve_field_value_and_url after the splat, so this is the same
719
+ # path that resolves through the source/mapper/attribute fallback chain.)
720
+ def raw_value_for_link(target, cfg, data_sources)
721
+ resolve_raw_field_value(target, cfg, field_attribute_name(cfg[:card_field]), data_sources, nil)
722
+ end
723
+
724
+ # Infer hover metadata for a plain link field by checking whether the
725
+ # registry has a `<Resource>InfoCard` hover-only card for the resource
726
+ # class the link points at. Returns nil when the resource class can't
727
+ # be determined or no matching hover card exists.
728
+ def infer_link_hover_metadata(cfg, _value, raw_value)
729
+ record_id = extract_link_record_id(raw_value)
730
+ return nil if record_id.blank?
731
+
732
+ klass = infer_link_klass(raw_value, cfg)
733
+ return nil unless klass
734
+
735
+ identifier = "#{klass.name.demodulize}InfoCard"
736
+ registry_card = find_hover_only_registry_card(identifier)
737
+ return nil unless registry_card
738
+
739
+ {
740
+ identifier: identifier,
741
+ resource_name: klass.name.to_s,
742
+ record_id: record_id
743
+ }
744
+ end
745
+
746
+ # Determine the AR class the link is pointing at — same priority order
747
+ # as link_path_segment: virtual_ref instance class → field_resource
748
+ # klass → attr_name-as-class.
749
+ def infer_link_klass(raw_value, cfg)
750
+ field = cfg[:card_field]
751
+ if raw_value && !primitive_link_value?(raw_value) && raw_value.class.respond_to?(:base_class)
752
+ return raw_value.class.base_class
753
+ end
754
+
755
+ if field.respond_to?(:field_resource) && field.field_resource.present?
756
+ name = field.field_resource.field_class_name
757
+ klass = name && (name.safe_constantize || ::HasHelpers::ApplicationHelper.translate_klass(name))
758
+ return klass if klass
759
+ end
760
+
761
+ attr_name = field_attribute_name(field)
762
+ if attr_name.present?
763
+ guess = attr_name.to_s.delete_suffix("_id").camelize
764
+ klass = guess.safe_constantize || ::HasHelpers::ApplicationHelper.translate_klass(guess)
765
+ return klass if klass
766
+ end
767
+
768
+ nil
769
+ end
770
+
771
+ def find_hover_only_registry_card(identifier_name)
772
+ @hover_only_cards_by_identifier ||=
773
+ ::HasHelpers::Cards::Registry.cards.each_with_object({}) do |card, memo|
774
+ next unless card[:hover_only]
775
+ id = card[:identifier]
776
+ id_str = id.respond_to?(:name) && !id.is_a?(String) ? id.name.to_s : id.to_s
777
+ memo[id_str] = card
778
+ end
779
+ @hover_only_cards_by_identifier[identifier_name.to_s]
780
+ end
781
+
782
+ # The builder may derive `:created_by_id` → `hover_resource: "CreatedBy"` from the column
783
+ # name, but "CreatedBy" is not always a real class (it's the role on Policy). When that's
784
+ # the case we fall back to the application's klass translator (e.g. iamhq maps
785
+ # "CreatedBy" → `HasHelpers::User`) and rewrite both `hover_resource` and `hover_card`
786
+ # to point at the real class.
787
+ def canonicalize_hover_target(cfg)
788
+ resource_name = cfg[:hover_resource].presence
789
+ identifier = cfg[:hover_card].presence
790
+ return [resource_name, identifier] unless resource_name
791
+
792
+ klass = resource_name.safe_constantize ||
793
+ ::HasHelpers::ApplicationHelper.translate_klass(resource_name)
794
+ return [resource_name, identifier] unless klass
795
+
796
+ canonical = klass.name.to_s
797
+ # Rewrite identifier only if it was the convention-derived one (matches the original
798
+ # hover_resource), preserving explicit user values.
799
+ rewritten_identifier =
800
+ identifier == "#{resource_name}InfoCard" ? "#{canonical.demodulize}InfoCard" : identifier
801
+
802
+ [canonical, rewritten_identifier]
803
+ end
804
+
805
+ def build_link_url(raw_value, attr_name, field)
806
+ record_id = extract_link_record_id(raw_value)
807
+ path_segment = link_path_segment(raw_value, attr_name, field)
808
+
809
+ return nil unless record_id && path_segment
810
+
811
+ "/#{path_segment}/#{record_id}"
812
+ end
813
+
814
+ def extract_link_record_id(raw_value)
815
+ return raw_value if raw_value.is_a?(String) || raw_value.is_a?(Numeric)
816
+ return raw_value.id if raw_value.respond_to?(:id)
817
+
818
+ nil
819
+ end
820
+
821
+ def link_path_segment(raw_value, attr_name, field)
822
+ if field.respond_to?(:virtual_ref) && field.virtual_ref.present? &&
823
+ raw_value && !primitive_link_value?(raw_value)
824
+ return demodulized_plural(raw_value.class)
825
+ end
826
+
827
+ if field.field_resource.present?
828
+ klass_segment = field_resource_class_segment(field)
829
+ return klass_segment if klass_segment
830
+ end
831
+
832
+ source = attr_name.presence || field.field_resource&.name
833
+ if source.present?
834
+ return source.to_s.delete_suffix("_id").underscore.pluralize
835
+ end
836
+
837
+ return demodulized_plural(raw_value.class) if raw_value && !primitive_link_value?(raw_value)
838
+
839
+ nil
840
+ end
841
+
842
+ def field_resource_class_segment(field)
843
+ class_name = field.field_resource&.field_class_name
844
+ return nil if class_name.blank?
845
+
846
+ klass =
847
+ class_name.safe_constantize ||
848
+ ::HasHelpers::ApplicationHelper.translate_klass(class_name)
849
+ return demodulized_plural(klass) if klass
850
+
851
+ nil
852
+ end
853
+
854
+ def primitive_link_value?(value)
855
+ value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(Symbol)
856
+ end
857
+
858
+ def demodulized_plural(klass)
859
+ return nil unless klass
860
+
861
+ base = klass.respond_to?(:base_class) ? klass.base_class : klass
862
+ base.name.demodulize.underscore.pluralize
863
+ end
864
+
865
+ def data_source_context
866
+ @data_source_context ||= Struct.new(:role, :resource, :identifier).new(role, resource, identifier)
867
+ end
868
+
869
+ def resolve_badge_color(cfg, value)
870
+ source = cfg[:badge_color]
871
+ color_value =
872
+ if source.respond_to?(:call)
873
+ source.call(cfg, value)
874
+ else
875
+ source
876
+ end
877
+
878
+ color_value.presence || ::Color::GREY.name
879
+ end
880
+
881
+ def resolve_badge_color_source(field, registry_meta)
882
+ return field.badge_color if field.respond_to?(:badge_color) && field.badge_color
883
+
884
+ registry_meta && registry_meta[:badge_color]
885
+ end
886
+
887
+ def resolve_url_resolver_source(field, registry_meta)
888
+ return field.url if field.respond_to?(:url) && field.url
889
+
890
+ registry_meta && registry_meta[:url]
891
+ end
892
+
893
+ def option_name_or_value(value)
894
+ return nil if value.nil?
895
+ return value.name if value.respond_to?(:name)
896
+
897
+ value
898
+ end
899
+
900
+ # Protection against infinite recursion: cap depth and degrade gracefully to string.
901
+ MAX_JSONIFY_DEPTH = 6
902
+
903
+ def jsonify_value(value, depth: 0)
904
+ return nil if value.nil?
905
+
906
+ # 1. Handling objects with ID (Models or RSpec Mocks)
907
+ return value.id if value.respond_to?(:id) && !value.is_a?(Class)
908
+
909
+ # 2. Primitive types: return as-is (strings, numbers, booleans)
910
+ case value
911
+ when String, Integer, Float, TrueClass, FalseClass
912
+ return value
913
+ when Symbol
914
+ return value.to_s
915
+ when BigDecimal
916
+ return value.to_f
917
+ when Time, Date, DateTime
918
+ return value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
919
+ end
920
+
921
+ # 3. Security control against infinite recursion
922
+ return value.to_s if depth >= MAX_JSONIFY_DEPTH
923
+
924
+ # 4. Data structures: recursively jsonify Hashes, Arrays, and Parameters, with depth control
925
+ case value
926
+ when ActionController::Parameters
927
+ jsonify_value(value.to_unsafe_h, depth: depth + 1)
928
+ when Hash
929
+ value.each_with_object({}) do |(k, v), acc|
930
+ acc[k.to_s] = jsonify_value(v, depth: depth + 1)
931
+ end
932
+ when Array
933
+ value.map { |v| jsonify_value(v, depth: depth + 1) }
934
+ else
935
+ # Fallback for other objects: try as_json, but avoid infinite recursion if it returns self
936
+ if value.respond_to?(:as_json)
937
+ json_data = value.as_json
938
+ # Avoid infinite recursion if as_json returns the object itself
939
+ json_data.equal?(value) ? value.to_s : jsonify_value(json_data, depth: depth + 1)
940
+ else
941
+ value.to_s
942
+ end
943
+ end
944
+ end
945
+
946
+ # Record + relationship
947
+ def load_record
948
+ # Prefer a preloaded record when available (used by .call_with_record)
949
+ return @load_record if defined?(@load_record) && @load_record
950
+
951
+ if @record
952
+ @load_record = @record
953
+ elsif record_id.present?
954
+ @load_record = resource.klass.find(record_id)
955
+ else
956
+ raise ArgumentError, "CardDataCommand requires either record or record_id"
957
+ end
958
+ end
959
+
960
+ def target_records(record, relationship, scope, expansion_scope, _registry_card)
961
+ return Array.wrap(record) if relationship.blank?
962
+
963
+ assoc =
964
+ if record.respond_to?(relationship.to_s)
965
+ record.public_send(relationship.to_s)
966
+ elsif record.respond_to?(relationship.to_s.singularize)
967
+ record.public_send(relationship.to_s.singularize)
968
+ end
969
+
970
+ records = assoc.presence
971
+ records = apply_scope(records, scope)
972
+ records = apply_scope(records, expansion_scope)
973
+
974
+ Array.wrap(records.presence)
975
+ end
976
+
977
+ def apply_scope(records, scope)
978
+ return records if scope.blank?
979
+
980
+ if scope.respond_to?(:call)
981
+ case scope.arity
982
+ when 1 then scope.call(records)
983
+ when 2 then scope.call(records, data_source_context)
984
+ else scope.call(records)
985
+ end
986
+ elsif records.respond_to?(scope.to_s)
987
+ records.public_send(scope.to_s)
988
+ else
989
+ records
990
+ end
991
+ end
992
+
993
+ # Registry (HasHelpers::Cards::Registry)
994
+ def relationship_for(card_definition, registry_card = nil)
995
+ registry_card ||= registry_card_for(card_definition)
996
+ registry_card && registry_card[:relationship].presence
997
+ end
998
+
999
+ def expansion_scope_for(card_definition, registry_card = nil)
1000
+ registry_card ||= registry_card_for(card_definition)
1001
+ registry_card && registry_card[:expansion_scope].presence
1002
+ end
1003
+
1004
+ def scope_for(card_definition, registry_card = nil)
1005
+ registry_card ||= registry_card_for(card_definition)
1006
+ registry_card && registry_card[:scope].presence
1007
+ end
1008
+
1009
+ def registry_card_for(card_definition)
1010
+ @registry_cards ||= ::HasHelpers::Cards::Registry.cards.index_by { |c| normalize_identifier(c[:identifier]) }
1011
+ @registry_cards[normalize_identifier(card_definition.identifier)]
1012
+ end
1013
+
1014
+ def registry_field_map_for(card_definition)
1015
+ card = registry_card_for(card_definition)
1016
+ return {} unless card
1017
+
1018
+ Array(card[:fields]).each_with_object({}) do |field_hash, memo|
1019
+ identity =
1020
+ case field_hash[:kind].to_sym
1021
+ when :custom
1022
+ [:custom, field_hash[:key].to_s.strip.gsub(/\s+/, "_").underscore]
1023
+ when :virtual
1024
+ [:virtual, field_hash[:ref].to_s]
1025
+ else
1026
+ [:resource, field_hash[:ref].to_s]
1027
+ end
1028
+
1029
+ memo[identity] = field_hash
1030
+ end
1031
+ end
1032
+
1033
+ def normalize_identifier(identifier_value)
1034
+ if identifier_value.respond_to?(:name) && !identifier_value.is_a?(String)
1035
+ identifier_value.name.to_s
1036
+ else
1037
+ identifier_value.to_s
1038
+ end
1039
+ end
1040
+
1041
+ def safe_id(record)
1042
+ record.respond_to?(:id) ? record.id : nil
1043
+ end
1044
+
1045
+ def dig_any(obj, key)
1046
+ # - "foo" (hash key)
1047
+ # - "foo.bar.baz" (path)
1048
+ # - symbol keys vs string keys
1049
+ parts = key.to_s.split(".")
1050
+ parts.reduce(obj) do |acc, k|
1051
+ break nil if acc.nil?
1052
+ if acc.is_a?(Hash)
1053
+ acc[k] || acc[k.to_sym]
1054
+ else
1055
+ # optional: support OpenStruct
1056
+ acc.respond_to?(k) ? acc.public_send(k) : nil
1057
+ end
1058
+ end
1059
+ end
1060
+ end
1061
+ end
1062
+ end