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.
- checksums.yaml +7 -0
- data/.gitignore +38 -0
- data/.rspec +3 -0
- data/.rubocop.yml +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tekton/Chart.yaml +14 -0
- data/.tekton/templates/pipelinerun.yaml +44 -0
- data/.tekton/values-main.yaml +53 -0
- data/.tekton/values-pr.yaml +40 -0
- data/.tekton/values.yaml +17 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +204 -0
- data/Gemfile +25 -0
- data/README.md +76 -0
- data/Rakefile +46 -0
- data/app/api/has_helpers/rack_api.rb +116 -0
- data/app/api/has_helpers/search_api.rb +173 -0
- data/app/assets/stylesheets/has_helpers/base.scss +5 -0
- data/app/assets/stylesheets/has_helpers/color_palette.scss +33 -0
- data/app/assets/stylesheets/has_helpers/color_palettes/default.scss +11 -0
- data/app/assets/stylesheets/has_helpers/components/dataset.scss +43 -0
- data/app/assets/stylesheets/has_helpers/mixins.scss +149 -0
- data/app/commands/has_helpers/cards/available_cards_command.rb +98 -0
- data/app/commands/has_helpers/cards/card_data_command.rb +1062 -0
- data/app/commands/reports/base_options.rb +79 -0
- data/app/commands/reports/belongs_to_options.rb +178 -0
- data/app/commands/reports/constant_options.rb +66 -0
- data/app/commands/reports/external_graphql_client.rb +116 -0
- data/app/commands/reports/fill_dates.rb +65 -0
- data/app/commands/reports/filter_option_generator.rb +186 -0
- data/app/commands/reports/generator.rb +310 -0
- data/app/commands/reports/has_many_options.rb +105 -0
- data/app/commands/reports/importer.rb +132 -0
- data/app/commands/reports/nested_attribute_options.rb +134 -0
- data/app/constants/color.rb +9 -0
- data/app/constants/file_format.rb +12 -0
- data/app/constants/has_helpers/cards/card_custom_field.rb +12 -0
- data/app/constants/has_helpers/cards/card_data_type.rb +11 -0
- data/app/constants/has_helpers/cards/card_identifier.rb +10 -0
- data/app/constants/has_helpers/cards/card_layout_type.rb +11 -0
- data/app/constants/has_helpers/cards/card_variant_type.rb +12 -0
- data/app/constants/has_helpers/link_type.rb +10 -0
- data/app/constants/has_helpers/notification_alert_status.rb +14 -0
- data/app/constants/has_helpers/notification_alert_type.rb +10 -0
- data/app/constants/has_helpers/operation.rb +11 -0
- data/app/constants/has_helpers/resource_type.rb +25 -0
- data/app/constants/icon.rb +22 -0
- data/app/constants/logical_operator.rb +7 -0
- data/app/constants/option.rb +10 -0
- data/app/constants/time_zone.rb +12 -0
- data/app/constructors/has_helpers/base_constructor.rb +62 -0
- data/app/controllers/concerns/has_helpers/service_trust/authenticated_as_service.rb +51 -0
- data/app/controllers/concerns/has_helpers/service_trust/graphql_authentication.rb +57 -0
- data/app/controllers/has_helpers/controller.rb +63 -0
- data/app/controllers/has_helpers/factory_controller.rb +261 -0
- data/app/controllers/has_helpers/graphql_extensions.rb +83 -0
- data/app/controllers/has_helpers/health_check_controller.rb +46 -0
- data/app/controllers/has_helpers/search_extensions.rb +92 -0
- data/app/controllers/has_helpers/service_trust/jwks_controller.rb +20 -0
- data/app/controllers/has_helpers/xlsx_export.rb +115 -0
- data/app/controllers/search_extensions.rb +5 -0
- data/app/graphql/has_helpers/base_type.rb +13 -0
- data/app/graphql/has_helpers/base_union.rb +9 -0
- data/app/graphql/has_helpers/enums/resource_enum.rb +24 -0
- data/app/graphql/has_helpers/middleware/application_access.rb +26 -0
- data/app/graphql/has_helpers/mutations/base.rb +27 -0
- data/app/graphql/has_helpers/mutations/external_data_sync.rb +72 -0
- data/app/graphql/has_helpers/mutations/mark_individual_alert_read.rb +31 -0
- data/app/graphql/has_helpers/mutations/mark_notifications_read.rb +50 -0
- data/app/graphql/has_helpers/mutations/toggle_favorite.rb +39 -0
- data/app/graphql/has_helpers/resolvers/app_nav.rb +107 -0
- data/app/graphql/has_helpers/resolvers/base.rb +27 -0
- data/app/graphql/has_helpers/resolvers/cards/available_cards.rb +31 -0
- data/app/graphql/has_helpers/resolvers/cards/card_data.rb +74 -0
- data/app/graphql/has_helpers/resolvers/cards/hover_card_data.rb +153 -0
- data/app/graphql/has_helpers/resolvers/favorite.rb +62 -0
- data/app/graphql/has_helpers/resolvers/internal/roles.rb +54 -0
- data/app/graphql/has_helpers/resolvers/is_favorite.rb +24 -0
- data/app/graphql/has_helpers/resolvers/list_notification_alerts.rb +47 -0
- data/app/graphql/has_helpers/resolvers/lookup.rb +36 -0
- data/app/graphql/has_helpers/resolvers/recently_viewed.rb +54 -0
- data/app/graphql/has_helpers/resolvers/resource_tab.rb +22 -0
- data/app/graphql/has_helpers/resolvers/search.rb +18 -0
- data/app/graphql/has_helpers/resolvers/search_query.rb +38 -0
- data/app/graphql/has_helpers/resolvers/user_application_order.rb +35 -0
- data/app/graphql/has_helpers/resources/organization_option_resource.rb +28 -0
- data/app/graphql/has_helpers/resources/organization_resource_resource.rb +18 -0
- data/app/graphql/has_helpers/resources/resource_group_resource.rb +10 -0
- data/app/graphql/has_helpers/resources/resource_resource.rb +21 -0
- data/app/graphql/has_helpers/resources/user_role_resource.rb +9 -0
- data/app/graphql/has_helpers/types/application_order_type.rb +18 -0
- data/app/graphql/has_helpers/types/cards/available_card_type.rb +20 -0
- data/app/graphql/has_helpers/types/cards/card_badge_type.rb +12 -0
- data/app/graphql/has_helpers/types/cards/card_config_type.rb +18 -0
- data/app/graphql/has_helpers/types/cards/card_data_type.rb +13 -0
- data/app/graphql/has_helpers/types/cards/card_entry_field_type.rb +21 -0
- data/app/graphql/has_helpers/types/cards/card_entry_type.rb +12 -0
- data/app/graphql/has_helpers/types/cards/card_field_config_type.rb +17 -0
- data/app/graphql/has_helpers/types/layouts/app_nav/link_section_type.rb +17 -0
- data/app/graphql/has_helpers/types/layouts/app_nav/link_type.rb +20 -0
- data/app/graphql/has_helpers/types/layouts/app_nav/sub_link_type.rb +17 -0
- data/app/graphql/has_helpers/types/layouts/app_nav/type.rb +16 -0
- data/app/graphql/has_helpers/types/lookup.rb +16 -0
- data/app/graphql/has_helpers/types/resource_tab/type.rb +15 -0
- data/app/graphql/has_helpers/types/role_type.rb +13 -0
- data/app/graphql/has_helpers/types/search/detail_badge_type.rb +15 -0
- data/app/graphql/has_helpers/types/search/detail_item_type.rb +14 -0
- data/app/graphql/has_helpers/types/search/detail_type.rb +16 -0
- data/app/graphql/has_helpers/types/search/filter_operation_enum.rb +19 -0
- data/app/graphql/has_helpers/types/search/input_type.rb +16 -0
- data/app/graphql/has_helpers/types/search/result_type.rb +72 -0
- data/app/graphql/has_helpers/types/search/search_filter_input_type.rb +17 -0
- data/app/graphql/has_helpers/types/search/type.rb +17 -0
- data/app/importers/has_helpers/cards/importer.rb +1223 -0
- data/app/lib/has_helpers/application_helper.rb +21 -0
- data/app/lib/has_helpers/dn/processor.rb +55 -0
- data/app/lib/has_helpers/dn/refresh.rb +13 -0
- data/app/lib/has_helpers/dn/utils/backfill_utils.rb +80 -0
- data/app/lib/has_helpers/dn/utils/query_utils.rb +216 -0
- data/app/lib/has_helpers/feature.rb +88 -0
- data/app/lib/has_helpers/hq_exception.rb +18 -0
- data/app/lib/has_helpers/index.rb +890 -0
- data/app/lib/has_helpers/organization_merge.rb +128 -0
- data/app/lib/has_helpers/query/elasticsearch.rb +20 -0
- data/app/lib/has_helpers/query/provider.rb +105 -0
- data/app/lib/has_helpers/query/sql.rb +133 -0
- data/app/lib/has_helpers/query.rb +14 -0
- data/app/lib/has_helpers/search/elasticsearch.rb +41 -0
- data/app/lib/has_helpers/search/elasticsearch_shared.rb +70 -0
- data/app/lib/has_helpers/search/provider.rb +149 -0
- data/app/lib/has_helpers/search/search_utils.rb +74 -0
- data/app/lib/has_helpers/search/sql_search.rb +219 -0
- data/app/lib/has_helpers/search.rb +46 -0
- data/app/models/ability.rb +233 -0
- data/app/models/has_helpers/application.rb +47 -0
- data/app/models/has_helpers/application_record.rb +7 -0
- data/app/models/has_helpers/cards/card_builder/base.rb +74 -0
- data/app/models/has_helpers/cards/card_builder/card_definition_builder.rb +486 -0
- data/app/models/has_helpers/cards/card_builder/definition_context.rb +82 -0
- data/app/models/has_helpers/cards/card_builder/field_builder.rb +142 -0
- data/app/models/has_helpers/cards/card_builder/resource_builder.rb +34 -0
- data/app/models/has_helpers/cards/card_definition.rb +41 -0
- data/app/models/has_helpers/cards/card_definition_resource.rb +19 -0
- data/app/models/has_helpers/cards/card_field.rb +39 -0
- data/app/models/has_helpers/cards/registry.rb +115 -0
- data/app/models/has_helpers/cards/role_card_field.rb +28 -0
- data/app/models/has_helpers/cards/role_resource_card.rb +41 -0
- data/app/models/has_helpers/dn_utils.rb +61 -0
- data/app/models/has_helpers/favorite.rb +20 -0
- data/app/models/has_helpers/master_record.rb +18 -0
- data/app/models/has_helpers/notification_alert.rb +52 -0
- data/app/models/has_helpers/organization.rb +16 -0
- data/app/models/has_helpers/organization_link.rb +92 -0
- data/app/models/has_helpers/organization_methods.rb +62 -0
- data/app/models/has_helpers/organization_option.rb +31 -0
- data/app/models/has_helpers/organization_resource.rb +45 -0
- data/app/models/has_helpers/pending_db_operation.rb +31 -0
- data/app/models/has_helpers/recently_viewed.rb +34 -0
- data/app/models/has_helpers/reindex_queue.rb +43 -0
- data/app/models/has_helpers/resource.rb +38 -0
- data/app/models/has_helpers/resource_group.rb +15 -0
- data/app/models/has_helpers/restriction.rb +15 -0
- data/app/models/has_helpers/restriction_operation.rb +13 -0
- data/app/models/has_helpers/role.rb +20 -0
- data/app/models/has_helpers/service_trust/audit_log.rb +20 -0
- data/app/models/has_helpers/service_trust/key.rb +56 -0
- data/app/models/has_helpers/stat.rb +13 -0
- data/app/models/has_helpers/user.rb +18 -0
- data/app/models/has_helpers/user_methods.rb +184 -0
- data/app/models/has_helpers/user_organization.rb +33 -0
- data/app/models/has_helpers/user_role.rb +27 -0
- data/app/models/has_helpers/user_session.rb +19 -0
- data/app/presenters/has_helpers/barcode_base_presenter.rb +113 -0
- data/app/presenters/has_helpers/base_presenter.rb +100 -0
- data/app/presenters/has_helpers/cacheable_item.rb +28 -0
- data/app/presenters/has_helpers/item_presenter.rb +52 -0
- data/app/presenters/has_helpers/items_presenter.rb +33 -0
- data/app/presenters/has_helpers/resource/action_presenter.rb +8 -0
- data/app/presenters/has_helpers/resource/cell_presenter.rb +52 -0
- data/app/presenters/has_helpers/resource/email_presenter.rb +34 -0
- data/app/presenters/has_helpers/resource/form_presenter.rb +76 -0
- data/app/presenters/has_helpers/resource/info_collection_presenter.rb +33 -0
- data/app/presenters/has_helpers/resource/info_presenter.rb +34 -0
- data/app/presenters/has_helpers/resource/list_presenter.rb +120 -0
- data/app/presenters/has_helpers/resource/note_presenter.rb +14 -0
- data/app/presenters/has_helpers/resource/row_presenter.rb +25 -0
- data/app/presenters/has_helpers/resource/tag_presenter.rb +31 -0
- data/app/presenters/has_helpers/search_presenter.rb +230 -0
- data/app/triggers/has_helpers/erase_denormalized_belongs_to_column_trigger.rb +85 -0
- data/app/triggers/has_helpers/limit_favorites.rb +97 -0
- data/app/utils/has_helpers/redis_configurable.rb +79 -0
- data/app/utils/has_helpers/sync_utils.rb +234 -0
- data/app/views/has_helpers/actions/_action.html.erb +31 -0
- data/app/views/has_helpers/alerts/_alert.html.erb +3 -0
- data/app/views/has_helpers/barcodes.html.erb +36 -0
- data/app/views/has_helpers/containers/_container.html.erb +3 -0
- data/app/views/has_helpers/email_notifier/system.html.erb +8 -0
- data/app/views/has_helpers/email_notifier/system.text.erb +7 -0
- data/app/views/has_helpers/enumerables/_enumerable.html.erb +1 -0
- data/app/views/has_helpers/icons/_icon.html.erb +1 -0
- data/app/views/has_helpers/render_objects/_render_object.html.erb +1 -0
- data/app/views/has_helpers/services/_service.html.erb +1 -0
- data/app/views/has_helpers/text_nodes/_text_node.html.erb +4 -0
- data/app/views/has_helpers/ui_attributes/_html_attrs.html.erb +1 -0
- data/app/workers/elasticsearch_process_queue.rb +11 -0
- data/app/workers/elasticsearch_reindex.rb +11 -0
- data/app/workers/elasticsearch_reindex_scheduler.rb +35 -0
- data/app/workers/good_job/configurable_backfill.rb +64 -0
- data/app/workers/good_job/cron_denormalization_stats_manager.rb +39 -0
- data/app/workers/good_job/denormalization_stats_worker.rb +57 -0
- data/app/workers/good_job/dn_update_worker.rb +30 -0
- data/app/workers/good_job/dn_worker.rb +47 -0
- data/app/workers/good_job/elasticsearch_old_index_cleaner_worker.rb +113 -0
- data/app/workers/good_job/elasticsearch_on_finish_reindex.rb +26 -0
- data/app/workers/good_job/elasticsearch_process_batch.rb +78 -0
- data/app/workers/good_job/elasticsearch_process_change_logs_master_worker.rb +27 -0
- data/app/workers/good_job/elasticsearch_process_change_logs_worker.rb +155 -0
- data/app/workers/good_job/elasticsearch_process_queue.rb +50 -0
- data/app/workers/good_job/elasticsearch_reindex.rb +31 -0
- data/app/workers/has_helpers/cards/orphan_card_sweeper.rb +28 -0
- data/app/workers/has_helpers/db_operations/clean_stats_worker.rb +42 -0
- data/app/workers/has_helpers/db_operations/helper_worker.rb +131 -0
- data/app/workers/has_helpers/db_operations/main_worker.rb +164 -0
- data/app/workers/has_helpers/db_operations/operations.rb +134 -0
- data/app/workers/has_helpers/db_operations/update_operations_stats_resume_worker.rb +35 -0
- data/app/workers/has_helpers/default_worker.rb +17 -0
- data/app/workers/has_helpers/good_job/notification_alert_cleanup_worker.rb +43 -0
- data/app/workers/has_helpers/service_trust/audit_log_cleanup_worker.rb +19 -0
- data/app/workers/sidekiq/elasticsearch_old_index_cleaner_worker.rb +112 -0
- data/app/workers/sidekiq/elasticsearch_process_batch.rb +87 -0
- data/app/workers/sidekiq/elasticsearch_process_queue.rb +40 -0
- data/app/workers/sidekiq/elasticsearch_reindex.rb +53 -0
- data/config/initializers/action_view.rb +7 -0
- data/config/initializers/inflections.rb +24 -0
- data/config/initializers/marginalia.rb +8 -0
- data/config/initializers/mime_types.rb +6 -0
- data/config/initializers/opentelemetry.rb +12 -0
- data/config/initializers/redis_conf.rb +40 -0
- data/config/initializers/searchkick.rb +9 -0
- data/config/initializers/will_paginate.rb +39 -0
- data/config/redis.yml +12 -0
- data/config/routes.rb +27 -0
- data/config/service_trust_routes.rb +5 -0
- data/config.ru +9 -0
- data/db/migrate/20230127194725_create_organization_table.rb +25 -0
- data/db/migrate/20230127194726_validate_organization_foreign_keys.rb +7 -0
- data/db/migrate/20230206141413_create_security_models.rb +42 -0
- data/db/migrate/20230206141513_create_restriction_model.rb +20 -0
- data/db/migrate/20230206142613_add_security_indexes.rb +14 -0
- data/db/migrate/20230206143513_security_model_constants_seed.rb +8 -0
- data/db/migrate/20230206161513_add_organization_to_security_models.rb +13 -0
- data/db/migrate/20230209121019_create_search_group_models.rb +36 -0
- data/db/migrate/20230209122919_add_search_group_indexes.rb +21 -0
- data/db/migrate/20230209181832_add_search_group_resources_type_constraint_trigger.rb +61 -0
- data/db/migrate/20230313220337_create_table_user_sessions.rb +31 -0
- data/db/migrate/20230517141106_update_resource_types.rb +12 -0
- data/db/migrate/20230518194611_add_resources_contraints.rb +17 -0
- data/db/migrate/20230518194642_create_nested_resource_model.rb +21 -0
- data/db/migrate/20230518194645_validate_security_models_constraints.rb +23 -0
- data/db/migrate/20230608125037_add_inactive_field_to_user_sessions.rb +7 -0
- data/db/migrate/20230919191525_add_parent_id_to_resources.rb +8 -0
- data/db/migrate/20230919191535_validate_parent_id_fk_resources.rb +7 -0
- data/db/migrate/20230919210456_drop_nested_resources.rb +7 -0
- data/db/migrate/20231222135620_add_user_role.rb +19 -0
- data/db/migrate/20240108172021_create_favorites_table.rb +20 -0
- data/db/migrate/20240108190209_create_organization_link_table_if_doesnt_exists.rb +31 -0
- data/db/migrate/20240109211445_validate_fks_org_links.rb +9 -0
- data/db/migrate/20240110135440_create_recently_vieweds.rb +20 -0
- data/db/migrate/20240112185312_link_type_options_seed.rb +7 -0
- data/db/migrate/20240116173201_organization_link_resource_tab_constraints.rb +19 -0
- data/db/migrate/20240131160949_add_user_organizations_table.rb +32 -0
- data/db/migrate/20240212132519_add_organization_in_user_sessions.rb +13 -0
- data/db/migrate/20240227135052_add_user_organization_composite_fks.rb +32 -0
- data/db/migrate/20240312193501_update_organization_links.rb +11 -0
- data/db/migrate/20240426185902_add_user_organizations_current_organization_column.rb +12 -0
- data/db/migrate/20240429214601_add_primary_trigger.rb +10 -0
- data/db/migrate/20241112131819_add_json_file_format_seed.rb +7 -0
- data/db/migrate/20250520133417_create_restriction_operations.rb +18 -0
- data/db/migrate/20250520133420_validate_create_restriction_operations.rb +8 -0
- data/db/migrate/20250520140642_add_indexes_to_restrictions.rb +9 -0
- data/db/migrate/20250529130741_create_notification_alerts_table.rb +28 -0
- data/db/migrate/20250530223332_create_data_migrations_table.rb +19 -0
- data/db/migrate/20250605121622_seed_notification_alert_status.rb +7 -0
- data/db/migrate/20250605131439_seed_notification_alert_types.rb +7 -0
- data/db/migrate/20250606173128_update_notification_alert_length_constraint.rb +18 -0
- data/db/migrate/20250606174043_validate_notification_alert_length_constraints.rb +8 -0
- data/db/migrate/20250624183000_add_data_import_alert_type.rb +11 -0
- data/db/migrate/20250722183935_create_organization_options_table.rb +38 -0
- data/db/migrate/20250722192724_validate_organization_options_fks_and_constraints.rb +32 -0
- data/db/migrate/20250722192936_add_indexes_to_organization_options_table.rb +15 -0
- data/db/migrate/20250915132448_add_column_restrictions_hash.rb +7 -0
- data/db/migrate/20250917143211_add_column_permissions_hash.rb +7 -0
- data/db/migrate/20251001145950_add_trust_this_device.rb +7 -0
- data/db/migrate/20251021130835_add_backend_urls_to_applications_table.rb +13 -0
- data/db/migrate/20251021172715_validate_backend_urls_constraints_in_applications.rb +9 -0
- data/db/migrate/20251030200020_create_card_definitions.rb +23 -0
- data/db/migrate/20251030201012_create_card_definition_resources.rb +24 -0
- data/db/migrate/20251030201236_create_card_fields.rb +34 -0
- data/db/migrate/20251103194317_create_role_resource_cards.rb +37 -0
- data/db/migrate/20251103194426_create_role_card_fields.rb +34 -0
- data/db/migrate/20251107173218_validate_columns_from_cards.rb +17 -0
- data/db/migrate/20251111173212_add_column_application_array.rb +7 -0
- data/db/migrate/20251112203326_add_card_types.rb +10 -0
- data/db/migrate/20251115120000_add_virtual_cards_support.rb +28 -0
- data/db/migrate/20251202231256_add_org_scoped_fk_role_resource_cards_to_roles.rb +14 -0
- data/db/migrate/20260105134504_add_accessor_field_resource_type.rb +7 -0
- data/db/migrate/20260115123000_add_default_visibility_to_card_definitions.rb +9 -0
- data/db/migrate/20260120175037_is_new_admin.rb +9 -0
- data/db/migrate/20260122392008_add_stats_table.rb +17 -0
- data/db/migrate/20260414204718_add_index_to_stats_table.rb +10 -0
- data/db/migrate/20260509120000_create_service_trust_keys.rb +25 -0
- data/db/migrate/20260509120001_create_service_trust_audit_logs.rb +26 -0
- data/db/migrate/20260511120000_add_direction_to_service_trust_audit_logs.rb +17 -0
- data/db/migrate/20260511121000_remove_role_fk_from_role_resource_cards.rb +19 -0
- data/db/migrate/20260511121001_add_disabled_at_to_role_resource_cards.rb +10 -0
- data/db/migrate/20260605120100_seed_hover_card_options.rb +8 -0
- data/db/migrate/20260608120000_seed_badge_list_card_data_type.rb +7 -0
- data/db/migrate/20260727000000_add_two_factor_trust_id_to_user_sessions.rb +11 -0
- data/db/seeds.rb +14 -0
- data/docs/Elasticsearch.md +23 -0
- data/docs/Search_API.md +126 -0
- data/docs/cards_role_proxy.md +107 -0
- data/docs/factory_api.md +135 -0
- data/has-helpers-pr-289-pipeline.yaml +16 -0
- data/has-helpers-pr-298-pipeline.yaml +15 -0
- data/has_helpers.gemspec +68 -0
- data/lib/active_record/migration/active_migration_patch.rb +123 -0
- data/lib/generators/has_helpers/install/USAGE +8 -0
- data/lib/generators/has_helpers/install/install_generator.rb +22 -0
- data/lib/generators/has_helpers/install/templates/migrate/has_helpers_initial_setup.rb +26 -0
- data/lib/has_helpers/active_record/base.rb +50 -0
- data/lib/has_helpers/active_record/date_range.rb +228 -0
- data/lib/has_helpers/active_record/nested_resource.rb +58 -0
- data/lib/has_helpers/active_record/validation.rb +63 -0
- data/lib/has_helpers/active_record.rb +520 -0
- data/lib/has_helpers/app/command.rb +107 -0
- data/lib/has_helpers/app.rb +10 -0
- data/lib/has_helpers/assets_precompile_helper.rb +20 -0
- data/lib/has_helpers/attributes.rb +196 -0
- data/lib/has_helpers/aws.rb +67 -0
- data/lib/has_helpers/cards/cache.rb +49 -0
- data/lib/has_helpers/cards/config.rb +42 -0
- data/lib/has_helpers/cards/remote_role.rb +27 -0
- data/lib/has_helpers/cards/remote_roles.rb +75 -0
- data/lib/has_helpers/cards/roles.rb +55 -0
- data/lib/has_helpers/cards.rb +34 -0
- data/lib/has_helpers/concerns/hydrate_concern.rb +76 -0
- data/lib/has_helpers/concerns/importable_accessors.rb +25 -0
- data/lib/has_helpers/concerns/restrictions_parseable.rb +51 -0
- data/lib/has_helpers/config.rb +11 -0
- data/lib/has_helpers/constant.rb +578 -0
- data/lib/has_helpers/constraints.rb +10 -0
- data/lib/has_helpers/core_ext/array.rb +28 -0
- data/lib/has_helpers/core_ext/date/infinity.rb +25 -0
- data/lib/has_helpers/core_ext/date.rb +39 -0
- data/lib/has_helpers/core_ext/enumerable.rb +20 -0
- data/lib/has_helpers/core_ext/false_class.rb +14 -0
- data/lib/has_helpers/core_ext/float.rb +18 -0
- data/lib/has_helpers/core_ext/hash.rb +71 -0
- data/lib/has_helpers/core_ext/integer.rb +18 -0
- data/lib/has_helpers/core_ext/nil_class.rb +30 -0
- data/lib/has_helpers/core_ext/object.rb +60 -0
- data/lib/has_helpers/core_ext/proc.rb +16 -0
- data/lib/has_helpers/core_ext/string.rb +81 -0
- data/lib/has_helpers/core_ext/symbol.rb +17 -0
- data/lib/has_helpers/core_ext/true_class.rb +14 -0
- data/lib/has_helpers/cursor.rb +46 -0
- data/lib/has_helpers/data_migration/base.rb +293 -0
- data/lib/has_helpers/data_migration/large_worker.rb +20 -0
- data/lib/has_helpers/data_migration/master_worker.rb +33 -0
- data/lib/has_helpers/data_migration/migrator.rb +83 -0
- data/lib/has_helpers/data_migration/model.rb +18 -0
- data/lib/has_helpers/data_migration/small_worker.rb +20 -0
- data/lib/has_helpers/data_migration.rb +8 -0
- data/lib/has_helpers/engine.rb +47 -0
- data/lib/has_helpers/extensions.rb +17 -0
- data/lib/has_helpers/factory_api.rb +47 -0
- data/lib/has_helpers/factory_scenario/definition.rb +33 -0
- data/lib/has_helpers/factory_scenario/dsl.rb +80 -0
- data/lib/has_helpers/factory_scenario/factory_bot_override.rb +53 -0
- data/lib/has_helpers/factory_scenario/registry.rb +31 -0
- data/lib/has_helpers/factory_scenario/runner.rb +285 -0
- data/lib/has_helpers/factory_scenario.rb +27 -0
- data/lib/has_helpers/fixed_expiration_time_cache_session_store.rb +20 -0
- data/lib/has_helpers/flattener/association.rb +140 -0
- data/lib/has_helpers/flattener/attribute.rb +42 -0
- data/lib/has_helpers/flattener/base.rb +428 -0
- data/lib/has_helpers/flattener/column/types.rb +71 -0
- data/lib/has_helpers/flattener/column.rb +97 -0
- data/lib/has_helpers/flattener/pivot.rb +49 -0
- data/lib/has_helpers/flattener/reset.rb +33 -0
- data/lib/has_helpers/flattener/resource.rb +114 -0
- data/lib/has_helpers/flattener/resource_generator.rb +21 -0
- data/lib/has_helpers/flattener/utils.rb +14 -0
- data/lib/has_helpers/flattener.rb +53 -0
- data/lib/has_helpers/hash_accessors.rb +62 -0
- data/lib/has_helpers/helpers/application.rb +291 -0
- data/lib/has_helpers/index/config.rb +15 -0
- data/lib/has_helpers/lazy_config.rb +61 -0
- data/lib/has_helpers/resource_importer.rb +260 -0
- data/lib/has_helpers/resources.rb +127 -0
- data/lib/has_helpers/rspec/constants.rb +61 -0
- data/lib/has_helpers/rspec/factories.rb +11 -0
- data/lib/has_helpers/rspec.rb +22 -0
- data/lib/has_helpers/sentry/graphql_trace.rb +37 -0
- data/lib/has_helpers/serializer.rb +41 -0
- data/lib/has_helpers/service_trust/audit.rb +45 -0
- data/lib/has_helpers/service_trust/authorization.rb +34 -0
- data/lib/has_helpers/service_trust/bearer_token.rb +23 -0
- data/lib/has_helpers/service_trust/capability.rb +32 -0
- data/lib/has_helpers/service_trust/client/response.rb +13 -0
- data/lib/has_helpers/service_trust/client/retry.rb +35 -0
- data/lib/has_helpers/service_trust/client.rb +128 -0
- data/lib/has_helpers/service_trust/configuration.rb +76 -0
- data/lib/has_helpers/service_trust/engine.rb +10 -0
- data/lib/has_helpers/service_trust/errors.rb +32 -0
- data/lib/has_helpers/service_trust/graphql/authorizable.rb +72 -0
- data/lib/has_helpers/service_trust/graphql_client.rb +54 -0
- data/lib/has_helpers/service_trust/jwks_client.rb +133 -0
- data/lib/has_helpers/service_trust/jwt_codec.rb +45 -0
- data/lib/has_helpers/service_trust/key_manager.rb +160 -0
- data/lib/has_helpers/service_trust/rate_limiter.rb +27 -0
- data/lib/has_helpers/service_trust/replay_cache.rb +26 -0
- data/lib/has_helpers/service_trust/signer.rb +47 -0
- data/lib/has_helpers/service_trust/test.rb +60 -0
- data/lib/has_helpers/service_trust/verifier.rb +85 -0
- data/lib/has_helpers/service_trust/verify_middleware.rb +55 -0
- data/lib/has_helpers/service_trust.rb +51 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_addresses.rb +42 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_advisors.rb +272 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_emails.rb +37 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_organizations.rb +48 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_people.rb +34 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_phones.rb +41 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_states.rb +26 -0
- data/lib/has_helpers/test_helpers/factory_data/unit_webs.rb +31 -0
- data/lib/has_helpers/test_helpers/shoulda_helpers.rb +249 -0
- data/lib/has_helpers/ui_attributes.rb +39 -0
- data/lib/has_helpers/utils/action.rb +87 -0
- data/lib/has_helpers/utils/active_record_utils.rb +40 -0
- data/lib/has_helpers/utils/alert.rb +42 -0
- data/lib/has_helpers/utils/container.rb +46 -0
- data/lib/has_helpers/utils/dataset.rb +34 -0
- data/lib/has_helpers/utils/email_notifier.rb +15 -0
- data/lib/has_helpers/utils/icon.rb +66 -0
- data/lib/has_helpers/utils/input.rb +45 -0
- data/lib/has_helpers/utils/log_notifier.rb +79 -0
- data/lib/has_helpers/utils/render_object.rb +32 -0
- data/lib/has_helpers/utils/s3_utils.rb +37 -0
- data/lib/has_helpers/utils/service.rb +87 -0
- data/lib/has_helpers/utils/slack_conf.rb +31 -0
- data/lib/has_helpers/utils/slack_notifier.rb +42 -0
- data/lib/has_helpers/utils/text_node.rb +34 -0
- data/lib/has_helpers/utils/user_restrictions.rb +266 -0
- data/lib/has_helpers/utils.rb +19 -0
- data/lib/has_helpers/version.rb +6 -0
- data/lib/has_helpers/web_socket_publisher.rb +20 -0
- data/lib/has_helpers/webhook.rb +17 -0
- data/lib/has_helpers.rb +63 -0
- data/lib/hq/resource.rb +55 -0
- data/lib/hq/structure/inserts.rb +176 -0
- data/lib/hq/structure.rb +115 -0
- data/lib/tasks/has_helpers/service_trust.rake +39 -0
- data/script/rails +10 -0
- data/sorbet/config +5 -0
- data/sorbet/rbi/gems/actionpack.rbi +1361 -0
- data/sorbet/rbi/gems/activemodel.rbi +755 -0
- data/sorbet/rbi/gems/activerecord.rbi +4713 -0
- data/sorbet/rbi/gems/activesupport.rbi +1970 -0
- data/sorbet/rbi/gems/barby.rbi +129 -0
- data/sorbet/rbi/gems/better_errors.rbi +155 -0
- data/sorbet/rbi/gems/binding_of_caller.rbi +22 -0
- data/sorbet/rbi/gems/byebug.rbi +19 -0
- data/sorbet/rbi/gems/chronic.rbi +480 -0
- data/sorbet/rbi/gems/chunky_png.rbi +510 -0
- data/sorbet/rbi/gems/climate_control.rbi +45 -0
- data/sorbet/rbi/gems/coderay.rbi +91 -0
- data/sorbet/rbi/gems/concurrent-ruby.rbi +1587 -0
- data/sorbet/rbi/gems/debug_inspector.rbi +18 -0
- data/sorbet/rbi/gems/erubi.rbi +26 -0
- data/sorbet/rbi/gems/holidays.rbi +362 -0
- data/sorbet/rbi/gems/i18n.rbi +132 -0
- data/sorbet/rbi/gems/method_source.rbi +63 -0
- data/sorbet/rbi/gems/mime-types-data.rbi +16 -0
- data/sorbet/rbi/gems/mime-types.rbi +217 -0
- data/sorbet/rbi/gems/mimemagic.rbi +36 -0
- data/sorbet/rbi/gems/minitest.rbi +282 -0
- data/sorbet/rbi/gems/mustache.rbi +151 -0
- data/sorbet/rbi/gems/paperclip.rbi +644 -0
- data/sorbet/rbi/gems/parallel.rbi +81 -0
- data/sorbet/rbi/gems/parallel_tests.rbi +27 -0
- data/sorbet/rbi/gems/pry-rails.rbi +71 -0
- data/sorbet/rbi/gems/pry.rbi +1964 -0
- data/sorbet/rbi/gems/rack.rbi +228 -0
- data/sorbet/rbi/gems/railties.rbi +364 -0
- data/sorbet/rbi/gems/rspec-core.rbi +1704 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +389 -0
- data/sorbet/rbi/gems/rspec-support.rbi +206 -0
- data/sorbet/rbi/gems/rspec.rbi +14 -0
- data/sorbet/rbi/gems/spreadsheet_architect.rbi +10 -0
- data/sorbet/rbi/gems/sprockets.rbi +659 -0
- data/sorbet/rbi/gems/terrapin.rbi +115 -0
- data/sorbet/rbi/gems/testhq.rbi +14 -0
- data/sorbet/rbi/gems/thor.rbi +482 -0
- data/sorbet/rbi/gems/thread_safe.rbi +81 -0
- data/sorbet/rbi/gems/tzinfo.rbi +407 -0
- data/sorbet/rbi/gems/zeitwerk.rbi +131 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +35111 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +59974 -0
- data/sorbet/rbi/sorbet-typed/lib/actionmailer/all/actionmailer.rbi +13 -0
- data/sorbet/rbi/sorbet-typed/lib/actionpack/all/actionpack.rbi +855 -0
- data/sorbet/rbi/sorbet-typed/lib/actionview/all/actionview.rbi +314 -0
- data/sorbet/rbi/sorbet-typed/lib/activemodel/all/activemodel.rbi +423 -0
- data/sorbet/rbi/sorbet-typed/lib/activerecord/>=5.2/activerecord.rbi +17 -0
- data/sorbet/rbi/sorbet-typed/lib/activerecord/all/activerecord.rbi +1415 -0
- data/sorbet/rbi/sorbet-typed/lib/activerecord/~>6.0.0/activerecord.rbi +475 -0
- data/sorbet/rbi/sorbet-typed/lib/activestorage/>=6.0.0/activestorage.rbi +19 -0
- data/sorbet/rbi/sorbet-typed/lib/activestorage/all/activestorage.rbi +173 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6.0.0.rc1/activesupport.rbi +23 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +625 -0
- data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +8684 -0
- data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +99 -0
- data/sorbet/rbi/sorbet-typed/lib/railties/all/railties.rbi +25 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +254 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/gem.rbi +4222 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +111 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +543 -0
- data/sorbet/rbi/todo.rbi +244 -0
- data/spec/commands/cards/available_cards_command_spec.rb +90 -0
- data/spec/commands/cards/card_data_command_spec.rb +1048 -0
- data/spec/constructors/has_helpers/base_constructor_spec.rb +49 -0
- data/spec/controllers/has_helpers/controller_spec.rb +71 -0
- data/spec/controllers/has_helpers/xlsx_export_spec.rb +96 -0
- data/spec/factories/advisors.rb +20 -0
- data/spec/factories/anonymous_cowards.rb +5 -0
- data/spec/factories/applications.rb +5 -0
- data/spec/factories/appointments.rb +6 -0
- data/spec/factories/attachment.rb +15 -0
- data/spec/factories/audits.rb +5 -0
- data/spec/factories/basic_schedules.rb +12 -0
- data/spec/factories/cards/card_definition_resources.rb +7 -0
- data/spec/factories/cards/card_definitions.rb +11 -0
- data/spec/factories/cards/card_fields.rb +22 -0
- data/spec/factories/cards/role_card_fields.rb +17 -0
- data/spec/factories/cards/role_resource_cards.rb +12 -0
- data/spec/factories/constant_definition.rb +7 -0
- data/spec/factories/contracts.rb +10 -0
- data/spec/factories/data_migration_models.rb +9 -0
- data/spec/factories/demographics.rb +22 -0
- data/spec/factories/events.rb +21 -0
- data/spec/factories/firms.rb +21 -0
- data/spec/factories/imports.rb +9 -0
- data/spec/factories/meetups.rb +13 -0
- data/spec/factories/organizations.rb +13 -0
- data/spec/factories/policies.rb +7 -0
- data/spec/factories/policy_notes.rb +9 -0
- data/spec/factories/read_only_values.rb +7 -0
- data/spec/factories/resources.rb +7 -0
- data/spec/factories/roles.rb +11 -0
- data/spec/factories/schedules.rb +13 -0
- data/spec/factories/shared/flattener/attributes.rb +10 -0
- data/spec/factories/shared/flattener/columns.rb +6 -0
- data/spec/factories/shared/flattener/resources.rb +5 -0
- data/spec/factories/templates.rb +12 -0
- data/spec/factories/user_organizations.rb +23 -0
- data/spec/factories/user_session.rb +11 -0
- data/spec/factories/users.rb +55 -0
- data/spec/fixtures/sample_config.yml +3 -0
- data/spec/graphql/has_helpers/mutations/external_data_sync_spec.rb +101 -0
- data/spec/graphql/has_helpers/resolvers/cards/available_cards_spec.rb +36 -0
- data/spec/graphql/has_helpers/resolvers/cards/card_data_spec.rb +42 -0
- data/spec/graphql/has_helpers/resolvers/cards/hover_card_data_spec.rb +59 -0
- data/spec/has_helpers/cards/cache_spec.rb +58 -0
- data/spec/has_helpers/cards/config_spec.rb +68 -0
- data/spec/has_helpers/cards/orphan_card_sweeper_spec.rb +97 -0
- data/spec/has_helpers/cards/remote_role_spec.rb +17 -0
- data/spec/has_helpers/cards/remote_roles_spec.rb +97 -0
- data/spec/has_helpers/cards/roles_spec.rb +52 -0
- data/spec/has_helpers/cards/smoke_spec.rb +168 -0
- data/spec/has_helpers/cards/support.rb +52 -0
- data/spec/has_helpers/resolvers/internal/roles_spec.rb +73 -0
- data/spec/has_helpers/service_trust/audit_log_cleanup_worker_spec.rb +33 -0
- data/spec/has_helpers/service_trust/audit_spec.rb +70 -0
- data/spec/has_helpers/service_trust/authenticated_as_service_spec.rb +65 -0
- data/spec/has_helpers/service_trust/authorization_spec.rb +37 -0
- data/spec/has_helpers/service_trust/capability_spec.rb +35 -0
- data/spec/has_helpers/service_trust/client_spec.rb +129 -0
- data/spec/has_helpers/service_trust/configuration_spec.rb +35 -0
- data/spec/has_helpers/service_trust/engine_spec.rb +13 -0
- data/spec/has_helpers/service_trust/errors_spec.rb +23 -0
- data/spec/has_helpers/service_trust/graphql_authentication_spec.rb +59 -0
- data/spec/has_helpers/service_trust/graphql_authorizable_spec.rb +91 -0
- data/spec/has_helpers/service_trust/graphql_client_spec.rb +47 -0
- data/spec/has_helpers/service_trust/jwks_client_spec.rb +85 -0
- data/spec/has_helpers/service_trust/jwks_controller_spec.rb +47 -0
- data/spec/has_helpers/service_trust/jwt_codec_spec.rb +30 -0
- data/spec/has_helpers/service_trust/key_manager_spec.rb +80 -0
- data/spec/has_helpers/service_trust/key_spec.rb +52 -0
- data/spec/has_helpers/service_trust/rake_tasks_spec.rb +58 -0
- data/spec/has_helpers/service_trust/rate_limiter_spec.rb +31 -0
- data/spec/has_helpers/service_trust/replay_cache_spec.rb +29 -0
- data/spec/has_helpers/service_trust/signer_verifier_spec.rb +159 -0
- data/spec/has_helpers/service_trust/smoke_spec.rb +216 -0
- data/spec/has_helpers/service_trust/support.rb +121 -0
- data/spec/has_helpers/service_trust/test_module_spec.rb +39 -0
- data/spec/has_helpers/service_trust/verify_middleware_spec.rb +82 -0
- data/spec/helpers/application_spec.rb +155 -0
- data/spec/importers/cards/importer_spec.rb +861 -0
- data/spec/internal/app/assets/config/manifest.js +0 -0
- data/spec/internal/app/constants/cat_egory.rb +33 -0
- data/spec/internal/app/constants/color.rb +19 -0
- data/spec/internal/app/constants/exclude_group_nil_constant.rb +5 -0
- data/spec/internal/app/constants/foo.rb +6 -0
- data/spec/internal/app/constants/fruit.rb +3 -0
- data/spec/internal/app/constants/generic_group_example.rb +12 -0
- data/spec/internal/app/constants/my_constant.rb +4 -0
- data/spec/internal/app/constants/product.rb +3 -0
- data/spec/internal/app/constants/shirt_size.rb +18 -0
- data/spec/internal/app/constants/simple_option.rb +14 -0
- data/spec/internal/app/constants/status.rb +5 -0
- data/spec/internal/app/constructors/organization_constructor.rb +4 -0
- data/spec/internal/app/controllers/application_controller.rb +27 -0
- data/spec/internal/app/controllers/search_extensions.rb +3 -0
- data/spec/internal/app/indices/advisor_index.rb +35 -0
- data/spec/internal/app/indices/contract_index.rb +52 -0
- data/spec/internal/app/indices/firm_index.rb +16 -0
- data/spec/internal/app/indices/policy_index.rb +20 -0
- data/spec/internal/app/models/advisor.rb +24 -0
- data/spec/internal/app/models/agency.rb +3 -0
- data/spec/internal/app/models/anonymous_coward.rb +5 -0
- data/spec/internal/app/models/another_event.rb +2 -0
- data/spec/internal/app/models/appointment.rb +3 -0
- data/spec/internal/app/models/attachment.rb +10 -0
- data/spec/internal/app/models/basic_schedule.rb +6 -0
- data/spec/internal/app/models/beneficiary.rb +6 -0
- data/spec/internal/app/models/client.rb +5 -0
- data/spec/internal/app/models/contract.rb +9 -0
- data/spec/internal/app/models/demographic.rb +4 -0
- data/spec/internal/app/models/division.rb +3 -0
- data/spec/internal/app/models/event.rb +10 -0
- data/spec/internal/app/models/event_template.rb +1 -0
- data/spec/internal/app/models/firm.rb +18 -0
- data/spec/internal/app/models/has_audits/audit.rb +15 -0
- data/spec/internal/app/models/import.rb +7 -0
- data/spec/internal/app/models/meetup.rb +8 -0
- data/spec/internal/app/models/organization.rb +17 -0
- data/spec/internal/app/models/organization_event.rb +5 -0
- data/spec/internal/app/models/policy.rb +16 -0
- data/spec/internal/app/models/policy_holder.rb +7 -0
- data/spec/internal/app/models/policy_note.rb +8 -0
- data/spec/internal/app/models/project.rb +3 -0
- data/spec/internal/app/models/read_only_value.rb +8 -0
- data/spec/internal/app/models/schedule.rb +12 -0
- data/spec/internal/app/models/template.rb +9 -0
- data/spec/internal/app/models/user.rb +31 -0
- data/spec/internal/app/models/user_organization.rb +17 -0
- data/spec/internal/app/views/has_lists/lists/_table.html.erb +1 -0
- data/spec/internal/config/database-standard.yml +24 -0
- data/spec/internal/config/initializers/resources.rb +19 -0
- data/spec/internal/config/routes.rb +19 -0
- data/spec/internal/config/storage.yml +3 -0
- data/spec/internal/db/good_job_schema.rb +141 -0
- data/spec/internal/db/schema.rb +334 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/flattener/base_spec.rb +93 -0
- data/spec/lib/flattener/column/types_spec.rb +65 -0
- data/spec/lib/has_helpers/active_record/base_spec.rb +53 -0
- data/spec/lib/has_helpers/active_record/date_range_spec.rb +498 -0
- data/spec/lib/has_helpers/active_record/lookup_model_spec.rb +408 -0
- data/spec/lib/has_helpers/active_record/nested_resource_spec.rb +43 -0
- data/spec/lib/has_helpers/app/command/reports/generator_specs.rb +887 -0
- data/spec/lib/has_helpers/app/command_spec.rb +117 -0
- data/spec/lib/has_helpers/app/support/mocks/graphql_reports_helpers.rb +96 -0
- data/spec/lib/has_helpers/app/support/mocks/reports_api_mock.rb +47 -0
- data/spec/lib/has_helpers/attributes_spec.rb +220 -0
- data/spec/lib/has_helpers/aws_spec.rb +126 -0
- data/spec/lib/has_helpers/concerns/hydrate_concern_spec.rb +473 -0
- data/spec/lib/has_helpers/constant/definition_spec.rb +128 -0
- data/spec/lib/has_helpers/constant_spec.rb +432 -0
- data/spec/lib/has_helpers/core_ext/array_spec.rb +22 -0
- data/spec/lib/has_helpers/core_ext/date/infinity_spec.rb +67 -0
- data/spec/lib/has_helpers/core_ext/date_spec.rb +27 -0
- data/spec/lib/has_helpers/core_ext/false_class_spec.rb +7 -0
- data/spec/lib/has_helpers/core_ext/float_spec.rb +9 -0
- data/spec/lib/has_helpers/core_ext/hash_spec.rb +48 -0
- data/spec/lib/has_helpers/core_ext/integer_spec.rb +9 -0
- data/spec/lib/has_helpers/core_ext/nil_class_spec.rb +17 -0
- data/spec/lib/has_helpers/core_ext/object_spec.rb +54 -0
- data/spec/lib/has_helpers/core_ext/proc_spec.rb +24 -0
- data/spec/lib/has_helpers/core_ext/string_spec.rb +154 -0
- data/spec/lib/has_helpers/core_ext/true_class_spec.rb +7 -0
- data/spec/lib/has_helpers/data_migration/base_spec.rb +470 -0
- data/spec/lib/has_helpers/data_migration/large_worker_spec.rb +34 -0
- data/spec/lib/has_helpers/data_migration/master_worker_spec.rb +51 -0
- data/spec/lib/has_helpers/data_migration/migrator_spec.rb +113 -0
- data/spec/lib/has_helpers/data_migration/model_spec.rb +52 -0
- data/spec/lib/has_helpers/data_migration/small_worker_spec.rb +34 -0
- data/spec/lib/has_helpers/good_job/notification_alert_cleanup_worker_spec.rb +89 -0
- data/spec/lib/has_helpers/hash_accessors_spec.rb +66 -0
- data/spec/lib/has_helpers/index_spec.rb +442 -0
- data/spec/lib/has_helpers/lazy_config_spec.rb +31 -0
- data/spec/lib/has_helpers/organization_merge_spec.rb +88 -0
- data/spec/lib/has_helpers/resource_importer_spec.rb +119 -0
- data/spec/lib/has_helpers/rspec_spec.rb +16 -0
- data/spec/lib/has_helpers/search/provider_spec.rb +26 -0
- data/spec/lib/has_helpers/search/search_utils_spec.rb +134 -0
- data/spec/lib/has_helpers/sentry/graphql_trace_spec.rb +140 -0
- data/spec/lib/has_helpers/ui_attributes_spec.rb +42 -0
- data/spec/lib/has_helpers/utils/action_spec.rb +49 -0
- data/spec/lib/has_helpers/utils/alert_spec.rb +33 -0
- data/spec/lib/has_helpers/utils/dataset_spec.rb +10 -0
- data/spec/lib/has_helpers/utils/icon_spec.rb +23 -0
- data/spec/lib/has_helpers/utils/input_spec.rb +40 -0
- data/spec/lib/has_helpers/utils/log_notifier_spec.rb +25 -0
- data/spec/lib/has_helpers/utils/render_object_spec.rb +17 -0
- data/spec/lib/has_helpers/utils/s3_utils_spec.rb +78 -0
- data/spec/lib/has_helpers/utils/service_spec.rb +79 -0
- data/spec/lib/has_helpers/utils/slack_notifier_spec.rb +70 -0
- data/spec/lib/has_helpers/utils/user_restrictions_spec.rb +343 -0
- data/spec/lib/hq/structure/inserts_spec.rb +152 -0
- data/spec/lib/hq/structure_spec.rb +12 -0
- data/spec/models/cards/base_spec.rb +149 -0
- data/spec/models/cards/card_definition_resource_spec.rb +103 -0
- data/spec/models/cards/card_definition_spec.rb +50 -0
- data/spec/models/cards/card_field_spec.rb +46 -0
- data/spec/models/cards/field_builder_spec.rb +318 -0
- data/spec/models/cards/registry_spec.rb +147 -0
- data/spec/models/cards/role_card_field_spec.rb +111 -0
- data/spec/models/cards/role_resource_card_spec.rb +112 -0
- data/spec/models/has_helpers/master_record_spec.rb +20 -0
- data/spec/models/has_helpers/user_organization_spec.rb +28 -0
- data/spec/models/has_helpers/user_session_spec.rb +16 -0
- data/spec/presenters/base_presenter_spec.rb +166 -0
- data/spec/rails_helper.rb +119 -0
- data/spec/requests/has_helpers/factory_controller_spec.rb +159 -0
- data/spec/requests/has_helpers/health_check_controller_spec.rb +37 -0
- data/spec/scenarios/empty_setup.rb +6 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/has_helpers/database_helpers.rb +12 -0
- data/spec/utils/sync_utils_spec.rb +233 -0
- data/spec/workers/goodjob/elasticsearch_process_batch_spec.rb +94 -0
- data/spec/workers/goodjob/elasticsearch_process_change_logs_master_worker_spec.rb +40 -0
- data/spec/workers/goodjob/elasticsearch_process_change_logs_worker_spec.rb +82 -0
- data/spec/workers/goodjob/elasticsearch_process_queue_spec.rb +89 -0
- data/spec/workers/goodjob/elasticsearch_reindex_spec.rb +49 -0
- metadata +1593 -0
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# HasHelpers::Cards::Importer
|
|
4
|
+
#
|
|
5
|
+
# Description:
|
|
6
|
+
#
|
|
7
|
+
# Implemented service responsible for persisting all card
|
|
8
|
+
# definitions declared via the DSL (HasHelpers::Cards::CardBuilder::Base subclasses)
|
|
9
|
+
# into the database tables:
|
|
10
|
+
#
|
|
11
|
+
# - card_definitions
|
|
12
|
+
# - card_fields
|
|
13
|
+
# - card_definition_resources
|
|
14
|
+
#
|
|
15
|
+
# It must:
|
|
16
|
+
# * Upsert records deterministically (idempotent)
|
|
17
|
+
# * Support selective import by resource or all
|
|
18
|
+
# * Perform auditing (checksum + diffing) to detect drift
|
|
19
|
+
# between code and DB definitions
|
|
20
|
+
# * Fail safely when structural changes occur
|
|
21
|
+
# (e.g. field removals / relationship removals / data_type changes)
|
|
22
|
+
# * Persist a definition_json and definition_checksum
|
|
23
|
+
# for each record for future audits
|
|
24
|
+
#
|
|
25
|
+
# This class implements that contract.
|
|
26
|
+
#
|
|
27
|
+
# Important notes:
|
|
28
|
+
# - The importer operates at "card" level (one transaction per card).
|
|
29
|
+
# - It uses the normalized DSL definitions (from HasHelpers::Cards::CardBuilder::Base subclasses).
|
|
30
|
+
# - It computes checksums based on a normalized JSON representation.
|
|
31
|
+
# - It supports "dry_run" mode (audit + diff only, no DB writes).
|
|
32
|
+
# - It supports resource filtering (import only cards that involve specific resources).
|
|
33
|
+
|
|
34
|
+
require "digest"
|
|
35
|
+
require "json"
|
|
36
|
+
|
|
37
|
+
module HasHelpers
|
|
38
|
+
module Cards
|
|
39
|
+
class Importer
|
|
40
|
+
# Raised when auditing detects a breaking drift between DSL (code) and DB.
|
|
41
|
+
# We use this error to stop the import and force devs to write a data migration.
|
|
42
|
+
class AuditError < StandardError; end
|
|
43
|
+
|
|
44
|
+
# Struct used to return a summary of the import operation.
|
|
45
|
+
Result = Struct.new(
|
|
46
|
+
:created_cards,
|
|
47
|
+
:updated_cards,
|
|
48
|
+
:created_fields,
|
|
49
|
+
:updated_fields,
|
|
50
|
+
:created_resources,
|
|
51
|
+
:updated_resources,
|
|
52
|
+
:skipped_cards,
|
|
53
|
+
keyword_init: true
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Struct representing the diff between DSL and DB for a single card.
|
|
57
|
+
# Auditing / drift:
|
|
58
|
+
# - card_changes: :new, :same, :changed
|
|
59
|
+
# - fields_added / fields_removed / fields_changed
|
|
60
|
+
# - resources_added / resources_removed
|
|
61
|
+
Diff = Struct.new(
|
|
62
|
+
:card_changes,
|
|
63
|
+
:fields_added,
|
|
64
|
+
:fields_removed,
|
|
65
|
+
:fields_changed,
|
|
66
|
+
:resources_added,
|
|
67
|
+
:resources_removed,
|
|
68
|
+
keyword_init: true
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
# Public API entry point.
|
|
73
|
+
# # Import all resources declared in the DSL
|
|
74
|
+
# HasHelpers::Cards::Importer.run
|
|
75
|
+
#
|
|
76
|
+
# # Import only selected resource types
|
|
77
|
+
# HasHelpers::Cards::Importer.run([Advisor, Carrier]) # or symbols: [:advisor, :carrier]
|
|
78
|
+
#
|
|
79
|
+
# # Options (keyword args)
|
|
80
|
+
# HasHelpers::Cards::Importer.run(resources: [:advisor], mode: :apply, audit: true)
|
|
81
|
+
#
|
|
82
|
+
# Supported modes (ClickUp):
|
|
83
|
+
# - mode: :dry_run → compute and print diffs, no DB writes,
|
|
84
|
+
# raise error if audit fails.
|
|
85
|
+
# - mode: :apply → default. Always audit first. If drift is breaking,
|
|
86
|
+
# raise and do not persist.
|
|
87
|
+
def run(*args, **opts)
|
|
88
|
+
::HasHelpers::Cards::Registry.cards
|
|
89
|
+
resources, mode, audit = normalize_run_arguments(args, opts)
|
|
90
|
+
|
|
91
|
+
application = opts[:application] || opts[:application_name]
|
|
92
|
+
raise ArgumentError, "application is required (e.g. application: 'AgenciesHQ')" if application.blank?
|
|
93
|
+
|
|
94
|
+
new(application: application, resources: resources, mode: mode, audit: audit).run
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Normalizes positional + keyword arguments into:
|
|
100
|
+
# - resources (filter) → Array<Class|Symbol> or nil
|
|
101
|
+
# - mode → :apply or :dry_run
|
|
102
|
+
# - audit → boolean
|
|
103
|
+
#
|
|
104
|
+
# This is compatible with:
|
|
105
|
+
# HasHelpers::Cards::Importer.run
|
|
106
|
+
# HasHelpers::Cards::Importer.run([Advisor, Carrier])
|
|
107
|
+
# HasHelpers::Cards::Importer.run(resources: [:advisor], mode: :apply, audit: true)
|
|
108
|
+
#
|
|
109
|
+
def normalize_run_arguments(args, opts)
|
|
110
|
+
# default mode is :apply
|
|
111
|
+
mode = (opts[:mode] || :apply).to_sym
|
|
112
|
+
# default audit is true (we always audit before writing)
|
|
113
|
+
audit = opts.key?(:audit) ? opts[:audit] : true
|
|
114
|
+
|
|
115
|
+
resources =
|
|
116
|
+
if opts.key?(:resources)
|
|
117
|
+
# explicit keyword arg takes precedence
|
|
118
|
+
opts[:resources]
|
|
119
|
+
else
|
|
120
|
+
# otherwise, optional first positional arg may be the resources filter
|
|
121
|
+
args.first
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
[resources, mode, audit]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Constructor is internal; external callers use .run.
|
|
129
|
+
#
|
|
130
|
+
# resources: Class|Symbol|Array or nil (no filter)
|
|
131
|
+
# mode: :apply or :dry_run
|
|
132
|
+
# audit: true/false (whether to run drift detection)
|
|
133
|
+
#
|
|
134
|
+
def initialize(application:, resources:, mode:, audit:)
|
|
135
|
+
# Normalize resource filter into a compact set of symbols representing resource names (e.g. [:advisor, :carrier]).
|
|
136
|
+
@application_name = application.to_s
|
|
137
|
+
@application = ::HasHelpers::Application.find_by!("LOWER(name) = ?", @application_name.downcase)
|
|
138
|
+
@resource_filter = normalize_resource_filter(resources)
|
|
139
|
+
@mode = (mode || :apply).to_sym
|
|
140
|
+
@audit = audit
|
|
141
|
+
|
|
142
|
+
# This struct accumulates counts while importing.
|
|
143
|
+
@result = Result.new(
|
|
144
|
+
created_cards: 0,
|
|
145
|
+
updated_cards: 0,
|
|
146
|
+
created_fields: 0,
|
|
147
|
+
updated_fields: 0,
|
|
148
|
+
created_resources: 0,
|
|
149
|
+
updated_resources: 0,
|
|
150
|
+
skipped_cards: 0
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Main execution entry.
|
|
155
|
+
#
|
|
156
|
+
# "Importer Flow"
|
|
157
|
+
#
|
|
158
|
+
# Steps:
|
|
159
|
+
# 1. (Optionally) run migrations for cards (hook only for now).
|
|
160
|
+
# 2. Discover card definitions from the DSL (CardBuilder::Base descendants).
|
|
161
|
+
# 3. For each card:
|
|
162
|
+
# - Normalize + compute checksum (code view).
|
|
163
|
+
# - Build DB view (if card exists).
|
|
164
|
+
# - Diff + classify changes.
|
|
165
|
+
# - Audit: if breaking drift, raise AuditError.
|
|
166
|
+
# - If mode = :apply, upsert card_definitions + card_fields +
|
|
167
|
+
# card_definition_resources inside a transaction.
|
|
168
|
+
# - If mode = :dry_run, just log diffs and don't write.
|
|
169
|
+
# 4. Optional global audit: detect cards existing only in DB but
|
|
170
|
+
# not in code, when there is no resource filter (breaking).
|
|
171
|
+
# 5. Log summary of created/updated/skipped records.
|
|
172
|
+
#
|
|
173
|
+
def run
|
|
174
|
+
# "We may add a migrations section just like we have for reports and webhooks"
|
|
175
|
+
run_migrations_if_any!
|
|
176
|
+
|
|
177
|
+
# Discover all card definitions exposed by the DSL (HasHelpers::Cards::CardBuilder::Base subclasses).
|
|
178
|
+
card_pairs = discover_card_definitions
|
|
179
|
+
# Each element is: [CardClass, raw_hash_definition]
|
|
180
|
+
validate_hover_cards!(card_pairs)
|
|
181
|
+
|
|
182
|
+
# Hover-only cards live entirely in-memory (DSL registry). They are NOT persisted.
|
|
183
|
+
# Any pre-existing rows for hover-only identifiers (whether currently flagged as
|
|
184
|
+
# hover_only in DB or not) are dropped here, then we skip the import for them below.
|
|
185
|
+
purge_persisted_hover_only_cards!(card_pairs)
|
|
186
|
+
|
|
187
|
+
# Import each card definition (or audit, depending on mode), skipping hover-only definitions.
|
|
188
|
+
card_pairs.each do |klass, raw_def|
|
|
189
|
+
next if hover_only_definition?(raw_def)
|
|
190
|
+
import_card!(klass, raw_def)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Global audit for "cards present in DB but missing in code".
|
|
194
|
+
# This is only meaningful when we are importing "all resources" (i.e. no resource filter is applied).
|
|
195
|
+
audit_removed_cards!(card_pairs) if audit && resource_filter.nil?
|
|
196
|
+
|
|
197
|
+
# Log structured summary for debugging / future dashboards.
|
|
198
|
+
log_summary
|
|
199
|
+
|
|
200
|
+
# Return Result struct (created/updated counts, etc.)
|
|
201
|
+
result
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Definitions where `hover_only: true` are skipped at import — they live in
|
|
205
|
+
# memory only and are assembled from the registry at request time.
|
|
206
|
+
def hover_only_definition?(raw_def)
|
|
207
|
+
defn = raw_def.is_a?(Hash) ? raw_def.deep_symbolize_keys : raw_def.to_h.deep_symbolize_keys
|
|
208
|
+
!!defn[:hover_only]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Removes any persisted CardDefinition rows that match a hover-only DSL definition
|
|
212
|
+
# (by identifier). Hover-only cards are in-memory only; this purges any rows left
|
|
213
|
+
# over from prior runs before we made them in-memory. Safe to call repeatedly.
|
|
214
|
+
# Dependent rows cascade via :dependent => :destroy associations.
|
|
215
|
+
def purge_persisted_hover_only_cards!(card_pairs)
|
|
216
|
+
return if mode == :dry_run
|
|
217
|
+
|
|
218
|
+
hover_only_ids =
|
|
219
|
+
card_pairs.
|
|
220
|
+
select { |_klass, defn| hover_only_definition?(defn) }.
|
|
221
|
+
map { |_klass, defn| normalize_identifier((defn.deep_symbolize_keys)[:identifier]) }.
|
|
222
|
+
compact.
|
|
223
|
+
uniq
|
|
224
|
+
return if hover_only_ids.empty?
|
|
225
|
+
|
|
226
|
+
identifier_ids = ::HasHelpers::Cards::CardIdentifier.where(name: hover_only_ids).pluck(:id)
|
|
227
|
+
return if identifier_ids.empty?
|
|
228
|
+
|
|
229
|
+
::HasHelpers::Cards::CardDefinition.where(identifier_id: identifier_ids).find_each(&:destroy)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
attr_reader :resource_filter, :mode, :audit, :result, :application_name, :application
|
|
235
|
+
|
|
236
|
+
# Placeholder to run idempotent migrations before importing.
|
|
237
|
+
def run_migrations_if_any!
|
|
238
|
+
# Example: HasHelpers::Cards::Migrations.run_all if defined?(HasHelpers::Cards::Migrations)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# 1) Discover Cards
|
|
242
|
+
# Enumerate all classes inheriting from HasHelpers::Cards::CardBuilder::Base.
|
|
243
|
+
# Each subclass has already evaluated the DSL and stored its normalized definitions in .definitions.
|
|
244
|
+
# We also apply resource filtering here ( "Support selective import by resource or all").
|
|
245
|
+
def discover_card_definitions
|
|
246
|
+
::HasHelpers::Cards::CardBuilder::Base.
|
|
247
|
+
descendants.
|
|
248
|
+
flat_map do |klass|
|
|
249
|
+
# Each card builder class can define one or many card definitions.
|
|
250
|
+
Array(klass.definitions).
|
|
251
|
+
flat_map do |defn|
|
|
252
|
+
defn = defn.deep_symbolize_keys
|
|
253
|
+
defn[:application] ||= application_name if application_name.present?
|
|
254
|
+
expand_per_resource(defn)
|
|
255
|
+
end.
|
|
256
|
+
map { |defn| [klass, defn] }
|
|
257
|
+
end.
|
|
258
|
+
select { |_klass, defn| passes_resource_filter?(defn) }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Splits a normalized card definition into one per resource.
|
|
262
|
+
# Original DSL allows multiple resources per card; we materialize a separate
|
|
263
|
+
# definition for each resource to keep a 1:1 relationship.
|
|
264
|
+
def expand_per_resource(definition)
|
|
265
|
+
resources = Array(definition[:resources])
|
|
266
|
+
return [definition] if resources.empty?
|
|
267
|
+
|
|
268
|
+
resources.map do |res|
|
|
269
|
+
dup = definition.deep_dup
|
|
270
|
+
dup[:resources] = [res]
|
|
271
|
+
dup
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Returns true if this card definition should be imported for the current resource_filter.
|
|
276
|
+
# Resource filter semantics:
|
|
277
|
+
# - Accept classes (Advisor) or symbols (:advisor).
|
|
278
|
+
# - If filter is nil/empty, we import all cards.
|
|
279
|
+
# We map each DSL resource (class_name) to a symbol (advisor, client, etc.), then intersect with the filter.
|
|
280
|
+
def passes_resource_filter?(definition)
|
|
281
|
+
return true if resource_filter.blank?
|
|
282
|
+
|
|
283
|
+
dsl_resources =
|
|
284
|
+
Array(definition[:resources]).
|
|
285
|
+
map { |r| r[:class_name] || r["class_name"] }.
|
|
286
|
+
compact.
|
|
287
|
+
map(&:to_s)
|
|
288
|
+
|
|
289
|
+
(dsl_resources.map { |cn| normalize_resource_name(cn) } & resource_filter).any?
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def validate_hover_cards!(card_pairs)
|
|
293
|
+
definitions =
|
|
294
|
+
card_pairs.map do |_klass, defn|
|
|
295
|
+
defn.deep_symbolize_keys
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
definitions_by_identifier =
|
|
299
|
+
definitions.index_by { |defn| normalize_identifier(defn[:identifier]) }
|
|
300
|
+
|
|
301
|
+
definitions.each do |definition|
|
|
302
|
+
parent_identifier = normalize_identifier(definition[:identifier])
|
|
303
|
+
parent_name = definition[:name].to_s
|
|
304
|
+
|
|
305
|
+
Array(definition[:fields]).each do |field|
|
|
306
|
+
field = field.deep_symbolize_keys
|
|
307
|
+
next unless normalize_data_type_name(field[:data_type]) == :hover_card
|
|
308
|
+
|
|
309
|
+
field_key = hover_field_key(field)
|
|
310
|
+
raw_hover_card = field[:hover_card]
|
|
311
|
+
if raw_hover_card.is_a?(Hash)
|
|
312
|
+
raise AuditError,
|
|
313
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' passed a Hash to `hover_card:` (#{raw_hover_card.inspect}). " \
|
|
314
|
+
"Use `hover_card_source:` for dynamic identifiers resolved from a data_source."
|
|
315
|
+
end
|
|
316
|
+
hover_card_identifier = raw_hover_card.is_a?(String) ? raw_hover_card.presence : nil
|
|
317
|
+
dynamic_hover_card = field[:hover_card_source].present?
|
|
318
|
+
|
|
319
|
+
unless hover_card_identifier || dynamic_hover_card
|
|
320
|
+
raise AuditError,
|
|
321
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' declares data_type HoverCard but is missing hover_card (or hover_card_source)."
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# hover_record_source is optional for :resource-kind fields: the field's own
|
|
325
|
+
# resolved value (e.g. `policy.carrier_id`) is used as the hover target id.
|
|
326
|
+
if field[:hover_record_source].blank? && field[:kind].to_s.to_sym != :resource
|
|
327
|
+
raise AuditError,
|
|
328
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' declares hover_card '#{hover_card_identifier || '(dynamic)'}' but is missing hover_record_source."
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Dynamic hover_card targets are resolved at request time; we can't audit
|
|
332
|
+
# their target shape statically.
|
|
333
|
+
next if dynamic_hover_card && hover_card_identifier.nil?
|
|
334
|
+
|
|
335
|
+
target = definitions_by_identifier[hover_card_identifier]
|
|
336
|
+
unless target
|
|
337
|
+
raise AuditError,
|
|
338
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' references hover_card '#{hover_card_identifier}', but no card with that identifier exists in this import."
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
unless target[:hover_only]
|
|
342
|
+
raise AuditError,
|
|
343
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' references hover_card '#{hover_card_identifier}', but that card is not hover_only: true."
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
nested_field = Array(target[:fields]).find do |target_field|
|
|
347
|
+
normalize_data_type_name(target_field.deep_symbolize_keys[:data_type]) == :hover_card
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
next unless nested_field
|
|
351
|
+
|
|
352
|
+
nested_key = hover_field_key(nested_field.deep_symbolize_keys)
|
|
353
|
+
raise AuditError,
|
|
354
|
+
"Card '#{parent_name}' (identifier=#{parent_identifier}) field '#{field_key}' references hover_card '#{hover_card_identifier}', but that hover-only card contains nested HoverCard field '#{nested_key}'."
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def hover_field_key(field)
|
|
360
|
+
(field[:key] || field[:ref] || field[:name]).to_s
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Converts "Advisor" → :advisor, "HasContactInfo::Contact" → :contact, etc.
|
|
364
|
+
def normalize_resource_name(class_name)
|
|
365
|
+
class_name.to_s.demodulize.underscore.to_sym
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Normalizes the resource filter argument into an array of symbols.
|
|
369
|
+
# Examples:
|
|
370
|
+
# normalize_resource_filter(nil) → nil (no filter)
|
|
371
|
+
# normalize_resource_filter([Advisor, Carrier]) → [:advisor, :carrier]
|
|
372
|
+
# normalize_resource_filter(:advisor) → [:advisor]
|
|
373
|
+
def normalize_resource_filter(resources)
|
|
374
|
+
return nil if resources.nil?
|
|
375
|
+
|
|
376
|
+
list = resources.is_a?(Array) ? resources : [resources]
|
|
377
|
+
list.map { |r| r.is_a?(Class) ? normalize_resource_name(r.name) : r.to_sym }.uniq
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# 2) Normalize + checksum
|
|
381
|
+
# Normalizes a raw DSL hash definition into a deterministic structure.
|
|
382
|
+
# This is critical because:
|
|
383
|
+
# - We will compute a checksum = SHA256(JSON(definition_json)).
|
|
384
|
+
# - We want the JSON to be stable across runs (idempotency).
|
|
385
|
+
# - We want to exclude transient / DB-specific attributes (timestamps, ids).
|
|
386
|
+
# It also aligns with the "definition_json" we persist in card_definitions.
|
|
387
|
+
def normalize_definition(raw)
|
|
388
|
+
card = raw.deep_dup
|
|
389
|
+
|
|
390
|
+
# icon_name / icon_color are non-persisted, card-level presentation hints.
|
|
391
|
+
# They live in the DSL registry only, so we drop them before building the
|
|
392
|
+
# definition_json / checksum to keep persistence (and drift audits) stable.
|
|
393
|
+
card.delete(:icon_name)
|
|
394
|
+
card.delete(:icon_color)
|
|
395
|
+
|
|
396
|
+
card[:name] = card[:name].to_s
|
|
397
|
+
card[:identifier] = normalize_identifier(card[:identifier])
|
|
398
|
+
# relationship is currently not persisted in DB, but we keep it in JSON
|
|
399
|
+
card[:relationship] = card[:relationship].to_s if card[:relationship]
|
|
400
|
+
card[:layout_type] = normalize_enum_name(card[:layout_type])
|
|
401
|
+
card[:variant] = normalize_enum_name(card[:variant])
|
|
402
|
+
card[:description] = card[:description].to_s if card.key?(:description)
|
|
403
|
+
card[:is_customizable] = !!card[:is_customizable]
|
|
404
|
+
card[:default_visibility] = card[:default_visibility] # rubocop:disable Lint/SelfAssignment
|
|
405
|
+
card[:virtual] = !!card[:virtual]
|
|
406
|
+
card[:default_position] = card[:default_position] # rubocop:disable Lint/SelfAssignment
|
|
407
|
+
card[:expansion_scope] = nil # runtime-only
|
|
408
|
+
card[:data_sources] =
|
|
409
|
+
Array(card[:data_sources]).
|
|
410
|
+
map { |ds| normalize_data_source(ds) }.
|
|
411
|
+
compact
|
|
412
|
+
# Normalize resources: array of hashes { class_name, is_mandatory }
|
|
413
|
+
card[:resources] =
|
|
414
|
+
Array(card[:resources]).
|
|
415
|
+
map do |r|
|
|
416
|
+
{ class_name: r[:class_name].to_s, is_mandatory: !!r[:is_mandatory] }
|
|
417
|
+
end.
|
|
418
|
+
sort_by { |r| r[:class_name] } # deterministic ordering
|
|
419
|
+
# Normalize fields: we standardize keys + values and then sort.
|
|
420
|
+
card[:fields] =
|
|
421
|
+
Array(card[:fields]).
|
|
422
|
+
map { |f| normalize_field_hash(f) }.
|
|
423
|
+
sort_by { |f| [f[:default_position] || 0, field_identity(f)] }
|
|
424
|
+
|
|
425
|
+
card
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Normalizes identifier (option) into a string name.
|
|
429
|
+
# e.g. HasHelpers::Cards::CardIdentifier::DEMOGRAPHICS -> "Demographics"
|
|
430
|
+
def normalize_identifier(value)
|
|
431
|
+
return nil if value.nil?
|
|
432
|
+
value.respond_to?(:name) && !value.is_a?(String) ? value.name.to_s : value.to_s
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Normalizes enums (layout_type, variant) to their name strings.
|
|
436
|
+
# e.g. HasHelpers::Cards::CardLayoutType::SUMMARY → "Summary".
|
|
437
|
+
def normalize_enum_name(value)
|
|
438
|
+
return nil if value.nil?
|
|
439
|
+
|
|
440
|
+
if value.respond_to?(:name) && !value.is_a?(String)
|
|
441
|
+
value.name.to_s
|
|
442
|
+
else
|
|
443
|
+
value.to_s
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Normalizes a field hash (resource or custom) for checksum + diff.
|
|
448
|
+
# Fields (with kind, data_type, etc.)
|
|
449
|
+
# We unify:
|
|
450
|
+
# - kind: :resource vs :custom
|
|
451
|
+
# - data_type: normalized symbol (e.g., :numeric, :string)
|
|
452
|
+
# - default_visibility, is_required: booleans
|
|
453
|
+
# - default_position: numeric (used for ordering but not breaking if changed)
|
|
454
|
+
def normalize_field_hash(f)
|
|
455
|
+
kind = (f[:kind] || f["kind"]).to_sym
|
|
456
|
+
|
|
457
|
+
base = {
|
|
458
|
+
kind: kind,
|
|
459
|
+
data_type: normalize_data_type_name(f[:data_type] || f["data_type"]),
|
|
460
|
+
default_visibility: !!(f[:default_visibility] || f["default_visibility"]),
|
|
461
|
+
default_position: f[:default_position] || f["default_position"],
|
|
462
|
+
is_required: !!(f[:is_required] || f["is_required"])
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case kind
|
|
466
|
+
when :resource
|
|
467
|
+
base.merge(
|
|
468
|
+
ref: (f[:ref] || f["ref"]).to_s,
|
|
469
|
+
name: (f.key?(:name) ? f[:name] : f["name"])
|
|
470
|
+
)
|
|
471
|
+
when :custom
|
|
472
|
+
base.merge(
|
|
473
|
+
key: (f[:key] || f["key"]).to_s,
|
|
474
|
+
name: f[:name] || f["name"]
|
|
475
|
+
# note: we intentionally exclude mapper from checksum, it's considered implementation detail, not structure.
|
|
476
|
+
)
|
|
477
|
+
when :virtual
|
|
478
|
+
base.merge(
|
|
479
|
+
ref: (f[:ref] || f["ref"]).to_s,
|
|
480
|
+
name: f[:name] || f["name"],
|
|
481
|
+
custom_label: !!(f[:custom_label] || f["custom_label"]),
|
|
482
|
+
custom_label_key: (f[:custom_label_key] || f["custom_label_key"])&.to_s
|
|
483
|
+
)
|
|
484
|
+
else
|
|
485
|
+
base
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Normalizes data_type for checksum purposes.
|
|
490
|
+
# Example:
|
|
491
|
+
# HasHelpers::Cards::CardDataType::Numeric → :numeric
|
|
492
|
+
# "numeric" → :numeric
|
|
493
|
+
# :numeric → :numeric
|
|
494
|
+
def normalize_data_type_name(value)
|
|
495
|
+
return nil if value.nil?
|
|
496
|
+
|
|
497
|
+
if value.respond_to?(:name) && !value.is_a?(String)
|
|
498
|
+
value.name.to_s.underscore.to_sym
|
|
499
|
+
elsif value.is_a?(Symbol)
|
|
500
|
+
value
|
|
501
|
+
else
|
|
502
|
+
value.to_s.underscore.to_sym
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def normalize_data_source(ds)
|
|
507
|
+
return nil if ds.nil?
|
|
508
|
+
|
|
509
|
+
{ name: ds[:name].to_s }
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Identity of a field, used for:
|
|
513
|
+
# - indexing
|
|
514
|
+
# - diff / drift detection
|
|
515
|
+
# For resource fields: [:resource, "age"]
|
|
516
|
+
# For custom fields: [:custom, "DEMOGRAPHICS_FULL_NAME"]
|
|
517
|
+
def field_identity(field_hash)
|
|
518
|
+
case field_hash[:kind]
|
|
519
|
+
when :custom
|
|
520
|
+
[:custom, field_hash[:key].to_s]
|
|
521
|
+
when :virtual
|
|
522
|
+
[:virtual, field_hash[:ref].to_s]
|
|
523
|
+
else
|
|
524
|
+
[:resource, field_hash[:ref].to_s]
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# "Compute a checksum"
|
|
529
|
+
# Uses SHA256 over the JSON-encoded normalized definition.
|
|
530
|
+
# This is the value we store in definition_checksum.
|
|
531
|
+
def checksum_for(normalized_definition)
|
|
532
|
+
Digest::SHA256.hexdigest(JSON.generate(normalized_definition))
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# 3) Import one card (audit + diff + upsert)
|
|
536
|
+
# Imports (or audits) a single card.
|
|
537
|
+
def import_card!(klass, raw_definition)
|
|
538
|
+
# Normalize DSL definition (this will be stored as definition_json).
|
|
539
|
+
normalized = normalize_definition(raw_definition)
|
|
540
|
+
checksum = checksum_for(normalized)
|
|
541
|
+
|
|
542
|
+
identifier_name = normalized[:identifier]
|
|
543
|
+
identifier_opt = identifier_option_for(identifier_name)
|
|
544
|
+
resource_hash = Array(normalized[:resources]).first
|
|
545
|
+
application_name = self.application_name
|
|
546
|
+
resource_opt = resource_hash && find_resource_class(resource_hash[:class_name], application_name)
|
|
547
|
+
|
|
548
|
+
# Concurrency: lock card rows by identifier/resource before writing to avoid race conditions
|
|
549
|
+
# We lock by identifier_id using SELECT ... FOR UPDATE.
|
|
550
|
+
db_card =
|
|
551
|
+
identifier_opt &&
|
|
552
|
+
resource_opt &&
|
|
553
|
+
::HasHelpers::Cards::CardDefinition.
|
|
554
|
+
joins(:card_definition_resources).
|
|
555
|
+
lock("FOR UPDATE").
|
|
556
|
+
where(identifier_id: identifier_opt.id, card_definition_resources: { resource_id: resource_opt.id }).
|
|
557
|
+
first
|
|
558
|
+
# If we already have a definition_checksum in DB and it matches the computed checksum, we consider there is no drift for this card.
|
|
559
|
+
if db_card &&
|
|
560
|
+
db_card.respond_to?(:definition_checksum) &&
|
|
561
|
+
db_card.definition_checksum.present? &&
|
|
562
|
+
db_card.definition_checksum == checksum
|
|
563
|
+
diffs = Diff.new(
|
|
564
|
+
card_changes: :same,
|
|
565
|
+
fields_added: [],
|
|
566
|
+
fields_removed: [],
|
|
567
|
+
fields_changed: [],
|
|
568
|
+
resources_added: [],
|
|
569
|
+
resources_removed: []
|
|
570
|
+
)
|
|
571
|
+
else
|
|
572
|
+
# Build "DB view" (normalized structure) from existing DB records, if any.
|
|
573
|
+
db_view = db_card && build_db_view_for(db_card)
|
|
574
|
+
# Compute structural diff using normalized DSL view vs DB view.
|
|
575
|
+
diffs = compute_diff(normalized, db_view)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Audit / drift classification.
|
|
579
|
+
# Fail safely when structural changes occur.
|
|
580
|
+
classify_and_raise_if_breaking!(klass, normalized, diffs) if audit
|
|
581
|
+
|
|
582
|
+
# If we are in dry_run mode:
|
|
583
|
+
# - we only log diffs
|
|
584
|
+
# - we do not write anything to DB
|
|
585
|
+
if mode == :dry_run
|
|
586
|
+
log_diffs(normalized[:identifier], diffs)
|
|
587
|
+
result.skipped_cards += 1
|
|
588
|
+
return
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Transactions: one transaction per card to ensure consistency.
|
|
592
|
+
::ActiveRecord::Base.transaction do
|
|
593
|
+
# Upsert card_definitions (by identifier) + definition_json + definition_checksum
|
|
594
|
+
card_def = upsert_card_definition!(normalized, checksum, db_card)
|
|
595
|
+
# Upsert card_definition_resources (links between card_definition and resource)
|
|
596
|
+
upsert_card_resources!(card_def, normalized[:resources], application_name)
|
|
597
|
+
# Upsert card_fields (one per resource/custom fiexld)
|
|
598
|
+
upsert_card_fields!(card_def, normalized[:fields])
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Build a "normalized DB view" for an existing card
|
|
603
|
+
# Builds a normalized hash representing how this card currently looks in the DB (card_definitions + card_fields + card_definition_resources).
|
|
604
|
+
# We reuse normalize_definition to align it with the DSL view so that:
|
|
605
|
+
# - compute_diff(code_view, db_view) becomes simple.
|
|
606
|
+
def build_db_view_for(card_def)
|
|
607
|
+
db_app = card_def.card_definition_resources.includes(resource: :application).first&.resource&.application&.name
|
|
608
|
+
{
|
|
609
|
+
application: db_app,
|
|
610
|
+
name: card_def.name,
|
|
611
|
+
identifier: card_def.identifier&.name,
|
|
612
|
+
relationship: nil,
|
|
613
|
+
layout_type: card_def.layout_type&.name,
|
|
614
|
+
variant: card_def.variant&.name,
|
|
615
|
+
description: card_def.description,
|
|
616
|
+
is_customizable: card_def.is_customizable,
|
|
617
|
+
virtual: card_def.respond_to?(:is_virtual) ? card_def.is_virtual : card_def.try(:virtual),
|
|
618
|
+
default_position: card_def.position,
|
|
619
|
+
resources: build_db_resources(card_def),
|
|
620
|
+
fields: build_db_fields(card_def)
|
|
621
|
+
}.then { |h| normalize_definition(h) }
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Returns normalized array of resources for the DB view.
|
|
625
|
+
# For each CardDefinitionResource we extract:
|
|
626
|
+
# - resource.name → class_name
|
|
627
|
+
# - is_mandatory → boolean
|
|
628
|
+
def build_db_resources(card_def)
|
|
629
|
+
card_def.card_definition_resources.includes(:resource).map do |link|
|
|
630
|
+
{
|
|
631
|
+
class_name: link.resource.name, # HasHelpers::Resource.name
|
|
632
|
+
is_mandatory: !!link.is_mandatory
|
|
633
|
+
}
|
|
634
|
+
end.sort_by { |r| r[:class_name] }
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Returns normalized array of fields for the DB view.
|
|
638
|
+
# We map them back to the same shape used in normalize_definition.
|
|
639
|
+
def build_db_fields(card_def)
|
|
640
|
+
card_def.card_fields.
|
|
641
|
+
includes(:field_resource, :data_type).
|
|
642
|
+
map do |field|
|
|
643
|
+
if field.virtual_ref.present?
|
|
644
|
+
{
|
|
645
|
+
kind: :virtual,
|
|
646
|
+
ref: field.virtual_ref.to_s,
|
|
647
|
+
name: field.field_custom_name,
|
|
648
|
+
data_type: normalize_data_type_name(field.data_type),
|
|
649
|
+
default_visibility: !!field.default_visibility,
|
|
650
|
+
default_position: field.default_position,
|
|
651
|
+
is_required: !!field.is_required,
|
|
652
|
+
custom_label: !!field.try(:custom_label),
|
|
653
|
+
custom_label_key: field.try(:custom_label_key).presence
|
|
654
|
+
}
|
|
655
|
+
elsif field.field_resource.present?
|
|
656
|
+
{
|
|
657
|
+
kind: :resource,
|
|
658
|
+
ref: field.field_resource&.name.to_s,
|
|
659
|
+
name: field.field_custom_name,
|
|
660
|
+
data_type: normalize_data_type_name(field.data_type),
|
|
661
|
+
default_visibility: !!field.default_visibility,
|
|
662
|
+
default_position: field.default_position,
|
|
663
|
+
is_required: !!field.is_required
|
|
664
|
+
}
|
|
665
|
+
else
|
|
666
|
+
{
|
|
667
|
+
kind: :custom,
|
|
668
|
+
key: field.field_custom_name.to_s,
|
|
669
|
+
name: field.field_custom_name,
|
|
670
|
+
data_type: normalize_data_type_name(field.data_type),
|
|
671
|
+
default_visibility: !!field.default_visibility,
|
|
672
|
+
default_position: field.default_position,
|
|
673
|
+
is_required: !!field.is_required
|
|
674
|
+
}
|
|
675
|
+
end
|
|
676
|
+
end.sort_by { |f| [f[:default_position] || 0, field_identity(f)] }
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Diff logic (code vs DB)
|
|
680
|
+
# Computes a Diff object given the DSL (code_view) and DB (db_view).
|
|
681
|
+
# "How to detect drift" + "Auditing".
|
|
682
|
+
# Cases:
|
|
683
|
+
# - db_view is nil → this is a brand new card (additive).
|
|
684
|
+
# - otherwise → we compute card attribute changes + fields/resources changes.
|
|
685
|
+
def compute_diff(code_view, db_view)
|
|
686
|
+
if db_view.nil?
|
|
687
|
+
return Diff.new(
|
|
688
|
+
card_changes: :new,
|
|
689
|
+
fields_added: code_view[:fields],
|
|
690
|
+
fields_removed: [],
|
|
691
|
+
fields_changed: [],
|
|
692
|
+
resources_added: code_view[:resources],
|
|
693
|
+
resources_removed: []
|
|
694
|
+
)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
Diff.new(
|
|
698
|
+
card_changes: card_changes(code_view, db_view),
|
|
699
|
+
fields_added: diff_fields_added(code_view, db_view),
|
|
700
|
+
fields_removed: diff_fields_removed(code_view, db_view),
|
|
701
|
+
fields_changed: diff_fields_changed(code_view, db_view),
|
|
702
|
+
resources_added: diff_resources_added(code_view, db_view),
|
|
703
|
+
resources_removed: diff_resources_removed(code_view, db_view)
|
|
704
|
+
)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Compares high-level card attributes (excluding fields/resources).
|
|
708
|
+
# Card attribute changes (e.g., layout_type, name, is_customizable) → breaking
|
|
709
|
+
# We mark card_attributes as :changed or :same.
|
|
710
|
+
def card_changes(code, db)
|
|
711
|
+
important_keys = %i[
|
|
712
|
+
name
|
|
713
|
+
layout_type
|
|
714
|
+
variant
|
|
715
|
+
virtual
|
|
716
|
+
is_customizable
|
|
717
|
+
description
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
changed = important_keys.any? { |key| code[key] != db[key] }
|
|
721
|
+
changed ? :changed : :same
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# Helper: builds an index of fields by their identity (kind + ref/key).
|
|
725
|
+
def index_fields_by_identity(fields)
|
|
726
|
+
fields.index_by { |f| field_identity(f) }
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Code fields that are missing in DB → additive.
|
|
730
|
+
def diff_fields_added(code, db)
|
|
731
|
+
code_index = index_fields_by_identity(code[:fields])
|
|
732
|
+
db_index = index_fields_by_identity(db[:fields])
|
|
733
|
+
|
|
734
|
+
(code_index.keys - db_index.keys).map { |k| code_index[k] }
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# DB fields that are missing in code → breaking (removal).
|
|
738
|
+
def diff_fields_removed(code, db)
|
|
739
|
+
code_index = index_fields_by_identity(code[:fields])
|
|
740
|
+
db_index = index_fields_by_identity(db[:fields])
|
|
741
|
+
|
|
742
|
+
(db_index.keys - code_index.keys).map { |k| db_index[k] }
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Fields present in both code and DB but with changed structural attributes.
|
|
746
|
+
# Example: Changed field attributes (e.g., data_type, required...) → breaking.
|
|
747
|
+
# Reordering is non-breaking, so position differences are ignored here.
|
|
748
|
+
def diff_fields_changed(code, db)
|
|
749
|
+
code_index = index_fields_by_identity(code[:fields])
|
|
750
|
+
db_index = index_fields_by_identity(db[:fields])
|
|
751
|
+
common = code_index.keys & db_index.keys
|
|
752
|
+
|
|
753
|
+
common.map do |key|
|
|
754
|
+
c = code_index[key]
|
|
755
|
+
d = db_index[key]
|
|
756
|
+
|
|
757
|
+
# We consider changes in these attributes as potentially breaking.
|
|
758
|
+
attrs = %i[
|
|
759
|
+
data_type
|
|
760
|
+
is_required
|
|
761
|
+
kind
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
if attrs.any? { |a| c[a] != d[a] }
|
|
765
|
+
{ identity: key, code: c, db: d }
|
|
766
|
+
end
|
|
767
|
+
end.compact
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# Resources present in code but missing in DB → additive.
|
|
771
|
+
def diff_resources_added(code, db)
|
|
772
|
+
code_set = Set.new(code[:resources].map { |r| r[:class_name].to_s })
|
|
773
|
+
db_set = Set.new(db[:resources].map { |r| r[:class_name].to_s })
|
|
774
|
+
|
|
775
|
+
(code_set - db_set).map do |cn|
|
|
776
|
+
code[:resources].find { |r| r[:class_name].to_s == cn }
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# Resources present in DB but missing in code → breaking (relationship removal).
|
|
781
|
+
# Exmaple: Removed relationship → breaking
|
|
782
|
+
def diff_resources_removed(code, db)
|
|
783
|
+
code_set = Set.new(code[:resources].map { |r| r[:class_name].to_s })
|
|
784
|
+
db_set = Set.new(db[:resources].map { |r| r[:class_name].to_s })
|
|
785
|
+
|
|
786
|
+
(db_set - code_set).map do |cn|
|
|
787
|
+
db[:resources].find { |r| r[:class_name].to_s == cn }
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Audit classification: raise if breaking drift.
|
|
792
|
+
# Given the diff, determine if there is any breaking drift.
|
|
793
|
+
# If yes, raise HasHelpers::Cards::Importer::AuditError with detailed messages,in line with the examples (Removed field, Changed data_type, Removed relationship).
|
|
794
|
+
def classify_and_raise_if_breaking!(_klass, code_view, diff)
|
|
795
|
+
card_id = code_view[:identifier]
|
|
796
|
+
name = code_view[:name]
|
|
797
|
+
|
|
798
|
+
msgs = []
|
|
799
|
+
|
|
800
|
+
# Card-level attribute changes
|
|
801
|
+
if diff.card_changes == :changed
|
|
802
|
+
msgs << "Card '#{name}' (identifier=#{card_id}) attributes changed (name/layout/variant/is_customizable/position/description). "\
|
|
803
|
+
"This is a breaking change. Create a data migration to update/remove it and then try again."
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Fields removed from code but still in DB
|
|
807
|
+
diff.fields_removed.each do |field|
|
|
808
|
+
label = field_identity(field).join(":")
|
|
809
|
+
msgs << "Card '#{name}' (identifier=#{card_id}) drift: field '#{label}' is persisted but not in code. "\
|
|
810
|
+
"This is a breaking change. Create a data migration to remove it and then try again."
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# Fields with changed attributes (e.g. data_type, required...)
|
|
814
|
+
diff.fields_changed.each do |change|
|
|
815
|
+
label = change[:identity].join(":")
|
|
816
|
+
msgs << "Card '#{name}' (identifier=#{card_id}) field '#{label}' changed from #{change[:db].inspect} to #{change[:code].inspect}. "\
|
|
817
|
+
"This is a breaking change. Create a data migration and then try again."
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# Relationships removed (card_definition_resources removed in code)
|
|
821
|
+
diff.resources_removed.each do |res|
|
|
822
|
+
msgs << "Card '#{name}' (identifier=#{card_id}) drift: resource '#{res[:class_name]}' is persisted but not present in code. "\
|
|
823
|
+
"This is a breaking change. Create a data migration to remove it and then try again."
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
raise AuditError, msgs.join("\n") if msgs.any?
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# Logs diffs for dry_run mode.
|
|
830
|
+
# Example: mode: :dry_run → compute and print diffs, no DB writes.
|
|
831
|
+
def log_diffs(identifier, diff)
|
|
832
|
+
Rails.logger.info(
|
|
833
|
+
event: "cards_importer.dry_run",
|
|
834
|
+
identifier: identifier,
|
|
835
|
+
card_changes: diff.card_changes,
|
|
836
|
+
fields_added_count: diff.fields_added.size,
|
|
837
|
+
fields_removed_count: diff.fields_removed.size,
|
|
838
|
+
fields_changed_count: diff.fields_changed.size,
|
|
839
|
+
resources_added_count: diff.resources_added.size,
|
|
840
|
+
resources_removed_count: diff.resources_removed.size
|
|
841
|
+
)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# Global audit: cards present only in DB
|
|
845
|
+
# When there is no resource filter, we also check for cards that:
|
|
846
|
+
# - exist in card_definitions (DB)
|
|
847
|
+
# - but do not appear in any DSL definition.
|
|
848
|
+
#
|
|
849
|
+
def audit_removed_cards!(card_pairs)
|
|
850
|
+
# Track definitions by [identifier, resource_name] so we detect per-resource removals
|
|
851
|
+
defs_in_code =
|
|
852
|
+
card_pairs.map do |_klass, defn|
|
|
853
|
+
norm = normalize_definition(defn.deep_symbolize_keys)
|
|
854
|
+
res = Array(norm[:resources]).first
|
|
855
|
+
[norm[:identifier], res&.[](:class_name)]
|
|
856
|
+
end.compact.to_set
|
|
857
|
+
|
|
858
|
+
app_id = application.id
|
|
859
|
+
|
|
860
|
+
::HasHelpers::Cards::CardDefinition.
|
|
861
|
+
joins(card_definition_resources: { resource: :application }).
|
|
862
|
+
where(applications: { id: app_id }).
|
|
863
|
+
includes(:identifier, card_definition_resources: :resource).
|
|
864
|
+
find_each do |card|
|
|
865
|
+
identifier_name = card.identifier&.name
|
|
866
|
+
resource_name = card.card_definition_resources.first&.resource&.name
|
|
867
|
+
next if identifier_name.blank?
|
|
868
|
+
next if defs_in_code.include?([identifier_name, resource_name])
|
|
869
|
+
|
|
870
|
+
raise AuditError,
|
|
871
|
+
"Card '#{card.name}' (identifier=#{identifier_name}, resource=#{resource_name}) exists in DB but not in the DSL. "\
|
|
872
|
+
"This is a breaking removal. Create a data migration to remove it and then try again."
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# 4) Persist (Upsert)
|
|
877
|
+
# Upserts into card_definitions using identifier as the logical key.
|
|
878
|
+
# Example attributes:
|
|
879
|
+
# - "Upsert records deterministically (idempotent)"
|
|
880
|
+
# - "Persist a definition_json and definition_checksum for each record"
|
|
881
|
+
# If db_card is nil → we create a new CardDefinition.
|
|
882
|
+
# Otherwise → we update the existing one.
|
|
883
|
+
def upsert_card_definition!(code, checksum, db_card)
|
|
884
|
+
card =
|
|
885
|
+
if db_card
|
|
886
|
+
result.updated_cards += 1
|
|
887
|
+
db_card
|
|
888
|
+
else
|
|
889
|
+
result.created_cards += 1
|
|
890
|
+
::HasHelpers::Cards::CardDefinition.new
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
card.name = code[:name]
|
|
894
|
+
card.identifier = identifier_option_for(code[:identifier])
|
|
895
|
+
card.layout_type = layout_type_option_for(code[:layout_type])
|
|
896
|
+
card.variant = variant_option_for(code[:variant])
|
|
897
|
+
card.description = code[:description]
|
|
898
|
+
card.is_customizable = code[:is_customizable]
|
|
899
|
+
card.default_visibility = code[:default_visibility] if card.respond_to?(:default_visibility=)
|
|
900
|
+
card.position = code[:default_position]
|
|
901
|
+
card.relationship = code[:relationship]
|
|
902
|
+
card.relationship_klass = code[:relationship_klass]
|
|
903
|
+
if card.has_attribute?(:is_virtual)
|
|
904
|
+
card.is_virtual = code[:virtual] ? true : nil
|
|
905
|
+
elsif card.respond_to?(:virtual=)
|
|
906
|
+
card.virtual = code[:virtual] ? true : nil
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# These 2 attributes must exist in card_definitions table:
|
|
910
|
+
# - definition_json (jsonb or text)
|
|
911
|
+
# - definition_checksum (string)
|
|
912
|
+
# They are used for auditing future runs.
|
|
913
|
+
if card.has_attribute?(:definition_json)
|
|
914
|
+
card.definition_json = code
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
if card.has_attribute?(:definition_checksum)
|
|
918
|
+
card.definition_checksum = checksum
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
card.save!
|
|
922
|
+
card
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Upserts card_definition_resources for a given card.
|
|
926
|
+
# Matching key: (card_definition_id, resource_id).
|
|
927
|
+
# Note: we do not delete DB relationships that are not present in code, because that would be a breaking removal and is handled at audit time.
|
|
928
|
+
def upsert_card_resources!(card_def, resources_from_code, application_name = nil)
|
|
929
|
+
# indexing the existing links by resource name ("Advisor", "Client", etc.)
|
|
930
|
+
existing_links_by_name =
|
|
931
|
+
card_def.card_definition_resources.includes(:resource).index_by { |link| link.resource.name }
|
|
932
|
+
|
|
933
|
+
resources_from_code.each_with_index do |res, index|
|
|
934
|
+
class_name = res[:class_name].to_s
|
|
935
|
+
|
|
936
|
+
# 1) if a link already exists for that resource name, we reuse it
|
|
937
|
+
link = existing_links_by_name[class_name]
|
|
938
|
+
resource = link&.resource
|
|
939
|
+
|
|
940
|
+
# 2) if not existing, we lookup the resource by name (may fail if DB is dirty)
|
|
941
|
+
resource ||= find_resource_class(class_name, application_name)
|
|
942
|
+
|
|
943
|
+
# 3) we ensure there is a link for (card_definition, resource)
|
|
944
|
+
link ||= ::HasHelpers::Cards::CardDefinitionResource.find_or_initialize_by(
|
|
945
|
+
card_definition_id: card_def.id,
|
|
946
|
+
resource_id: resource.id
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
if link.new_record?
|
|
950
|
+
result.created_resources += 1
|
|
951
|
+
else
|
|
952
|
+
result.updated_resources += 1
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
link.is_mandatory = res[:is_mandatory]
|
|
956
|
+
# If this is the first time we see the link, we set a default position.
|
|
957
|
+
link.default_position ||= (index + 1) * 10
|
|
958
|
+
link.save!
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Upserts card_fields for a given card.
|
|
963
|
+
# Matching keys:
|
|
964
|
+
# - (card_definition_id, field_id) for resource fields
|
|
965
|
+
# - (card_definition_id, field_custom_key) for custom fields
|
|
966
|
+
# We use find_or_initialize_field to encapsulate that logic.
|
|
967
|
+
def upsert_card_fields!(card_def, fields_from_code)
|
|
968
|
+
# All resources (HasHelpers::Resource) associated with this card
|
|
969
|
+
parent_ids = card_def.card_definition_resources.pluck(:resource_id)
|
|
970
|
+
|
|
971
|
+
fields_from_code.each do |f|
|
|
972
|
+
field = find_or_initialize_field(card_def, f, parent_ids)
|
|
973
|
+
|
|
974
|
+
if field.new_record?
|
|
975
|
+
result.created_fields += 1
|
|
976
|
+
else
|
|
977
|
+
result.updated_fields += 1
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
apply_field_attributes!(field, f, parent_ids, card_def)
|
|
981
|
+
field.save!
|
|
982
|
+
end
|
|
983
|
+
# As with resources, we do not delete existing DB fields
|
|
984
|
+
# that are missing from code; that drift is handled by the audit.
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
# Finds or initializes a CardField given the card_def and field hash.
|
|
988
|
+
# Custom fields:
|
|
989
|
+
# - identity: (card_definition_id, field_custom_name)
|
|
990
|
+
# Resource fields:
|
|
991
|
+
# - identity: (card_definition_id, field_resource)
|
|
992
|
+
def find_or_initialize_field(card_def, field_hash, parent_ids)
|
|
993
|
+
identity = field_identity(field_hash)
|
|
994
|
+
|
|
995
|
+
case identity.first
|
|
996
|
+
when :custom
|
|
997
|
+
::HasHelpers::Cards::CardField.find_or_initialize_by(
|
|
998
|
+
card_definition_id: card_def.id,
|
|
999
|
+
field_custom_name: field_hash[:name]
|
|
1000
|
+
)
|
|
1001
|
+
when :virtual
|
|
1002
|
+
scope = ::HasHelpers::Cards::CardField.where(card_definition_id: card_def.id, virtual_ref: identity.last)
|
|
1003
|
+
scope.first || ::HasHelpers::Cards::CardField.new(card_definition_id: card_def.id, virtual_ref: identity.last)
|
|
1004
|
+
else
|
|
1005
|
+
resource = find_field_resource(identity.last, parent_ids, card_def.relationship_klass)
|
|
1006
|
+
::HasHelpers::Cards::CardField.find_or_initialize_by(
|
|
1007
|
+
card_definition_id: card_def.id,
|
|
1008
|
+
field_resource: resource
|
|
1009
|
+
)
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# Applies attributes from the normalized field hash onto a CardField model.
|
|
1014
|
+
# - Sets field_custom_name for custom fields.
|
|
1015
|
+
# - Sets field_resource for resource fields.
|
|
1016
|
+
# - Sets data_type, default_position, default_visibility, is_required.
|
|
1017
|
+
def apply_field_attributes!(field, f, parent_ids, card_def)
|
|
1018
|
+
case f[:kind]
|
|
1019
|
+
when :custom
|
|
1020
|
+
field.field_custom_name = f[:name]
|
|
1021
|
+
field.field_id = nil
|
|
1022
|
+
field.virtual_ref = nil if field.respond_to?(:virtual_ref=)
|
|
1023
|
+
when :virtual
|
|
1024
|
+
field.field_custom_name = f[:name]
|
|
1025
|
+
field.field_id = nil
|
|
1026
|
+
field.virtual_ref = f[:ref] if field.respond_to?(:virtual_ref=)
|
|
1027
|
+
field.field_resource = nil if field.respond_to?(:field_resource=)
|
|
1028
|
+
field.custom_label = !!f[:custom_label] if field.respond_to?(:custom_label=)
|
|
1029
|
+
field.custom_label_key = f[:custom_label_key].presence if field.respond_to?(:custom_label_key=)
|
|
1030
|
+
else
|
|
1031
|
+
resource = find_field_resource(f[:ref], parent_ids, card_def.relationship_klass)
|
|
1032
|
+
field.field_resource = resource
|
|
1033
|
+
|
|
1034
|
+
# Persist static label override (DSL name:)
|
|
1035
|
+
name_override = (f[:name] || f["name"]).presence
|
|
1036
|
+
field.field_custom_name = name_override if name_override
|
|
1037
|
+
|
|
1038
|
+
field.virtual_ref = nil if field.respond_to?(:virtual_ref=)
|
|
1039
|
+
|
|
1040
|
+
field.custom_label = false if field.respond_to?(:custom_label=)
|
|
1041
|
+
field.custom_label_key = nil if field.respond_to?(:custom_label_key=)
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
field.data_type = lookup_card_data_type(f[:data_type])
|
|
1045
|
+
field.default_position = f[:default_position]
|
|
1046
|
+
field.default_visibility = f[:default_visibility]
|
|
1047
|
+
field.is_required = f[:is_required]
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# Lookups (Options + Resources)
|
|
1051
|
+
# Finds the HasHelpers::Cards::CardIdentifier option by its name.
|
|
1052
|
+
def identifier_option_for(identifier_name)
|
|
1053
|
+
return nil if identifier_name.blank?
|
|
1054
|
+
|
|
1055
|
+
if identifier_name.is_a?(::HasHelpers::Cards::CardIdentifier)
|
|
1056
|
+
identifier_name
|
|
1057
|
+
else
|
|
1058
|
+
::HasHelpers::Cards::CardIdentifier.find_by!(name: identifier_name.to_s)
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Finds the HasHelpers::Cards::CardLayoutType option by its name.
|
|
1063
|
+
def layout_type_option_for(layout_name)
|
|
1064
|
+
return nil if layout_name.blank?
|
|
1065
|
+
|
|
1066
|
+
if layout_name.is_a?(::HasHelpers::Cards::CardLayoutType)
|
|
1067
|
+
layout_name
|
|
1068
|
+
else
|
|
1069
|
+
::HasHelpers::Cards::CardLayoutType.find_by!(name: layout_name.to_s)
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Finds the HasHelpers::Cards::CardVariantType option by its name.
|
|
1074
|
+
def variant_option_for(variant_name)
|
|
1075
|
+
return nil if variant_name.blank?
|
|
1076
|
+
|
|
1077
|
+
if variant_name.is_a?(::HasHelpers::Cards::CardVariantType)
|
|
1078
|
+
variant_name
|
|
1079
|
+
else
|
|
1080
|
+
::HasHelpers::Cards::CardVariantType.find_by!(name: variant_name.to_s)
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Resolves data_type (symbol or option) to HasHelpers::Cards::CardDataType.
|
|
1085
|
+
# Example:
|
|
1086
|
+
# :numeric → HasHelpers::Cards::CardDataType.find_by!(name: "Numeric")
|
|
1087
|
+
def lookup_card_data_type(data_type_ident)
|
|
1088
|
+
return nil if data_type_ident.nil?
|
|
1089
|
+
|
|
1090
|
+
if data_type_ident.is_a?(::HasHelpers::Cards::CardDataType)
|
|
1091
|
+
data_type_ident
|
|
1092
|
+
else
|
|
1093
|
+
::HasHelpers::Cards::CardDataType.find_by!(name: data_type_ident.to_s.camelize)
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# Search for the correct field (HasHelpers::Resource) restricting by:
|
|
1098
|
+
# - name (field_name)
|
|
1099
|
+
# - parent_id in the card's resources (when present)
|
|
1100
|
+
# - relationship_klass is the relationship class for the relationship from parent resource this card is using
|
|
1101
|
+
# - application sanity between this row and its field_resource (if any)
|
|
1102
|
+
#
|
|
1103
|
+
# If after filtering there is more than one match, we treat it as a real
|
|
1104
|
+
# data duplication problem and raise AuditError (so devs clean it up).
|
|
1105
|
+
def find_field_resource(field_name, parent_ids, relationship_klass = nil)
|
|
1106
|
+
base_resource = ::HasHelpers::Resource.find(parent_ids.first)
|
|
1107
|
+
application = base_resource.application
|
|
1108
|
+
parent_resource = (relationship_klass && ::HasHelpers::Resource.find_by(name: relationship_klass, parent_id: nil, application: application)) || base_resource
|
|
1109
|
+
|
|
1110
|
+
parent_klass =
|
|
1111
|
+
parent_resource.name.safe_constantize ||
|
|
1112
|
+
::HasHelpers::ApplicationHelper.translate_klass(parent_resource.name)
|
|
1113
|
+
association_info = inspect_field_and_association(parent_klass, field_name)
|
|
1114
|
+
|
|
1115
|
+
raise AuditError, "The field #{field_name} is not a valid column from the class #{parent_resource.name}" if association_info[0].nil?
|
|
1116
|
+
|
|
1117
|
+
field_class_name = association_info[1]
|
|
1118
|
+
|
|
1119
|
+
scope = ::HasHelpers::Resource.where(name: field_name.to_s, parent: parent_resource, field_class_name: field_class_name)
|
|
1120
|
+
# We eager load field_resource to avoid N+1 queries when we filter in Ruby.
|
|
1121
|
+
matches =
|
|
1122
|
+
scope.
|
|
1123
|
+
includes(:field_resource). # assumes belongs_to :field_resource, class_name: "HasHelpers::Resource", optional: true
|
|
1124
|
+
order(:created_at, :id).
|
|
1125
|
+
to_a
|
|
1126
|
+
|
|
1127
|
+
# Keep only rows where:
|
|
1128
|
+
# - there is no field_resource (nil), or
|
|
1129
|
+
# - the field_resource belongs to the same application as the resource itself.
|
|
1130
|
+
filtered = matches.select do |resource|
|
|
1131
|
+
fr = resource.field_resource
|
|
1132
|
+
fr.nil? || fr.application_id == resource.application_id
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
case filtered.size
|
|
1136
|
+
when 1
|
|
1137
|
+
# Ideal case: a single candidate after sanity filtering
|
|
1138
|
+
filtered.first
|
|
1139
|
+
when 0
|
|
1140
|
+
# Nothing found with these constraints: real error in DSL/DB alignment
|
|
1141
|
+
raise ::ActiveRecord::RecordNotFound,
|
|
1142
|
+
"No field found with name=#{field_name} for parent_ids=#{parent_ids.inspect}"
|
|
1143
|
+
else
|
|
1144
|
+
# With all filters applied we still have multiple rows → duplicated data
|
|
1145
|
+
ids = filtered.map(&:id).join(",")
|
|
1146
|
+
raise AuditError,
|
|
1147
|
+
"Ambiguous field '#{field_name}' for relationship_klass=#{relationship_klass} parent_ids=#{parent_ids.inspect}. " \
|
|
1148
|
+
"Found #{filtered.size} candidates (ids=#{ids}) even after filtering by application. " \
|
|
1149
|
+
"Please clean up duplicated HasHelpers::Resource rows so (name + parent_id + application_id) is unique."
|
|
1150
|
+
end
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# Resolves a resource class name to HasHelpers::Resource.
|
|
1154
|
+
# We assume there is a HasHelpers::Resource record with this name.
|
|
1155
|
+
def find_resource_class(class_name, application_name = nil)
|
|
1156
|
+
scope = ::HasHelpers::Resource.where(name: class_name.to_s, parent_id: nil)
|
|
1157
|
+
|
|
1158
|
+
if application_name.present?
|
|
1159
|
+
app = ::HasHelpers::Application.where("LOWER(name) = ?", application_name.to_s.downcase).first!
|
|
1160
|
+
scope = scope.where(application_id: app.id)
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
scope.first!
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
# Returns [field, association_class_name]
|
|
1167
|
+
# - If `field` maps to a belongs_to association (e.g. created_by_id -> :created_by),
|
|
1168
|
+
# returns the association's class name.
|
|
1169
|
+
# - If it's a normal column, returns [field, nil]
|
|
1170
|
+
# - If it's neither a column nor a belongs_to foreign key, returns [nil, nil]
|
|
1171
|
+
#
|
|
1172
|
+
# Examples:
|
|
1173
|
+
# inspect_field_and_association(Policy, "created_by_id") #=> ["created_by_id", "HasHelpers::User"]
|
|
1174
|
+
# inspect_field_and_association(Policy, "carrier_id") #=> ["carrier_id", "Carrier"]
|
|
1175
|
+
# inspect_field_and_association(Policy, "effective_date")#=> ["effective_date", nil]
|
|
1176
|
+
# inspect_field_and_association(Policy, "nope") #=> [nil, nil]
|
|
1177
|
+
def inspect_field_and_association(klass, field)
|
|
1178
|
+
return [nil, nil] unless klass.is_a?(Class) && field.present?
|
|
1179
|
+
|
|
1180
|
+
field = field.to_s
|
|
1181
|
+
|
|
1182
|
+
# 1) Must be a real column, otherwise it's invalid.
|
|
1183
|
+
return [nil, nil] unless klass.column_names.include?(field)
|
|
1184
|
+
|
|
1185
|
+
# 2) If it's the foreign_key of a belongs_to association, return that association's klass.
|
|
1186
|
+
refl =
|
|
1187
|
+
klass.reflect_on_all_associations(:belongs_to).find do |r|
|
|
1188
|
+
r.foreign_key.to_s == field
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
assoc_class_name =
|
|
1192
|
+
if refl
|
|
1193
|
+
# Prefer ActiveRecord's resolved class if possible, fallback to metadata.
|
|
1194
|
+
begin
|
|
1195
|
+
refl.klass.name
|
|
1196
|
+
rescue NameError
|
|
1197
|
+
refl.class_name
|
|
1198
|
+
end
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
[field, assoc_class_name]
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
# Summary logging
|
|
1205
|
+
# Logs a concise summary of the import operation.
|
|
1206
|
+
# "Logging: use structured logs for audit diff outputs."
|
|
1207
|
+
def log_summary
|
|
1208
|
+
Rails.logger.info(
|
|
1209
|
+
event: "cards_importer.summary",
|
|
1210
|
+
created_cards: result.created_cards,
|
|
1211
|
+
updated_cards: result.updated_cards,
|
|
1212
|
+
created_fields: result.created_fields,
|
|
1213
|
+
updated_fields: result.updated_fields,
|
|
1214
|
+
created_resources: result.created_resources,
|
|
1215
|
+
updated_resources: result.updated_resources,
|
|
1216
|
+
skipped_cards: result.skipped_cards,
|
|
1217
|
+
mode: mode,
|
|
1218
|
+
resource_filter: resource_filter
|
|
1219
|
+
)
|
|
1220
|
+
end
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
end
|