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,890 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HasHelpers::Index
4
+ class HasHelpers::Index::Error < StandardError; end
5
+
6
+ MAX_NGRAM_SIZE = 20 # TODO Try to reduce this
7
+ MIN_NGRAM_SIZE = 1
8
+ MAX_NGRAM_DIFF = MAX_NGRAM_SIZE - MIN_NGRAM_SIZE
9
+
10
+ def self.search(query, indexes:, where: {}, routing: nil, show_inactive: nil, **options)
11
+ unless where[:organization_id].present? || !routing.nil?
12
+ raise HasHelpers::Index::Error, <<-MSG.squish
13
+ `organization_id` is required inside the `where` option
14
+ unless `routing` is specified. Add `routing: false` to search
15
+ across all organizations.
16
+ MSG
17
+ end
18
+
19
+ organization_id = where[:organization_id]
20
+ indexes = Array(indexes)
21
+ weighted_attrs = indexes.map(&:weighted_attr).uniq
22
+ weighted_attr = weighted_attrs.length == 1 ? weighted_attrs.first : "updated_at"
23
+
24
+ where = where.except(:organization_id)
25
+ where[:_and] ||= []
26
+ where[:_and] << {
27
+ _or: [
28
+ { organization_id: organization_id },
29
+ { organization_id: nil }
30
+ ]
31
+ }
32
+
33
+ unless show_inactive || indexes.any?(&:show_end_dated_records?)
34
+ [
35
+ { is_inactive: nil },
36
+ {
37
+ _or: [
38
+ { start_date: nil },
39
+ { start_date: { lte: ::Date.today } }
40
+ ]
41
+ },
42
+ {
43
+ _or: [
44
+ { end_date: nil },
45
+ { end_date: { gte: ::Date.today } }
46
+ ]
47
+ }
48
+ ].each { |default_filter| where[:_and] << default_filter }
49
+ end
50
+
51
+ routing_key =
52
+ if routing == false
53
+ nil
54
+ else
55
+ default_routing_key =
56
+ if indexes.any?(&:is_global?)
57
+ [0, organization_id]
58
+ else
59
+ organization_id
60
+ end
61
+
62
+ routing || default_routing_key
63
+ end
64
+
65
+ organization = ::HasHelpers::Organization.find_by(id: organization_id)
66
+ use_keyword_query = organization && ::HasHelpers::Feature.active?(:keyword_search, organization) && is_keyword_query?(query, indexes)
67
+ index_name = use_keyword_query ? indexes.map(&:keyword_search_index_name) : indexes.map(&:index_name)
68
+ Searchkick.search(
69
+ query,
70
+ index_name: index_name.size > 1 ? index_name.select { |index| active_indices.include? index } : index_name, # only use available indexes for gloabl search
71
+ fields: use_keyword_query ? nil : [:all_fields],
72
+ load: false,
73
+ misspellings: false,
74
+ routing: routing_key,
75
+ where: where,
76
+ **options
77
+ ) do |body|
78
+ q = body[:query]
79
+
80
+ q[:bool][:must][:bool][:should][0][:dis_max][:queries].tap do |queries|
81
+ if use_keyword_query
82
+ queries.clear
83
+ keyword_query_array = []
84
+ keyword_queries = parse_keyword_query(query, indexes)
85
+ keyword_queries.each do |field_name, search_text|
86
+ keyword_query_array << {
87
+ match: {
88
+ field_name => {
89
+ query: search_text,
90
+ boost: 20,
91
+ operator: "and",
92
+ analyzer: "classic_search"
93
+ }
94
+ }
95
+ }
96
+ end
97
+ queries << { bool: { must: keyword_query_array } }
98
+ else
99
+ # Weight name matches higher
100
+ if organization && ::HasHelpers::Feature.active?(:classic_name_search, organization)
101
+ queries << {
102
+ match: {
103
+ "name.analyzed" => {
104
+ query: query,
105
+ boost: 25,
106
+ operator: "and",
107
+ analyzer: "classic_search"
108
+ }
109
+ }
110
+ }
111
+ else
112
+ queries << {
113
+ match: {
114
+ "name.analyzed" => {
115
+ query: query,
116
+ boost: 25,
117
+ operator: "and",
118
+ analyzer: "searchkick_search"
119
+ }
120
+ }
121
+ }
122
+ end
123
+
124
+ # Tokens matched using the classic search should be weighted between our searchkick queries (10)
125
+ # and the name query (25). Classic search is useful for searching emails or acronyms.
126
+ queries << {
127
+ match: {
128
+ "all_fields.analyzed" => {
129
+ query: query,
130
+ boost: 20,
131
+ operator: "and",
132
+ analyzer: "classic_search"
133
+ }
134
+ }
135
+ }
136
+
137
+ # Also search keywords with the classic search.
138
+ # The analyzed field uses tokens filtered by nGrams which can make
139
+ # it so that results aren't found if the query is tokenized into
140
+ # tokens greater than the max ngram length
141
+ if query.length > MAX_NGRAM_SIZE
142
+ queries << {
143
+ match: {
144
+ "all_fields.keyword" => {
145
+ query: query,
146
+ boost: 20,
147
+ operator: "and",
148
+ analyzer: "classic_search"
149
+ }
150
+ }
151
+ }
152
+ end
153
+ end
154
+
155
+ # Use exact search if query contains double quotes
156
+ if query.count('"') > 1
157
+ q[:bool][:must] = [q[:bool][:must]]
158
+ q[:bool][:must] << {
159
+ match: {
160
+ "all_fields.keyword" => {
161
+ query: query,
162
+ operator: "and",
163
+ analyzer: "keyword_search"
164
+ }
165
+ }
166
+ }
167
+ end
168
+ end
169
+
170
+ # Weight recently viewed/updated records higher
171
+ body[:query] = {
172
+ function_score: {
173
+ score_mode: "sum",
174
+ boost_mode: "multiply",
175
+ functions: [{ weight: 1 }, { weight: 5, gauss: { "#{weighted_attr}": { scale: "365d", decay: 0.5 } } }],
176
+ query: q
177
+ }
178
+ }
179
+ end
180
+ end
181
+
182
+ def self.included(mod)
183
+ @indices ||= Set.new
184
+ @indices.add(mod)
185
+ mod.extend(ClassMethods)
186
+ mod.send(:configure)
187
+ end
188
+
189
+ # indexes only one big resource at a time
190
+ def self.scheduled_reindexing
191
+ all_index_names = ::HasHelpers::Index.indices.to_a.map(&:name)
192
+ all_index_names.each do |index_name|
193
+ ElasticsearchReindexScheduler.perform_async(index_name)
194
+ end
195
+ end
196
+
197
+ # HasHelpers::Index.indices
198
+ def self.indices
199
+ @indices
200
+ end
201
+
202
+ # Returns all indices that are in use. It does this
203
+ # by checking if they have an alias.
204
+ def self.active_indices
205
+ Searchkick.client.indices.get_alias.map { |_name, options| options["aliases"].keys }.flatten.compact
206
+ end
207
+
208
+ def self.config
209
+ ::HasHelpers.config.index
210
+ end
211
+
212
+ def self.after_index(key)
213
+ config.after_index.call(key)
214
+ end
215
+
216
+ def self.before_index(klass)
217
+ config.before_index.call(klass)
218
+ end
219
+
220
+ def self.filter_hidden_constants(results, organization_id)
221
+ config.filter_hidden_constants.call(results, organization_id)
222
+ end
223
+
224
+ # Sets default attributes to add to every index records.
225
+ # ==Example
226
+ # HasHelpers::Index.default_attributes do
227
+ # {
228
+ # is_inactive: false,
229
+ # indexed_at: Time.now
230
+ # }
231
+ # end
232
+ def self.default_attributes(&block)
233
+ if block.present?
234
+ @default_attributes = block.call
235
+ else
236
+ @default_attributes || { is_inactive: nil }
237
+ end
238
+ end
239
+
240
+ def self.parse_keyword_query(query_string, indexes = indices)
241
+ queries = {}
242
+ columns_to_search = indexes.map(&:columns_to_search).flatten.uniq
243
+ separated_query_string = query_string.downcase.split(/(?:\s+(?=\S+:))/).reject(&:empty?).map(&:rstrip)
244
+ separated_query_string.each do |single_query|
245
+ if /^(?<query_field>\w+):\s*(?<query_term>.+)\s*/ =~ single_query
246
+ field_name = query_term.length > MAX_NGRAM_SIZE ? "keyword" : "wildcard_index"
247
+ queries["#{query_field}.#{field_name}"] = query_term if columns_to_search.include?(query_field.to_sym)
248
+ end
249
+ end
250
+ queries
251
+ end
252
+
253
+ def self.is_keyword_query?(query_string, indexes = indices)
254
+ query_string.include?(":") &&
255
+ indexes.each do |idx|
256
+ return true if /#{idx.keyword_search_field_regex}/.match?(query_string)
257
+ end
258
+ false
259
+ end
260
+
261
+ module ClassMethods
262
+ EXCLUDED_SEARCH_TYPES = [:date, :datetime, :uuid]
263
+
264
+ def weighted_attr(attr_name = :updated_at)
265
+ @weighted_attr ||= attr_name
266
+ end
267
+
268
+ def is_heavy_index(value = false)
269
+ @is_heavy_index ||= value
270
+ end
271
+
272
+ def search(query, **options)
273
+ HasHelpers::Index.search(query, indexes: [self], **options)
274
+ end
275
+
276
+ def refresh
277
+ client.indices.refresh index: index_name
278
+ client.indices.refresh index: keyword_search_index_name
279
+ end
280
+
281
+ def reindex
282
+ ::ActiveRecord::Base.connection.execute <<-SQL
283
+ DROP VIEW IF EXISTS #{view_name};
284
+ CREATE VIEW #{view_name} AS #{self.sql};
285
+ SQL
286
+ clean
287
+ new_name = "#{index_name}_#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}"
288
+ new_keyword_index_name = "#{keyword_search_index_name}_#{Time.zone.now.strftime('%Y%m%d%H%M%S%L')}"
289
+ # Create the new index, with refresh turned off for faster reindexing.
290
+ create(new_name, settings.deep_merge!(settings: { index: { refresh_interval: -1 } }))
291
+ create(new_keyword_index_name, keyword_index_settings.deep_merge!(settings: { index: { refresh_interval: -1 } }))
292
+ client.indices.put_settings(body: { index: { blocks: { read_only_allow_delete: false } } })
293
+ ElasticsearchReindex.perform_async(self.name, new_name)
294
+ ElasticsearchReindex.perform_async(self.name, new_keyword_index_name)
295
+ "#{new_name} #{new_keyword_index_name}"
296
+ end
297
+
298
+ def reindex_queue
299
+ ::HasHelpers::ReindexQueue.new(index_name)
300
+ end
301
+
302
+ def keyword_reindex_queue
303
+ ::HasHelpers::ReindexQueue.new(keyword_search_index_name)
304
+ end
305
+
306
+ def model
307
+ @model ||= name.remove("Index").constantize
308
+ end
309
+
310
+ # Specifies what other models should trigger a reindex, as well as the conditions for triggering one.
311
+ # The <b>to</b> attribute the key to search against the from set. <b>id</b> by default.
312
+ # The <b>from</b> attribute to get the object from. By default <b>model.name.foreign_key e.g. advisor_id</b> or <b>id</b> if dependency_inheritance is present.
313
+ # The <b>dependent_attributes</b> attribute is a list of the attributes that has to be changed to make a reindex
314
+ # The <b>dependency_inheritance</b> attribute is a inheritance list of lists declaring the paths to get to the resource.
315
+ #
316
+ # Example usage (using ::AdvisorIndex as the example index):
317
+ #
318
+ # depends_on(::Contract)
319
+ # This will make it so that any change to a contract that has an advisor_id will cause the advisor to be reindexed.
320
+ #
321
+ # depends_on("Contract": { dependent_attributes: %w(writing_number advisor_id) })
322
+ # This will make it so that the advisor is reindexed only when the contract's writing number or advisor id is updated.
323
+ #
324
+ # Example usage (using ::ContractIndex as the example index):
325
+ #
326
+ # depends_on("ContractLevel": { dependent_attributes: %w(product_type_id) })
327
+ # This will make it so that the ContractIndex is reindexed only when the contract level's product_type_id is updated.
328
+ #
329
+ # depends_on("Carrier": { dependent_attributes: %w(name),
330
+ # dependency_inheritance: [
331
+ # <tt>[</tt> { target: "Contract", to: "carrier_id" } <tt>]</tt>
332
+ # ]
333
+ # })
334
+ # As the Carrier has an indirect reference to a contract we have to set the inheritance to get to it.
335
+ # This will update the ContractIndex only when the carrier's name is updated through the path defined on the
336
+ # dependency_inheritance array. In this case this contract will be updated:
337
+ # Contract.where( carrier_id: "UPDATED_CARRIER_ID" )
338
+ #
339
+ # depends_on("HasContactInfo::Demographic": { dependent_attributes: %w(nickname ssn),
340
+ # dependency_inheritance: [
341
+ # [ { target: "Advisor", to: "demographic_id" },
342
+ # { target: "Contract", to: "advisor_id" }
343
+ # ]
344
+ # ]
345
+ # })
346
+ # This will make it so that the Contract is reindex only when (following the inheritance) the Contract's Advisor's
347
+ # nicknamme or ssn are updated
348
+ #
349
+ # depends_on("Firm": { from: "id", dependent_attributes: %w(name taxid),
350
+ # dependency_inheritance: [
351
+ # <tt>[</tt>{ target: "Contract", to: "corporate_id" }<tt>]</tt> ,
352
+ # <tt>[</tt>{ target: "Contract", to: "broker_dealer_id" }<tt>]</tt>,
353
+ # <tt>[</tt>{ target: "Contract", to: "firm_id" }<tt>]</tt>
354
+ # ]
355
+ # })
356
+ # In this case we have 3 lists inside the dependency_inheritance list. Each will be independet of each other.
357
+ # This will make it so that the Contract is reindexed ony when:
358
+ # 1. The Contract's corporate name or taxid are updated
359
+ # 2. The Contract's broker_dealer name or taxid are updated
360
+ # 3. The Contract's firm name or taxid gets updated
361
+ #
362
+ def depends_on(*klasses, **klasses_with_keys)
363
+ @depends_on ||= [].tap do |dependency_array|
364
+ klasses.each do |dependency_klass|
365
+ klass = dependency_klass.to_s.safe_constantize
366
+ dependency_array << add_dependency(klass: klass)
367
+ end
368
+ klasses_with_keys.each do |klass_name, keys|
369
+ dependency_array << add_dependency(
370
+ klass: klass_name.to_s.safe_constantize,
371
+ from_key: keys[:from], to_key: keys[:to],
372
+ dependent_attributes: keys[:dependent_attributes],
373
+ dependency_inheritance: keys[:dependency_inheritance])
374
+ end
375
+ end
376
+ end
377
+
378
+ # Deletes all the indices that are not currently in use. It does this
379
+ # by checking if they have an alias.
380
+ def clean
381
+ indices_to_delete = old_indices
382
+
383
+ # If clean occurs during a reindex, and it runs in the time between
384
+ # when the new index is created and when it's assigned an alias, then
385
+ # it's possible that it will delete the newly created index which would
386
+ # cause reindexing to fail. We need to make sure that the most recent
387
+ # index is not affected by cleaning.
388
+ keyword_indices = indices.keys.select { |idx_name| idx_name.include?("keyword") }
389
+ non_keyword_indices = indices.keys - keyword_indices
390
+ most_recent_keyword_index_name = keyword_indices.sort.last
391
+ most_recent_index_name = non_keyword_indices.sort.last
392
+ [most_recent_index_name, most_recent_keyword_index_name].each do |most_recent_name|
393
+ if most_recent_name&.match?(/20\d{15}/) # If the most recent index doesn't have a timestamp, it should probably be deleted anyway.
394
+ indices_to_delete = indices_to_delete.reject do |non_aliased_index_name, _|
395
+ non_aliased_index_name == most_recent_name
396
+ end
397
+ end
398
+ end
399
+
400
+ indices_to_delete.each { |name, _options| delete(name) }
401
+ end
402
+
403
+ def clean!
404
+ raise Error, "clean! should only be used for testing" unless ::HasUtils::Env.test?
405
+
406
+ refresh
407
+ all_deletes = [index_name, keyword_search_index_name].map do |idx_name|
408
+ results = client.search(index: idx_name, body: { query: { match_all: {} } })
409
+ results.fetch("hits", {}).fetch("hits", []).map do |result|
410
+ {
411
+ delete: {
412
+ routing: result["_routing"],
413
+ _index: idx_name,
414
+ _id: result["_id"],
415
+ _type: result["_type"]
416
+ }
417
+ }
418
+ end
419
+ end
420
+ Searchkick.indexer.queue(all_deletes.flatten)
421
+ refresh
422
+ end
423
+
424
+ def indices
425
+ regex = /\A#{Regexp.escape(index_name)}/
426
+ keyword_regex = /\A#{Regexp.escape("keyword_" + index_name)}/
427
+ client.indices.get_alias.select { |name, _options| name.match?(regex) || name.match?(keyword_regex) }
428
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
429
+ {}
430
+ end
431
+
432
+ # Returns all indices without an alias.
433
+ # PolicyIndex.old_indices
434
+ def old_indices
435
+ indices.select { |_name, options| options["aliases"].blank? }
436
+ end
437
+
438
+ # Promotes an index for searching.
439
+ def promote(index)
440
+ index_alias = index.include?("keyword") ? keyword_search_index_name : index_name
441
+ old_indices =
442
+ begin
443
+ client.indices.get_alias(name: index_alias).keys
444
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
445
+ {}
446
+ end
447
+ actions = old_indices.map { |old_name| { remove: { index: old_name, alias: index_alias } } } + [{ add: { index: index, alias: index_alias } }]
448
+ client.indices.update_aliases body: { actions: actions }
449
+ end
450
+
451
+ def update_settings(index, settings)
452
+ client.indices.put_settings index: index, body: settings
453
+ end
454
+
455
+ def get_settings(index)
456
+ client.indices.get_settings index: index
457
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
458
+ # not found
459
+ {}
460
+ end
461
+
462
+ def heavy_indices
463
+ ::HasHelpers::Index.indices.to_a.select { |index| index.is_heavy_index }
464
+ end
465
+
466
+ def get_refresh_interval(settings, index_name)
467
+ settings.fetch(index_name, {}).fetch("settings", {}).fetch("index", {}).fetch("refresh_interval", nil)
468
+ end
469
+
470
+ def index_refresh_disabled?(refresh_interval)
471
+ refresh_interval == "-1"
472
+ end
473
+
474
+ def indexing_allowed?
475
+ heavy_indices.each do |h_index_name|
476
+ next if h_index_name == name
477
+
478
+ indices_with_aliases = indices.select { |_, index| !index["aliases"].empty? }
479
+ next if indices_with_aliases.empty?
480
+
481
+ indices_with_aliases.each do |index_name, _|
482
+ settings = get_settings(index_name)
483
+ refresh_interval = get_refresh_interval(settings, index_name)
484
+ return false if index_refresh_disabled?(refresh_interval)
485
+ end
486
+ end
487
+
488
+ true
489
+ end
490
+
491
+ # Searchkick.client.indices.get_alias(name: "policy_index_production").keys
492
+ # Searchkick.client.indices.delete(index: index)
493
+ def delete(index)
494
+ client.indices.delete(index: index)
495
+ end
496
+
497
+ def all_values_from_attributes(attrs)
498
+ normalize_fields(attrs.slice(*columns_to_search).values.compact)
499
+ end
500
+
501
+ def global_index!
502
+ @is_global = true
503
+ end
504
+
505
+ def is_global?
506
+ @is_global
507
+ end
508
+
509
+ def show_end_dated_records!
510
+ @show_end_dated_records = true
511
+ end
512
+
513
+ def show_end_dated_records?
514
+ @show_end_dated_records || false
515
+ end
516
+
517
+ def keyword_search_index_name
518
+ @keyword_search_index_name = ["keyword", type.tr("/", "_"), ENV["ENV_NAME"] || ::HasUtils::Env.current_environment].join("_")
519
+ end
520
+
521
+ def index_name
522
+ @index_name = [type.tr("/", "_"), ENV["ENV_NAME"] || ::HasUtils::Env.current_environment].join("_")
523
+ end
524
+
525
+ def type
526
+ name.underscore
527
+ end
528
+
529
+ def sql(sql = nil, &block)
530
+ @sql ||= sql || (block && block.call)
531
+ end
532
+
533
+ def active_record
534
+ @active_record ||= begin
535
+ ar = Class.new(::ActiveRecord::Base)
536
+ ar.primary_key = "id"
537
+ ar.table_name = view_name
538
+ ar.inheritance_column = nil
539
+ ar
540
+ end
541
+ end
542
+
543
+ def columns_to_search
544
+ @columns_to_search ||= active_record.columns.
545
+ reject { |column| EXCLUDED_SEARCH_TYPES.include?(column.type) || column.name =~ /_id[s]?$/ }.
546
+ map { |column| column.name.to_sym }
547
+ end
548
+
549
+ def fields
550
+ @fields ||= active_record.columns.map { |column| column.name.to_sym }
551
+ end
552
+
553
+ def filters
554
+ @filters
555
+ end
556
+
557
+ def filters=(filters)
558
+ @filters = filters.map(&:to_sym)
559
+ end
560
+
561
+ def view_name
562
+ "#{name.underscore.tr("/", "_")}_view"
563
+ end
564
+
565
+ def keyword_search_field_regex
566
+ # Sorting by longest length to avoid fields whose names are part of another field from taking priority.
567
+ # For example, if name was before code_name in the regex, then code_name:test would match on name:test
568
+ @keyword_search_field_regex ||= columns_to_search.sort_by(&:length).reverse.join(":|") + ":"
569
+ end
570
+
571
+ private
572
+
573
+ def add_dependency(klass:, from_key: nil, to_key: nil, dependent_attributes: nil, dependency_inheritance: nil)
574
+ from_column = from_key || (dependency_inheritance.blank? ? model.name.foreign_key : "id")
575
+ to_column = to_key || "id"
576
+ index_model = model
577
+ klass.class_eval do
578
+ after_commit ->(obj) do
579
+ index_model.process_dependencies(
580
+ obj,
581
+ from_column,
582
+ to_column,
583
+ index_model,
584
+ dependent_attributes,
585
+ dependency_inheritance,
586
+ obj.previous_changes
587
+ )
588
+ end
589
+
590
+ define_inheritance_search(klass)
591
+ end
592
+ { "#{klass.name}": { from: from_column, to: to_column, dependent_attributes: dependent_attributes, dependency_inheritance: dependency_inheritance } }
593
+ end
594
+
595
+ def normalize_fields(fields)
596
+ fields.map do |field|
597
+ field.is_a?(Array) ? normalize_fields(field) : field.to_s
598
+ end
599
+ end
600
+
601
+ def configure
602
+ index_klass = self
603
+ idx_name = index_name
604
+ keyword_idx_name = keyword_search_index_name
605
+
606
+ model.class_eval do
607
+ after_commit -> (obj) {
608
+ obj.reindex
609
+ }
610
+
611
+ define_singleton_method :elasticsearch do |query, **options|
612
+ index_klass.search query, **options
613
+ end
614
+
615
+ # Access the search index class from the model.
616
+ # Example: `Advisor.search_index` => `AdvisorIndex`
617
+ define_singleton_method :search_index do
618
+ index_klass
619
+ end
620
+
621
+ define_method :reindex do
622
+ key = HasHelpers::Index.before_index(self)
623
+ routing_key = index_klass.is_global? ? 0 : self.try(:organization_id)
624
+ index_klass.reindex_queue.push(self.id.to_s, key, routing_key: routing_key)
625
+ index_klass.keyword_reindex_queue.push(self.id.to_s, key, routing_key: routing_key)
626
+ ElasticsearchProcessQueue.perform_async(index_klass.name, idx_name)
627
+ ElasticsearchProcessQueue.perform_async(index_klass.name, keyword_idx_name)
628
+ end
629
+ end
630
+ end
631
+
632
+ def client
633
+ Searchkick.client
634
+ end
635
+
636
+ def create(name, options = settings)
637
+ client.indices.create index: name, body: options, include_type_name: true
638
+ end
639
+
640
+ # Searchkick has no easy way of defaulting indices to "word_start", so we have
641
+ # to use manual mappings. This is a step in the direction of not needing searchkick.
642
+ # TODO: Add an entry for each "filter", with include_in_all set to false. This will prevent
643
+ # searching filters.
644
+ def settings
645
+ super_settings = {
646
+ settings: {
647
+ index: {
648
+ max_ngram_diff: MAX_NGRAM_DIFF
649
+ },
650
+ analysis: {
651
+ analyzer: {
652
+ searchkick_index: {
653
+ type: "custom",
654
+ tokenizer: "classic",
655
+ filter: ["lowercase", "asciifolding", "searchkick_index_shingle"]
656
+ },
657
+ searchkick_search: {
658
+ type: "custom",
659
+ tokenizer: "classic",
660
+ filter: ["lowercase", "asciifolding", "searchkick_search_shingle"]
661
+ },
662
+ searchkick_search2: {
663
+ type: "custom",
664
+ tokenizer: "standard",
665
+ filter: ["lowercase", "asciifolding"]
666
+ },
667
+ classic_search: {
668
+ type: "custom",
669
+ tokenizer: "classic",
670
+ filter: ["lowercase", "asciifolding"]
671
+ },
672
+ searchkick_word_middle_index: {
673
+ type: "custom",
674
+ tokenizer: "classic",
675
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
676
+ },
677
+ keyword_index: {
678
+ type: "custom",
679
+ tokenizer: "keyword",
680
+ filter: ["lowercase", "asciifolding"]
681
+ },
682
+ keyword_search: {
683
+ type: "custom",
684
+ tokenizer: "keywords_with_quotes",
685
+ filter: ["lowercase", "asciifolding"]
686
+ }
687
+ },
688
+ tokenizer: {
689
+ keywords_with_quotes: {
690
+ type: "pattern",
691
+ pattern: /"((?:\\"|[^"]|\\")*)"/,
692
+ group: 1
693
+ }
694
+ },
695
+ filter: {
696
+ searchkick_index_shingle: {
697
+ type: "shingle",
698
+ token_separator: ""
699
+ },
700
+ searchkick_search_shingle: {
701
+ type: "shingle",
702
+ token_separator: "",
703
+ output_unigrams: false,
704
+ output_unigrams_if_no_shingles: true
705
+ },
706
+ searchkick_ngram: {
707
+ type: "ngram",
708
+ min_gram: MIN_NGRAM_SIZE,
709
+ max_gram: MAX_NGRAM_SIZE
710
+ }
711
+ },
712
+ char_filter: {
713
+ ampersand: {
714
+ type: "mapping",
715
+ mappings: ["\u0026=\u003e and "]
716
+ }
717
+ }
718
+ }
719
+ },
720
+ mappings: {
721
+ type => {
722
+ properties: {
723
+ "#{weighted_attr}": {
724
+ type: "date"
725
+ }
726
+ },
727
+ _routing: {
728
+ required: true
729
+ },
730
+ dynamic_templates: [
731
+ {
732
+ name_template: {
733
+ match: "name",
734
+ match_mapping_type: "string",
735
+ mapping: {
736
+ type: "keyword",
737
+ ignore_above: 30000,
738
+ fields: {
739
+ analyzed: {
740
+ type: "text",
741
+ analyzer: "searchkick_word_middle_index"
742
+ }
743
+ }
744
+ }
745
+ }
746
+ }, {
747
+ all_fields_template: {
748
+ match: "all_fields",
749
+ mapping: {
750
+ type: "keyword",
751
+ fields: {
752
+ analyzed: {
753
+ type: "text",
754
+ analyzer: "searchkick_word_middle_index"
755
+ },
756
+ keyword: {
757
+ type: "text",
758
+ analyzer: "keyword_index"
759
+ }
760
+ }
761
+ }
762
+ }
763
+ }, {
764
+ string_template: {
765
+ match: "*",
766
+ match_mapping_type: "string",
767
+ mapping: {
768
+ type: "keyword",
769
+ ignore_above: 30000,
770
+ fields: {
771
+ analyzed: {
772
+ type: "text",
773
+ analyzer: "searchkick_index"
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ ]
780
+ }
781
+ }
782
+ }
783
+ # Disable indexing for `result_type` to reduce heap usage and avoid unnecessary mapping overhead.
784
+ # This field is used only for rendering search results and does not need to be searchable.
785
+ # With `enabled: false`, it remains in _source but is excluded from indexing and analysis.
786
+ super_settings.deep_merge(
787
+ mappings: {
788
+ type => {
789
+ properties: {
790
+ result_type: { type: "object", enabled: false }
791
+ }
792
+ }
793
+ }
794
+ )
795
+ end
796
+
797
+ def keyword_index_settings
798
+ keyword_settings = settings
799
+ keyword_settings[:mappings][type][:dynamic_templates] = [
800
+ {
801
+ string_template: {
802
+ match: "*",
803
+ unmatch: "*id",
804
+ match_mapping_type: "string",
805
+ mapping: {
806
+ type: "keyword",
807
+ ignore_above: 30000,
808
+ fields: {
809
+ wildcard_index: {
810
+ type: "text",
811
+ analyzer: "searchkick_word_middle_index"
812
+ },
813
+ keyword: {
814
+ type: "text",
815
+ analyzer: "keyword_index"
816
+ }
817
+ }
818
+ }
819
+ }
820
+ }, {
821
+ string_template: {
822
+ match: "*id",
823
+ match_mapping_type: "string",
824
+ mapping: {
825
+ type: "keyword",
826
+ ignore_above: 30000,
827
+ fields: {
828
+ keyword: {
829
+ type: "text",
830
+ analyzer: "keyword_index"
831
+ }
832
+ }
833
+ }
834
+ }
835
+ }
836
+ ]
837
+ keyword_settings
838
+ end
839
+ end
840
+
841
+ module ActiveRecord
842
+ def self.included(mod)
843
+ mod.extend(ClassMethods)
844
+ end
845
+
846
+ module ClassMethods
847
+ def search_index
848
+ "#{self.base_class.name}Index".constantize
849
+ rescue
850
+ raise HasHelpers::Index::Error, "Index not defined for #{self.name}"
851
+ end
852
+
853
+ def process_dependencies(obj, from_column, to_column, index_model, dependent_attributes, dependency_inheritance, previous_changes)
854
+ if (id = obj.read_attribute(from_column)) &&
855
+ (dependent_attributes.nil? || (previous_changes.keys & dependent_attributes).any?)
856
+
857
+ if previous_changes[from_column]
858
+ index_model.where("#{to_column}": previous_changes[from_column].reject(&:nil?)).find_each(&:reindex)
859
+ else
860
+ index_model.find_by("#{to_column}": id)&.reindex
861
+ end
862
+ if dependency_inheritance.present?
863
+ dependency_inheritance.each do |keys|
864
+ keys_copy = keys.dup
865
+ obj.inheritance_search(keys_copy, id, index_model).reject(&:nil?).each(&:reindex)
866
+ end
867
+ end
868
+ end
869
+ end
870
+
871
+ def define_inheritance_search(model_class)
872
+ model_class.class_eval do
873
+ define_method("inheritance_search") do |dependency_array, id, model|
874
+ keys = dependency_array.pop
875
+ inner_to_column = keys[:to] || "id"
876
+ target = keys[:target].to_s.safe_constantize
877
+
878
+ if dependency_array.empty?
879
+ target.where("#{inner_to_column}": id)
880
+ else
881
+ target.where("#{inner_to_column}": inheritance_search(dependency_array, id, model).ids)
882
+ end
883
+ end
884
+ end
885
+ end
886
+ end
887
+ end
888
+ end
889
+
890
+ ::ActiveRecord::Base.include(HasHelpers::Index::ActiveRecord)