open-loam 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +148 -8
- data/LICENSE +1 -1
- data/README.md +27 -25
- data/app/jobs/{loam → open_loam}/custom_field_reindex_job.rb +6 -6
- data/app/jobs/{loam → open_loam}/event_delivery_job.rb +8 -8
- data/app/jobs/open_loam/event_log_prune_job.rb +15 -0
- data/app/jobs/{loam → open_loam}/event_redelivery_sweep_job.rb +4 -4
- data/app/jobs/{loam → open_loam}/webhook_delivery_job.rb +17 -8
- data/app/models/open_loam/api_token.rb +63 -0
- data/app/models/{loam → open_loam}/audit_record.rb +3 -3
- data/app/models/{loam → open_loam}/auth_attempt.rb +5 -5
- data/app/models/{loam → open_loam}/business_rule.rb +10 -10
- data/app/models/{loam → open_loam}/business_rule_run.rb +5 -5
- data/app/models/{loam → open_loam}/comment.rb +9 -9
- data/app/models/{loam → open_loam}/config.rb +7 -7
- data/app/models/{loam → open_loam}/custom_field_value.rb +5 -5
- data/app/models/{loam → open_loam}/dashboard_widget.rb +5 -5
- data/app/models/open_loam/dictionary.rb +20 -0
- data/app/models/{loam → open_loam}/dictionary_entry.rb +7 -7
- data/app/models/{loam → open_loam}/event_delivery.rb +4 -4
- data/app/models/open_loam/event_record.rb +42 -0
- data/app/models/{loam → open_loam}/field_definition.rb +8 -8
- data/app/models/{loam → open_loam}/inbound_webhook_delivery.rb +4 -4
- data/app/models/{loam → open_loam}/inbound_webhook_source.rb +13 -13
- data/app/models/{loam → open_loam}/membership.rb +6 -6
- data/app/models/{loam → open_loam}/mfa_credential.rb +8 -8
- data/app/models/{loam → open_loam}/notification.rb +10 -10
- data/app/models/{loam → open_loam}/pending_action.rb +70 -26
- data/app/models/{loam → open_loam}/perspective.rb +5 -5
- data/app/models/{loam → open_loam}/progress_job.rb +7 -7
- data/app/models/{loam → open_loam}/record_lock.rb +5 -5
- data/app/models/{loam → open_loam}/scheduled_job.rb +7 -7
- data/app/models/open_loam/search_token.rb +9 -0
- data/app/models/{loam → open_loam}/sso_identity.rb +4 -4
- data/app/models/open_loam/sso_provider.rb +66 -0
- data/app/models/open_loam/tenant.rb +20 -0
- data/app/models/{loam → open_loam}/translation.rb +5 -5
- data/app/models/open_loam/webhook_endpoint.rb +39 -0
- data/app/views/{loam → open_loam}/attachments/_attachments.html.erb +1 -1
- data/app/views/{loam → open_loam}/comments/_comments.html.erb +1 -1
- data/app/views/{loam → open_loam}/custom_fields/_fields.html.erb +6 -2
- data/lib/generators/{loam → open_loam}/entity/entity_generator.rb +9 -9
- data/lib/generators/{loam → open_loam}/entity/templates/api_controller.rb +2 -1
- data/lib/generators/{loam → open_loam}/entity/templates/controller.rb +32 -22
- data/lib/generators/{loam → open_loam}/entity/templates/entity_test.rb +12 -12
- data/lib/generators/{loam → open_loam}/entity/templates/migration.rb +5 -5
- data/lib/generators/{loam → open_loam}/entity/templates/model.rb +10 -10
- data/lib/generators/{loam → open_loam}/entity/templates/policy.rb +1 -1
- data/lib/generators/{loam → open_loam}/entity/templates/views/_form.html.erb +8 -5
- data/lib/generators/{loam → open_loam}/entity/templates/views/deleted.html.erb +7 -7
- data/lib/generators/open_loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/index.html.erb +94 -0
- data/lib/generators/open_loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/show.html.erb +38 -0
- data/lib/generators/{loam → open_loam}/install/install_generator.rb +58 -47
- data/lib/generators/open_loam/install/templates/AGENTS.md +345 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_index.html.erb +1 -1
- data/lib/generators/open_loam/install/templates/admin/api_tokens_controller.rb +48 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_index.html.erb +23 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/base_controller.rb +101 -38
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/comments_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/configs_controller.rb +9 -9
- data/lib/generators/open_loam/install/templates/admin/dashboard_controller.rb +12 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionary_entries_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/events_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/features_controller.rb +9 -9
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_new.html.erb +2 -2
- data/lib/generators/open_loam/install/templates/admin/history_controller.rb +64 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/history_index.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/imports_controller.rb +25 -13
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_new.html.erb +2 -4
- data/lib/generators/open_loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_controller.rb +11 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_controller.rb +5 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_index.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_controller.rb +13 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_index.html.erb +1 -1
- data/lib/generators/open_loam/install/templates/admin/progress_jobs_controller.rb +23 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/progress_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/record_locks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/search_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_controller.rb +33 -34
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_index.html.erb +8 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_new.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_index.html.erb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin.css +1 -1
- data/lib/generators/open_loam/install/templates/api_base_controller.rb +127 -0
- data/lib/generators/{loam → open_loam}/install/templates/guardrails_test.rb +17 -17
- data/lib/generators/open_loam/install/templates/import_job.rb +38 -0
- data/lib/generators/{loam → open_loam}/install/templates/inbound_webhooks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/initializer.rb +87 -75
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_api_tokens.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_audit_records.rb +14 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_auth_attempts.rb → open_loam/install/templates/migrations/create_open_loam_auth_attempts.rb} +3 -3
- data/lib/generators/{loam/install/templates/migrations/create_loam_business_rules.rb → open_loam/install/templates/migrations/create_open_loam_business_rules.rb} +10 -10
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_comments.rb +13 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_configs.rb → open_loam/install/templates/migrations/create_open_loam_configs.rb} +7 -7
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_custom_field_values.rb +19 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dictionaries.rb +11 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_dictionary_entries.rb → open_loam/install/templates/migrations/create_open_loam_dictionary_entries.rb} +6 -6
- data/lib/generators/{loam/install/templates/migrations/create_loam_event_deliveries.rb → open_loam/install/templates/migrations/create_open_loam_event_deliveries.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_event_records.rb +13 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_field_definitions.rb → open_loam/install/templates/migrations/create_open_loam_field_definitions.rb} +4 -4
- data/lib/generators/{loam/install/templates/migrations/create_loam_inbound_webhooks.rb → open_loam/install/templates/migrations/create_open_loam_inbound_webhooks.rb} +9 -10
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_memberships.rb +11 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_mfa_credentials.rb → open_loam/install/templates/migrations/create_open_loam_mfa_credentials.rb} +4 -4
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_notifications.rb +15 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_pending_actions.rb → open_loam/install/templates/migrations/create_open_loam_pending_actions.rb} +9 -9
- data/lib/generators/{loam/install/templates/migrations/create_loam_perspectives.rb → open_loam/install/templates/migrations/create_open_loam_perspectives.rb} +5 -5
- data/lib/generators/{loam/install/templates/migrations/create_loam_progress_jobs.rb → open_loam/install/templates/migrations/create_open_loam_progress_jobs.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_record_locks.rb +16 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_scheduled_jobs.rb → open_loam/install/templates/migrations/create_open_loam_scheduled_jobs.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_search_tokens.rb +18 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_sso_providers.rb → open_loam/install/templates/migrations/create_open_loam_sso_providers.rb} +15 -12
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_tenants.rb +9 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_translations.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/{loam → open_loam}/install/templates/migrations/create_users.rb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/user.rb +5 -5
- data/lib/generators/{loam → open_loam}/primary_key_options.rb +23 -23
- data/lib/open-loam.rb +2 -2
- data/lib/{loam → open_loam}/attachable.rb +4 -4
- data/lib/{loam → open_loam}/auditable.rb +23 -23
- data/lib/{loam → open_loam}/auth_throttle.rb +20 -12
- data/lib/{loam → open_loam}/base32.rb +1 -1
- data/lib/{loam → open_loam}/bulk.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/actions.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/condition.rb +5 -5
- data/lib/{loam → open_loam}/business_rules.rb +19 -19
- data/lib/{loam → open_loam}/commentable.rb +6 -6
- data/lib/{loam → open_loam}/configs.rb +19 -19
- data/lib/{loam → open_loam}/cron.rb +2 -2
- data/lib/{loam → open_loam}/csv.rb +2 -2
- data/lib/{loam → open_loam}/current.rb +5 -5
- data/lib/{loam → open_loam}/custom_field_index.rb +33 -30
- data/lib/{loam → open_loam}/custom_fields.rb +17 -17
- data/lib/{loam → open_loam}/dashboard.rb +5 -5
- data/lib/{loam → open_loam}/dictionaries.rb +12 -12
- data/lib/{loam → open_loam}/durable_events.rb +19 -19
- data/lib/{loam → open_loam}/encryptable.rb +39 -34
- data/lib/{loam → open_loam}/encryption/cipher.rb +3 -3
- data/lib/{loam → open_loam}/encryption/key_provider.rb +19 -3
- data/lib/{loam → open_loam}/encryption.rb +58 -15
- data/lib/open_loam/engine.rb +65 -0
- data/lib/{loam → open_loam}/enrichers.rb +5 -5
- data/lib/{loam → open_loam}/errors.rb +20 -10
- data/lib/{loam → open_loam}/eval.rb +2 -2
- data/lib/open_loam/event_log.rb +77 -0
- data/lib/{loam → open_loam}/event_stream.rb +14 -14
- data/lib/{loam → open_loam}/eventful.rb +12 -12
- data/lib/{loam → open_loam}/events.rb +5 -5
- data/lib/{loam → open_loam}/export.rb +12 -12
- data/lib/{loam → open_loam}/features.rb +15 -15
- data/lib/{loam → open_loam}/generated_key.rb +6 -6
- data/lib/{loam → open_loam}/import.rb +12 -15
- data/lib/{loam → open_loam}/inbound_webhooks.rb +19 -18
- data/lib/{loam → open_loam}/lifecycle.rb +54 -25
- data/lib/{loam/locales/loam.en.yml → open_loam/locales/open_loam.en.yml} +5 -5
- data/lib/{loam → open_loam}/mcp/server.rb +9 -9
- data/lib/{loam → open_loam}/mcp.rb +31 -22
- data/lib/{loam → open_loam}/notifications.rb +5 -5
- data/lib/{loam → open_loam}/open_api.rb +16 -16
- data/lib/open_loam/outbound_url.rb +100 -0
- data/lib/{loam → open_loam}/overrides.rb +10 -10
- data/lib/{loam → open_loam}/pending_actions.rb +9 -9
- data/lib/{loam → open_loam}/permissions.rb +8 -8
- data/lib/{loam → open_loam}/perspectives.rb +9 -9
- data/lib/{loam → open_loam}/policy.rb +21 -8
- data/lib/{loam → open_loam}/progress.rb +6 -6
- data/lib/{loam → open_loam}/record_locks.rb +9 -9
- data/lib/{loam → open_loam}/scheduler.rb +20 -20
- data/lib/{loam → open_loam}/search/driver.rb +3 -3
- data/lib/{loam → open_loam}/search/like_driver.rb +5 -5
- data/lib/{loam → open_loam}/search/token_driver.rb +16 -16
- data/lib/{loam → open_loam}/search.rb +5 -5
- data/lib/{loam → open_loam}/searchable.rb +22 -22
- data/lib/{loam → open_loam}/soft_deletable.rb +10 -10
- data/lib/{loam → open_loam}/sso/claims.rb +1 -1
- data/lib/{loam → open_loam}/sso/fake_provider.rb +1 -1
- data/lib/open_loam/sso/http_client.rb +51 -0
- data/lib/{loam → open_loam}/sso/oidc_provider.rb +2 -2
- data/lib/{loam → open_loam}/sso.rb +39 -18
- data/lib/{loam → open_loam}/telemetry.rb +6 -6
- data/lib/{loam → open_loam}/tenant_record.rb +10 -10
- data/lib/{loam → open_loam}/test_helpers.rb +4 -4
- data/lib/{loam → open_loam}/totp.rb +3 -3
- data/lib/{loam → open_loam}/translatable.rb +24 -24
- data/lib/{loam → open_loam}/undo.rb +11 -11
- data/lib/open_loam/version.rb +3 -0
- data/lib/{loam → open_loam}/webhooks.rb +4 -4
- data/lib/{loam → open_loam}/widgets.rb +9 -9
- data/lib/{loam → open_loam}/workflow.rb +45 -45
- data/lib/open_loam.rb +124 -0
- data/lib/tasks/open_loam.rake +203 -0
- data/lib/tasks/{loam_eval.rake → open_loam_eval.rake} +7 -7
- data/lib/tasks/open_loam_mcp.rake +20 -0
- metadata +247 -242
- data/app/models/loam/api_token.rb +0 -40
- data/app/models/loam/dictionary.rb +0 -20
- data/app/models/loam/search_token.rb +0 -9
- data/app/models/loam/sso_provider.rb +0 -35
- data/app/models/loam/tenant.rb +0 -20
- data/app/models/loam/webhook_endpoint.rb +0 -26
- data/lib/generators/loam/entity/templates/views/edit.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/index.html.erb +0 -90
- data/lib/generators/loam/entity/templates/views/new.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/show.html.erb +0 -36
- data/lib/generators/loam/install/templates/AGENTS.md +0 -341
- data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +0 -30
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +0 -10
- data/lib/generators/loam/install/templates/admin/history_controller.rb +0 -38
- data/lib/generators/loam/install/templates/admin/layout.html.erb +0 -76
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +0 -19
- data/lib/generators/loam/install/templates/api_base_controller.rb +0 -90
- data/lib/generators/loam/install/templates/import_job.rb +0 -25
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +0 -14
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +0 -13
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +0 -19
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +0 -12
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +0 -16
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +0 -18
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +0 -9
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +0 -13
- data/lib/loam/engine.rb +0 -57
- data/lib/loam/sso/http_client.rb +0 -42
- data/lib/loam/version.rb +0 -3
- data/lib/loam.rb +0 -122
- data/lib/tasks/loam.rake +0 -171
- data/lib/tasks/loam_mcp.rake +0 -20
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/features_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_preview.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_summary.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_activated.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_show.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pagination.rb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/search_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_mfa_challenge.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_new.html.erb +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# The one way to read and write per-tenant settings.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# OpenLoam::Configs.get("rental.late_fee_per_day") # resolved for the current tenant
|
|
5
|
+
# OpenLoam::Configs.set("rental.late_fee_per_day", 45) # this tenant's override
|
|
6
|
+
# OpenLoam::Configs.set("rental.currency", "PLN", scope: :global)
|
|
7
|
+
# OpenLoam::Configs.reset("rental.late_fee_per_day") # drop the override → fall back
|
|
8
8
|
#
|
|
9
9
|
# Resolution order, most specific first:
|
|
10
10
|
# 1. the current tenant's OVERRIDE row (tenant_id = current tenant)
|
|
11
11
|
# 2. the GLOBAL row (tenant_id NULL)
|
|
12
|
-
# 3. the declared default in
|
|
12
|
+
# 3. the declared default in OpenLoam.config_defaults[key]
|
|
13
13
|
# 4. the `default:` argument (nil)
|
|
14
14
|
#
|
|
15
|
-
# Reads are memoized per request in
|
|
15
|
+
# Reads are memoized per request in OpenLoam::Current.config_cache, keyed by
|
|
16
16
|
# [key, tenant_id], and the whole cache is dropped on any write. That is the
|
|
17
17
|
# deliberately-simple prototype cache; a Rails.cache-backed layer shared across
|
|
18
18
|
# requests is the scaling path, and it would slot in behind this same API.
|
|
@@ -36,7 +36,7 @@ module Loam
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Write a setting. scope: :tenant writes the current tenant's override and
|
|
39
|
-
# requires a tenant in context (a missing one raises, like every
|
|
39
|
+
# requires a tenant in context (a missing one raises, like every OpenLoam
|
|
40
40
|
# write); scope: :global writes the app-wide row (tenant_id NULL). Upsert.
|
|
41
41
|
def set(key, value, scope: :tenant)
|
|
42
42
|
key = key.to_s
|
|
@@ -66,8 +66,8 @@ module Loam
|
|
|
66
66
|
# defaults, plus any global or current-tenant rows already written. Sorted
|
|
67
67
|
# and de-duplicated, for the settings screen.
|
|
68
68
|
def defined_keys
|
|
69
|
-
rows =
|
|
70
|
-
(
|
|
69
|
+
rows = OpenLoam::Config.where(tenant_id: [nil, OpenLoam.tenant&.id]).distinct.pluck(:key)
|
|
70
|
+
(OpenLoam.config_defaults.keys + rows).map(&:to_s).uniq.sort
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
private
|
|
@@ -76,39 +76,39 @@ module Loam
|
|
|
76
76
|
# the caller then applies its own `default:` without caching it.
|
|
77
77
|
def resolve(key)
|
|
78
78
|
if (tenant_id = current_tenant_id)
|
|
79
|
-
override =
|
|
79
|
+
override = OpenLoam::Config.find_by(key: key, tenant_id: tenant_id)
|
|
80
80
|
return [true, override.value_json] if override
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
global =
|
|
83
|
+
global = OpenLoam::Config.find_by(key: key, tenant_id: nil)
|
|
84
84
|
return [true, global.value_json] if global
|
|
85
85
|
|
|
86
|
-
return [true,
|
|
86
|
+
return [true, OpenLoam.config_defaults[key]] if OpenLoam.config_defaults.key?(key)
|
|
87
87
|
|
|
88
88
|
[false, nil]
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# The row(s) a write/read targets. :global is the tenant_id NULL row;
|
|
92
92
|
# :tenant is the current tenant's — which requires a tenant in context,
|
|
93
|
-
# so
|
|
93
|
+
# so OpenLoam.tenant! raises MissingTenantError when there is none.
|
|
94
94
|
def row_for(key, scope)
|
|
95
95
|
case scope
|
|
96
|
-
when :global then
|
|
97
|
-
when :tenant then
|
|
96
|
+
when :global then OpenLoam::Config.where(key: key, tenant_id: nil)
|
|
97
|
+
when :tenant then OpenLoam::Config.where(key: key, tenant_id: OpenLoam.tenant!.id)
|
|
98
98
|
else raise ArgumentError, "unknown config scope #{scope.inspect} (use :tenant or :global)"
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def current_tenant_id
|
|
103
|
-
|
|
103
|
+
OpenLoam.tenant&.id
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def cache
|
|
107
|
-
|
|
107
|
+
OpenLoam::Current.config_cache ||= {}
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def clear_cache
|
|
111
|
-
|
|
111
|
+
OpenLoam::Current.config_cache = {}
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require "set"
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module OpenLoam
|
|
4
4
|
# A small, dependency-free "next run" calculator for a standard 5-field cron
|
|
5
5
|
# expression ("min hour dom month dow") plus an "interval:N" (every N seconds)
|
|
6
|
-
# form — same stdlib-only spirit as
|
|
6
|
+
# form — same stdlib-only spirit as OpenLoam::Totp / OpenLoam::Base32. Timezone-aware;
|
|
7
7
|
# always returns UTC. Supports `*`, lists (`1,15`), ranges (`1-5`), and steps
|
|
8
8
|
# (`*/15`, `0-30/10`), and the classic cron day-of-month / day-of-week OR quirk
|
|
9
9
|
# (when BOTH are restricted, a match on either fires).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# CSV formula-injection defense. A cell whose value starts with =, +, -, @, or
|
|
3
3
|
# a tab/CR is treated as a FORMULA by Excel/Google Sheets when the file is
|
|
4
4
|
# opened — so `=cmd|...` or `=HYPERLINK(...)` in imported/exported data can run.
|
|
5
5
|
# Prefixing such a value with a single quote neutralizes it while leaving the
|
|
6
|
-
# visible text intact. Applied on every CSV cell
|
|
6
|
+
# visible text intact. Applied on every CSV cell OpenLoam writes (export + the
|
|
7
7
|
# import error file).
|
|
8
8
|
module Csv
|
|
9
9
|
DANGEROUS = /\A[=+\-@\t\r]/
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
require "active_support/current_attributes"
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module OpenLoam
|
|
4
4
|
# Per-request (and per-job) execution context. Every tenant-scoped query,
|
|
5
5
|
# audit record, and published event reads from here.
|
|
6
6
|
class Current < ActiveSupport::CurrentAttributes
|
|
7
|
-
# config_cache memoizes resolved
|
|
7
|
+
# config_cache memoizes resolved OpenLoam::Configs values for the duration of
|
|
8
8
|
# one request/job, keyed by [key, tenant_id]. It resets with everything else
|
|
9
9
|
# here, so a value can never be read stale across requests.
|
|
10
10
|
#
|
|
11
|
-
# mutation_mode is the approval-gate seam (
|
|
11
|
+
# mutation_mode is the approval-gate seam (OpenLoam::PendingActions): nil/:direct
|
|
12
12
|
# means writes commit normally; :confirm means a caller (an MCP tool acting
|
|
13
13
|
# for an AI agent) stages writes for human approval instead. It lives here so
|
|
14
14
|
# it is per-request and resets automatically.
|
|
15
|
-
# dictionary_cache memoizes resolved
|
|
15
|
+
# dictionary_cache memoizes resolved OpenLoam::Dictionaries lookups for one
|
|
16
16
|
# request/job, keyed by [kind, key, tenant_id] — same posture as config_cache.
|
|
17
|
-
# locale is the current content-translation language (
|
|
17
|
+
# locale is the current content-translation language (OpenLoam::Translatable) —
|
|
18
18
|
# request state like tenant/actor, set in a before_action and reset here.
|
|
19
19
|
attribute :tenant, :actor, :config_cache, :mutation_mode, :dictionary_cache, :locale
|
|
20
20
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
module
|
|
2
|
-
# The read-model index for custom fields (
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# The read-model index for custom fields (OpenLoam::CustomFieldValue) — a typed-EAV
|
|
3
3
|
# projection that makes custom-field filter/sort/search index-backed.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# OpenLoam::CustomFieldIndex.filter(DamageReport, "severity", "eq", "critical") # a relation
|
|
6
|
+
# OpenLoam::CustomFieldIndex.order(DamageReport, "severity", :asc)
|
|
7
|
+
# OpenLoam::CustomFieldIndex.reindex(DamageReport) # rebuild
|
|
8
8
|
#
|
|
9
|
-
# SAFETY: `field_key` must name a real
|
|
9
|
+
# SAFETY: `field_key` must name a real OpenLoam::FieldDefinition (no arbitrary
|
|
10
10
|
# keys); values are cast to the declared type; queries are tenant-scoped
|
|
11
11
|
# throughout (index rows carry tenant_id, the model relation is default-scoped),
|
|
12
12
|
# so a filter can't reach across tenants. Encrypted data lives in real columns,
|
|
@@ -16,7 +16,7 @@ module Loam
|
|
|
16
16
|
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
|
-
# --- maintenance (called from
|
|
19
|
+
# --- maintenance (called from OpenLoam::CustomFields) ---
|
|
20
20
|
|
|
21
21
|
# Re-project one record's custom fields (delete + insert). Soft-delete keeps
|
|
22
22
|
# the rows (the filter's base scope hides the record anyway — the same
|
|
@@ -26,18 +26,18 @@ module Loam
|
|
|
26
26
|
return unless model.respond_to?(:custom_field_definitions)
|
|
27
27
|
|
|
28
28
|
type = index_type(model)
|
|
29
|
-
|
|
29
|
+
OpenLoam::CustomFieldValue.where(indexable_type: type, indexable_id: record.id).delete_all
|
|
30
30
|
|
|
31
31
|
rows = model.custom_field_definitions.filter_map { |definition| row_for(record, definition, type) }
|
|
32
|
-
|
|
32
|
+
OpenLoam::CustomFieldValue.insert_all(rows) if rows.any?
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def remove(record)
|
|
36
|
-
|
|
36
|
+
OpenLoam::CustomFieldValue.where(indexable_type: index_type(record.class), indexable_id: record.id).delete_all
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def reindex(model)
|
|
40
|
-
|
|
40
|
+
OpenLoam::CustomFieldValue.where(indexable_type: index_type(model)).delete_all
|
|
41
41
|
model.find_each { |record| project(record) }
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ module Loam
|
|
|
47
47
|
field_key = field_key.to_s
|
|
48
48
|
refuse_unknown_field!(model, field_key)
|
|
49
49
|
refuse_unreadable_field!(model, field_key)
|
|
50
|
-
raise
|
|
50
|
+
raise OpenLoam::Error, "unknown custom-field op #{op.inspect}" unless OPS.include?(op.to_s)
|
|
51
51
|
|
|
52
52
|
if covered?(model, field_key)
|
|
53
53
|
self.partial = false
|
|
@@ -89,12 +89,12 @@ module Loam
|
|
|
89
89
|
self.partial = true
|
|
90
90
|
schedule_reindex(model)
|
|
91
91
|
end
|
|
92
|
-
table =
|
|
92
|
+
table = OpenLoam::CustomFieldValue.table_name
|
|
93
93
|
conn = model.connection
|
|
94
94
|
join = "LEFT JOIN #{table} ON #{table}.indexable_type = #{conn.quote(index_type(model))} " \
|
|
95
95
|
"AND #{table}.indexable_id = #{model.table_name}.id " \
|
|
96
96
|
"AND #{table}.field_key = #{conn.quote(field_key.to_s)} " \
|
|
97
|
-
"AND #{table}.tenant_id = #{
|
|
97
|
+
"AND #{table}.tenant_id = #{conn.quote(OpenLoam.tenant!.id)}"
|
|
98
98
|
model.joins(join).order(Arel.sql("#{table}.value_text #{dir.to_s.casecmp('desc').zero? ? 'DESC' : 'ASC'}"))
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -132,22 +132,22 @@ module Loam
|
|
|
132
132
|
# Was the last filter/order served over an incomplete index? The admin surfaces
|
|
133
133
|
# this as an honest "results may be incomplete, reindexing…".
|
|
134
134
|
def partial?
|
|
135
|
-
Thread.current[:
|
|
135
|
+
Thread.current[:open_loam_index_partial] == true
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def partial=(value)
|
|
139
|
-
Thread.current[:
|
|
139
|
+
Thread.current[:open_loam_index_partial] = value
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# Enqueue a background reindex to heal a gap — DEDUPED so a hot gappy field
|
|
143
143
|
# doesn't enqueue one per request. In-process dedup here (single-process
|
|
144
144
|
# prototype); a DB/cache marker is the multi-process path.
|
|
145
145
|
def schedule_reindex(model)
|
|
146
|
-
key = [
|
|
146
|
+
key = [ OpenLoam.tenant!.id, model.base_class.name ]
|
|
147
147
|
return if pending_reindex.include?(key)
|
|
148
148
|
|
|
149
149
|
pending_reindex << key
|
|
150
|
-
|
|
150
|
+
OpenLoam::CustomFieldReindexJob.perform_later(OpenLoam.tenant!.id, model.base_class.name)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
def clear_pending(tenant_id, model_name)
|
|
@@ -188,7 +188,7 @@ module Loam
|
|
|
188
188
|
# --- internals ---
|
|
189
189
|
|
|
190
190
|
def base(model)
|
|
191
|
-
|
|
191
|
+
OpenLoam::CustomFieldValue.where(indexable_type: index_type(model)) # tenant-scoped by default_scope
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
def index_type(model_or_class)
|
|
@@ -202,16 +202,19 @@ module Loam
|
|
|
202
202
|
when "neq" then rows.where.not(value_text: value.to_s)
|
|
203
203
|
when "contains" then rows.where("value_text LIKE ?", "%#{sanitize_like(value)}%")
|
|
204
204
|
when "present" then rows.where.not(value_text: [ nil, "" ])
|
|
205
|
-
when "gt"
|
|
206
|
-
when "gte" then numeric(rows, value, ">=")
|
|
207
|
-
when "lt" then numeric(rows, value, "<")
|
|
208
|
-
when "lte" then numeric(rows, value, "<=")
|
|
205
|
+
when "gt", "gte", "lt", "lte" then numeric(rows, value, op)
|
|
209
206
|
end
|
|
210
207
|
end
|
|
211
208
|
|
|
212
|
-
# Numeric comparisons hit the indexed value_number column.
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
# Numeric comparisons hit the indexed value_number column. Built through
|
|
210
|
+
# Arel rather than an interpolated fragment: `op` reaches here from
|
|
211
|
+
# params[:cf_op], and a comparison operator cannot be a bound parameter, so
|
|
212
|
+
# the only safe form is one that never becomes a string.
|
|
213
|
+
NUMERIC_PREDICATES = { "gt" => :gt, "gte" => :gteq, "lt" => :lt, "lte" => :lteq }.freeze
|
|
214
|
+
|
|
215
|
+
def numeric(rows, value, op)
|
|
216
|
+
column = OpenLoam::CustomFieldValue.arel_table[:value_number]
|
|
217
|
+
rows.where(column.public_send(NUMERIC_PREDICATES.fetch(op), value.to_f))
|
|
215
218
|
end
|
|
216
219
|
|
|
217
220
|
def row_for(record, definition, type)
|
|
@@ -235,7 +238,7 @@ module Loam
|
|
|
235
238
|
|
|
236
239
|
def refuse_unknown_field!(model, field_key)
|
|
237
240
|
known = model.respond_to?(:custom_field_definitions) && model.custom_field_definitions.exists?(name: field_key)
|
|
238
|
-
raise
|
|
241
|
+
raise OpenLoam::Error, "no custom field #{field_key.inspect} on #{model.name}" unless known
|
|
239
242
|
end
|
|
240
243
|
|
|
241
244
|
# THE oracle guard: filtering or sorting on a custom field the current role
|
|
@@ -243,16 +246,16 @@ module Loam
|
|
|
243
246
|
# records match. Enforced against the current actor's role; a system/background
|
|
244
247
|
# context (no actor — reindex, a business rule) is trusted and not gated.
|
|
245
248
|
def refuse_unreadable_field!(model, field_key)
|
|
246
|
-
actor =
|
|
249
|
+
actor = OpenLoam::Current.actor
|
|
247
250
|
return if actor.nil?
|
|
248
251
|
|
|
249
252
|
definition = model.custom_field_definitions.find_by(name: field_key.to_s)
|
|
250
253
|
return if definition.nil? # unknown field is refuse_unknown_field!'s job
|
|
251
254
|
|
|
252
|
-
role =
|
|
255
|
+
role = OpenLoam::Membership.find_by(user_id: actor.id)&.role
|
|
253
256
|
return if definition.readable_by?(role)
|
|
254
257
|
|
|
255
|
-
raise
|
|
258
|
+
raise OpenLoam::FieldAccessError, "custom field #{field_key.inspect} is not readable by #{role.inspect}"
|
|
256
259
|
end
|
|
257
260
|
|
|
258
261
|
def sanitize_like(value)
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Runtime, migration-free fields — stored in the `custom_fields` json
|
|
3
3
|
# column every generated entity table carries. Included automatically by
|
|
4
|
-
# `rails g
|
|
5
|
-
#
|
|
4
|
+
# `rails g open_loam:entity`. Shape is declared at runtime via a
|
|
5
|
+
# OpenLoam::FieldDefinition (typically through the admin "Field definitions"
|
|
6
6
|
# screen), not in code — that's what makes it migration-free.
|
|
7
7
|
#
|
|
8
8
|
# equipment.custom_field(:serial_number) # => "SN-123" (cast)
|
|
9
9
|
# equipment.set_custom_field(:serial_number, "SN-123")
|
|
10
10
|
#
|
|
11
|
-
# Reading/writing a name with no matching
|
|
12
|
-
# tenant + entity raises
|
|
13
|
-
# same "fail loudly at the access site" philosophy as
|
|
11
|
+
# Reading/writing a name with no matching OpenLoam::FieldDefinition for this
|
|
12
|
+
# tenant + entity raises OpenLoam::UnknownCustomFieldError immediately — the
|
|
13
|
+
# same "fail loudly at the access site" philosophy as OpenLoam.tenant!.
|
|
14
14
|
module CustomFields
|
|
15
15
|
extend ActiveSupport::Concern
|
|
16
16
|
|
|
17
17
|
included do
|
|
18
|
-
# Keep the read-model index (
|
|
18
|
+
# Keep the read-model index (OpenLoam::CustomFieldIndex) current so custom-field
|
|
19
19
|
# filter/sort is index-backed. Re-project only when the custom_fields column
|
|
20
20
|
# actually changed (a soft-delete touches only deleted_at, so the rows stay
|
|
21
21
|
# and the base scope hides the record — the L-912 posture). A no-op for a
|
|
22
|
-
# model with no custom_fields column. Guarded by respond_to? so
|
|
22
|
+
# model with no custom_fields column. Guarded by respond_to? so OpenLoam::
|
|
23
23
|
# CustomFields stays includable in a plain class for a DSL-only test.
|
|
24
24
|
if respond_to?(:after_save)
|
|
25
|
-
after_save :
|
|
26
|
-
after_destroy :
|
|
25
|
+
after_save :open_loam_reindex_custom_fields, if: :open_loam_custom_fields_dirty?
|
|
26
|
+
after_destroy :open_loam_deindex_custom_fields
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
class_methods do
|
|
31
31
|
def custom_field_definitions
|
|
32
|
-
|
|
32
|
+
OpenLoam::FieldDefinition.where(entity_type: name)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -53,15 +53,15 @@ module Loam
|
|
|
53
53
|
value = custom_field(name)
|
|
54
54
|
return value unless definition.field_type == "dictionary"
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
OpenLoam::Dictionaries.label_for(definition.dictionary_key, value)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
private
|
|
60
60
|
|
|
61
|
-
def
|
|
62
|
-
def
|
|
61
|
+
def open_loam_reindex_custom_fields = OpenLoam::CustomFieldIndex.project(self)
|
|
62
|
+
def open_loam_deindex_custom_fields = OpenLoam::CustomFieldIndex.remove(self)
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def open_loam_custom_fields_dirty?
|
|
65
65
|
return false unless self.class.respond_to?(:custom_field_definitions)
|
|
66
66
|
|
|
67
67
|
# On create every column counts as changed; on update, only re-project when
|
|
@@ -71,8 +71,8 @@ module Loam
|
|
|
71
71
|
|
|
72
72
|
def custom_field_definition!(name)
|
|
73
73
|
custom_field_definitions_by_name.fetch(name.to_s) do
|
|
74
|
-
raise UnknownCustomFieldError, "No
|
|
75
|
-
"(tenant #{
|
|
74
|
+
raise UnknownCustomFieldError, "No OpenLoam::FieldDefinition named #{name.inspect} for #{self.class.name} " \
|
|
75
|
+
"(tenant #{OpenLoam.tenant&.id.inspect})"
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Resolves the admin dashboard for the current tenant/actor: the ordered,
|
|
3
3
|
# role-visible, computed widgets to show. Uses the tenant's configured
|
|
4
|
-
#
|
|
4
|
+
# OpenLoam::DashboardWidget rows (a manager arranges them); falls back to every
|
|
5
5
|
# registered widget in registration order when the tenant has configured none,
|
|
6
6
|
# so the dashboard is useful out of the box.
|
|
7
7
|
module Dashboard
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def for(actor:, role:)
|
|
11
|
-
configured =
|
|
12
|
-
keys = configured.presence ||
|
|
11
|
+
configured = OpenLoam::DashboardWidget.active.ordered.pluck(:widget_key)
|
|
12
|
+
keys = configured.presence || OpenLoam::Widgets.keys
|
|
13
13
|
|
|
14
|
-
keys.filter_map { |key|
|
|
14
|
+
keys.filter_map { |key| OpenLoam::Widgets.resolve(key, actor: actor, role: role) }
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Read API for per-tenant managed lookup lists (
|
|
3
|
-
#
|
|
4
|
-
# memoized per request in
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Read API for per-tenant managed lookup lists (OpenLoam::Dictionary /
|
|
3
|
+
# OpenLoam::DictionaryEntry). Everything is tenant-scoped automatically and
|
|
4
|
+
# memoized per request in OpenLoam::Current.dictionary_cache (keyed with the tenant
|
|
5
5
|
# id, so an as_tenant switch mid-request can't read another tenant's list) —
|
|
6
|
-
# the same simple prototype cache as
|
|
6
|
+
# the same simple prototype cache as OpenLoam::Configs.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# OpenLoam::Dictionaries.entries("damage_severity") # active, ordered
|
|
9
|
+
# OpenLoam::Dictionaries.default("damage_severity") # the default entry
|
|
10
|
+
# OpenLoam::Dictionaries.label_for("damage_severity", "critical") # => "Critical"
|
|
11
11
|
module Dictionaries
|
|
12
12
|
class << self
|
|
13
13
|
# The dictionary for the current tenant, or nil.
|
|
14
14
|
def get(key)
|
|
15
|
-
memo([:dict, key.to_s]) {
|
|
15
|
+
memo([:dict, key.to_s]) { OpenLoam::Dictionary.find_by(key: key.to_s) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Active entries, ordered by position. Empty for an unknown key.
|
|
@@ -38,14 +38,14 @@ module Loam
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def clear_cache
|
|
41
|
-
|
|
41
|
+
OpenLoam::Current.dictionary_cache = {}
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
46
|
def memo(subkey)
|
|
47
|
-
store = (
|
|
48
|
-
cache_key = subkey + [
|
|
47
|
+
store = (OpenLoam::Current.dictionary_cache ||= {})
|
|
48
|
+
cache_key = subkey + [ OpenLoam.tenant&.id ]
|
|
49
49
|
return store[cache_key] if store.key?(cache_key)
|
|
50
50
|
|
|
51
51
|
store[cache_key] = yield
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Durable (persistent) event subscribers — the twin of
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Durable (persistent) event subscribers — the twin of OpenLoam::Events.subscribe,
|
|
3
3
|
# and the formal contract L-706 pins down.
|
|
4
4
|
#
|
|
5
5
|
# THE CONTRACT
|
|
6
|
-
# * Ephemeral —
|
|
6
|
+
# * Ephemeral — OpenLoam::Events.subscribe(&block): runs INLINE in the publisher's
|
|
7
7
|
# thread, synchronously, best-effort. An exception in the block PROPAGATES
|
|
8
8
|
# into whatever published the event. No persistence, no retry. Right for
|
|
9
9
|
# cheap in-process fan-out (the webhook dispatcher enqueues its own jobs
|
|
10
10
|
# this way).
|
|
11
|
-
# * Persistent —
|
|
12
|
-
#
|
|
11
|
+
# * Persistent — OpenLoam::DurableEvents.register(...): publishing commits a
|
|
12
|
+
# OpenLoam::EventDelivery row in the event's tenant, then hands the handler to a
|
|
13
13
|
# background job. The handler's exception is CONTAINED in the job; delivery
|
|
14
14
|
# is retried with backoff and, past MAX_ATTEMPTS, parked as `dead` for an
|
|
15
15
|
# operator (Admin::EventDeliveriesController).
|
|
@@ -29,10 +29,10 @@ module Loam
|
|
|
29
29
|
MAX_ATTEMPTS = 5
|
|
30
30
|
# Backoff (seconds) indexed by attempt number; the last value is the cap.
|
|
31
31
|
BACKOFF = [ 0, 60, 300, 1800, 7200 ].freeze
|
|
32
|
-
SWEEP_KEY = "
|
|
32
|
+
SWEEP_KEY = "open_loam_event_redelivery_sweep".freeze
|
|
33
33
|
|
|
34
34
|
class << self
|
|
35
|
-
# --- declarative registry (mirrors
|
|
35
|
+
# --- declarative registry (mirrors OpenLoam::Scheduler.register) ---
|
|
36
36
|
|
|
37
37
|
# key: a stable identifier stored on every delivery row it produces.
|
|
38
38
|
# to: an event name ("billing.invoice.paid") or a domain prefix
|
|
@@ -51,7 +51,7 @@ module Loam
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def subscribers_for(event_name)
|
|
54
|
-
registered.select { |entry|
|
|
54
|
+
registered.select { |entry| OpenLoam::Events.pattern_matches?(entry[:pattern], event_name) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def handler_for(key)
|
|
@@ -59,19 +59,19 @@ module Loam
|
|
|
59
59
|
entry && resolve(entry[:handler])
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# --- wiring (mirrors
|
|
62
|
+
# --- wiring (mirrors OpenLoam::Webhooks.subscribe!) ---
|
|
63
63
|
|
|
64
|
-
# Wired once from
|
|
64
|
+
# Wired once from OpenLoam::Engine. Idempotent: subscribing twice would persist
|
|
65
65
|
# every event twice.
|
|
66
66
|
def subscribe!
|
|
67
|
-
@subscription ||=
|
|
67
|
+
@subscription ||= OpenLoam::Events.subscribe_all { |event_name, payload| capture(event_name, payload) }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Persist a delivery row per matching durable subscriber, in the event's
|
|
71
71
|
# tenant, then nudge a job per row. The ROW is the durable record; the job
|
|
72
72
|
# is only the accelerator (the sweep redelivers rows whose job was lost).
|
|
73
73
|
def capture(event_name, payload)
|
|
74
|
-
tenant =
|
|
74
|
+
tenant = OpenLoam::Tenant.find_by(id: payload[:tenant_id])
|
|
75
75
|
return if tenant.nil? # nil-tenant events are not durably delivered (as Webhooks.dispatch)
|
|
76
76
|
|
|
77
77
|
matches = subscribers_for(event_name)
|
|
@@ -81,18 +81,18 @@ module Loam
|
|
|
81
81
|
# scalars by convention, never records (same rule as the webhook path).
|
|
82
82
|
deliverable = payload.transform_keys(&:to_s)
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
OpenLoam.as_tenant(tenant) do
|
|
85
85
|
matches.each do |sub|
|
|
86
|
-
delivery =
|
|
86
|
+
delivery = OpenLoam::EventDelivery.create!(
|
|
87
87
|
subscriber_key: sub[:key], event_name: event_name.to_s,
|
|
88
88
|
payload: deliverable, status: "pending", attempts: 0
|
|
89
89
|
)
|
|
90
|
-
|
|
90
|
+
OpenLoam::EventDeliveryJob.perform_later(tenant.id, delivery.id)
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
# --- delivery (called by
|
|
95
|
+
# --- delivery (called by OpenLoam::EventDeliveryJob) ---
|
|
96
96
|
|
|
97
97
|
# Run one delivery and advance the row's state. Manual, ROW-STATE retries —
|
|
98
98
|
# deliberately NOT ActiveJob retry_on, which keeps retry state in the queue
|
|
@@ -102,7 +102,7 @@ module Loam
|
|
|
102
102
|
return unless delivery.status == "pending"
|
|
103
103
|
return if delivery.next_attempt_at && delivery.next_attempt_at > now # duplicate nudge, backoff not elapsed
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
OpenLoam::Telemetry.span("durable_event_delivery",
|
|
106
106
|
subscriber_key: delivery.subscriber_key, event_name: delivery.event_name) do
|
|
107
107
|
run_delivery(delivery, now)
|
|
108
108
|
end
|
|
@@ -136,8 +136,8 @@ module Loam
|
|
|
136
136
|
# no cross-tenant scan is needed.
|
|
137
137
|
def redeliver_stuck(now: Time.current, limit: 500)
|
|
138
138
|
count = 0
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
OpenLoam::EventDelivery.due(now).limit(limit).find_each do |delivery|
|
|
140
|
+
OpenLoam::EventDeliveryJob.perform_later(delivery.tenant_id, delivery.id)
|
|
141
141
|
count += 1
|
|
142
142
|
end
|
|
143
143
|
count
|