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,19 +1,19 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Staging and reviewing gated mutations (see
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Staging and reviewing gated mutations (see OpenLoam::PendingAction).
|
|
3
3
|
#
|
|
4
|
-
# pending =
|
|
4
|
+
# pending = OpenLoam::PendingActions.stage(
|
|
5
5
|
# summary: "Raise the excavator's daily rate to 1100",
|
|
6
6
|
# on: equipment, action: :update, changes: { daily_rate: 1100 }
|
|
7
7
|
# )
|
|
8
8
|
# pending.preview # => { "daily_rate" => { "from" => 950, "to" => 1100 } }
|
|
9
9
|
# pending.approve!(by: manager) # role-gated; executes the change
|
|
10
10
|
#
|
|
11
|
-
# `stage` NEVER touches the target — it only records the intent.
|
|
11
|
+
# `stage` NEVER touches the target — it only records the intent. OpenLoam does not
|
|
12
12
|
# intercept Active Record globally (that would be fragile and out of scope);
|
|
13
13
|
# this is the primitive a confirm-mode write path calls instead of saving.
|
|
14
14
|
module PendingActions
|
|
15
15
|
class << self
|
|
16
|
-
def stage(summary:, on:, action:, changes: {}, idempotency_key: nil, actor:
|
|
16
|
+
def stage(summary:, on:, action:, changes: {}, idempotency_key: nil, actor: OpenLoam::Current.actor)
|
|
17
17
|
target_type, target_id = resolve_target(on)
|
|
18
18
|
changes = changes.transform_keys(&:to_s)
|
|
19
19
|
key = idempotency_key || compute_key(target_type, target_id, action, changes)
|
|
@@ -21,10 +21,10 @@ module Loam
|
|
|
21
21
|
# The same proposal staged twice collapses to one row — but only a still
|
|
22
22
|
# PENDING one. A rejected or executed proposal with this key may be
|
|
23
23
|
# re-staged as a fresh pending row (the partial index allows the coexist).
|
|
24
|
-
existing =
|
|
24
|
+
existing = OpenLoam::PendingAction.pending.find_by(idempotency_key: key)
|
|
25
25
|
return existing if existing
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
OpenLoam::PendingAction.create!(
|
|
28
28
|
actor_id: actor&.id,
|
|
29
29
|
action_type: action.to_s,
|
|
30
30
|
target_type: target_type,
|
|
@@ -35,7 +35,7 @@ module Loam
|
|
|
35
35
|
)
|
|
36
36
|
rescue ActiveRecord::RecordNotUnique
|
|
37
37
|
# Lost a concurrent double-stage race — return the pending row that won.
|
|
38
|
-
|
|
38
|
+
OpenLoam::PendingAction.pending.find_by!(idempotency_key: key)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
@@ -53,7 +53,7 @@ module Loam
|
|
|
53
53
|
# the same key; a reordered hash is treated as a different proposal.
|
|
54
54
|
def compute_key(target_type, target_id, action, changes)
|
|
55
55
|
payload = [ target_type, target_id, action, changes.to_json ].join("|")
|
|
56
|
-
|
|
56
|
+
OpenLoam::Encryption.blind_index_scoped(payload, "tenant/#{OpenLoam.tenant!.id}")
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Feature-string permissions with wildcards (L-705) — a fine-grained capability
|
|
3
3
|
# layer that sits UNDER the coarse role. Roles answer "manager or clerk";
|
|
4
4
|
# permissions answer "may this role do `equipment.edit`" without inventing a new
|
|
5
|
-
# role for every distinction. Orthogonal to
|
|
6
|
-
# writes on a record) and to
|
|
5
|
+
# role for every distinction. Orthogonal to OpenLoam::Policy (which gates field-level
|
|
6
|
+
# writes on a record) and to OpenLoam::Features (a per-tenant capability switch).
|
|
7
7
|
#
|
|
8
8
|
# Declared once, in the initializer (like broadcast_events / scheduler defaults):
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# OpenLoam::Permissions.configure do
|
|
11
11
|
# role :admin, allow: "*" # everything
|
|
12
12
|
# role :manager, allow: %w[equipment.* damage_report.* billing.read]
|
|
13
13
|
# role :clerk, allow: %w[equipment.read damage_report.create]
|
|
14
14
|
# end
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# OpenLoam::Permissions.allow?(:clerk, "equipment.read") # => true
|
|
17
|
+
# OpenLoam::Permissions.allow?(:clerk, "equipment.edit") # => false
|
|
18
|
+
# OpenLoam.can?("equipment.edit") # for the current actor's role
|
|
19
19
|
#
|
|
20
20
|
# DENY BY DEFAULT: a role with no matching grant (or no grants at all) is denied.
|
|
21
21
|
#
|
|
@@ -72,7 +72,7 @@ module Loam
|
|
|
72
72
|
# Tiny DSL so `configure { role ... }` reads well without a receiver.
|
|
73
73
|
class DSL
|
|
74
74
|
def role(name, allow:)
|
|
75
|
-
|
|
75
|
+
OpenLoam::Permissions.role(name, allow: allow)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
end
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Reading the saved views (
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Reading the saved views (OpenLoam::Perspective) a user may see for an entity.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# OpenLoam::Perspectives.visible_to("Equipment", user: current_actor) # pick list
|
|
5
|
+
# OpenLoam::Perspectives.default_for("Equipment", user: current_actor) # the applicable default
|
|
6
|
+
# OpenLoam::Perspectives.resolve("Equipment", user:, id: params[:perspective_id])
|
|
7
7
|
#
|
|
8
|
-
# Every query is tenant-scoped by
|
|
8
|
+
# Every query is tenant-scoped by OpenLoam::Perspective. The membership role used
|
|
9
9
|
# for role-shared views is the user's role in the CURRENT tenant.
|
|
10
10
|
module Perspectives
|
|
11
11
|
class << self
|
|
12
12
|
# The views this user may see for an entity: their own private ones, the
|
|
13
13
|
# role-shared ones matching their membership role, and the tenant-wide
|
|
14
14
|
# ones. Default(s) first, then by name. Named `visible_to` rather than
|
|
15
|
-
# `for` so it does not read like
|
|
15
|
+
# `for` so it does not read like OpenLoam::Policy.for (which builds a policy).
|
|
16
16
|
def visible_to(entity_type, user:)
|
|
17
|
-
base =
|
|
17
|
+
base = OpenLoam::Perspective.where(entity_type: entity_type.to_s)
|
|
18
18
|
role = membership_role(user)
|
|
19
19
|
|
|
20
20
|
# Build the OR chain on UNORDERED relations — `.or` refuses to combine
|
|
@@ -55,7 +55,7 @@ module Loam
|
|
|
55
55
|
def membership_role(user)
|
|
56
56
|
return nil unless user
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
OpenLoam::Membership.find_by(user_id: user.id)&.role
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Base policy. One policy class per entity, one instance per (actor, record)
|
|
3
3
|
# pair. Action checks (read?/create?/update?/destroy?) default to "any member
|
|
4
4
|
# of the current tenant"; field-level write access is declared, not coded:
|
|
5
5
|
#
|
|
6
|
-
# class EquipmentPolicy <
|
|
6
|
+
# class EquipmentPolicy < OpenLoam::Policy
|
|
7
7
|
# field :daily_rate, writable: [:manager]
|
|
8
8
|
# end
|
|
9
9
|
#
|
|
10
|
-
# Roles come from
|
|
10
|
+
# Roles come from OpenLoam::Membership (actor + current tenant -> role).
|
|
11
11
|
class Policy
|
|
12
12
|
class << self
|
|
13
13
|
def field_rules
|
|
@@ -19,10 +19,23 @@ module Loam
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def for(record)
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
policy_class_for(record.class).new(OpenLoam::Current.actor, record)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A policy for a MODEL rather than one record — for bulk paths (export,
|
|
26
|
+
# import) that decide the column set once, before any row is read. The
|
|
27
|
+
# blank instance is a stand-in: the field checks key off the role, but
|
|
28
|
+
# the custom-field ones need record.class to find the definitions.
|
|
29
|
+
def for_model(model, actor)
|
|
30
|
+
policy_class_for(model).new(actor, model.new)
|
|
31
|
+
end
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
# Falling back to the base Policy for a model with no policy class would
|
|
34
|
+
# fail OPEN — every check there answers "any member", so a missing policy
|
|
35
|
+
# would silently grant what an explicit one restricts.
|
|
36
|
+
def policy_class_for(model)
|
|
37
|
+
"#{model.name}Policy".safe_constantize ||
|
|
38
|
+
raise(Error, "No policy defined for #{model.name} (expected #{model.name}Policy)")
|
|
26
39
|
end
|
|
27
40
|
end
|
|
28
41
|
|
|
@@ -36,7 +49,7 @@ module Loam
|
|
|
36
49
|
def role
|
|
37
50
|
return nil unless actor
|
|
38
51
|
|
|
39
|
-
@role ||=
|
|
52
|
+
@role ||= OpenLoam::Membership.find_by(user_id: actor.id)&.role&.to_sym
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def member? = role.present?
|
|
@@ -73,7 +86,7 @@ module Loam
|
|
|
73
86
|
end
|
|
74
87
|
|
|
75
88
|
# Same semantics as the static `field writable:` declaration, but for a
|
|
76
|
-
# runtime
|
|
89
|
+
# runtime OpenLoam::FieldDefinition (see OpenLoam::CustomFields): no
|
|
77
90
|
# writable_roles recorded means any member may write it.
|
|
78
91
|
def custom_field_writable?(field_name)
|
|
79
92
|
return false unless member?
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Entry point for progress tracking. Start a job, then drive it through the
|
|
3
|
-
# returned
|
|
3
|
+
# returned OpenLoam::ProgressJob:
|
|
4
4
|
#
|
|
5
|
-
# progress =
|
|
5
|
+
# progress = OpenLoam::Progress.start(name: "Reindex", total: equipment.count)
|
|
6
6
|
# equipment.find_each do |record|
|
|
7
7
|
# break if progress.cancelled? # cooperative cancel
|
|
8
8
|
# do_work(record)
|
|
@@ -12,18 +12,18 @@ module Loam
|
|
|
12
12
|
#
|
|
13
13
|
# The job is created in the current tenant (and stamped with the current
|
|
14
14
|
# actor), so it is only visible and streamable there. In a background job,
|
|
15
|
-
# establish the tenant with
|
|
15
|
+
# establish the tenant with OpenLoam.as_tenant first, like the webhook/digest jobs.
|
|
16
16
|
module Progress
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
19
|
def start(name:, total:, key: nil)
|
|
20
|
-
|
|
20
|
+
OpenLoam::ProgressJob.create!(
|
|
21
21
|
key: key,
|
|
22
22
|
name: name,
|
|
23
23
|
total: total.to_i,
|
|
24
24
|
completed: 0,
|
|
25
25
|
status: "running",
|
|
26
|
-
actor_id:
|
|
26
|
+
actor_id: OpenLoam.actor&.id,
|
|
27
27
|
started_at: Time.current
|
|
28
28
|
)
|
|
29
29
|
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
module
|
|
2
|
-
# Advisory record locks (
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Advisory record locks (OpenLoam::RecordLock) — a "who's editing this" courtesy
|
|
3
3
|
# for the multi-user admin.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# OpenLoam::RecordLocks.acquire(record, by: current_actor) # take/refresh, or nil if held
|
|
6
|
+
# OpenLoam::RecordLocks.holder(record) # the user holding it, or nil
|
|
7
|
+
# OpenLoam::RecordLocks.release(record, by: current_actor) # give it up
|
|
8
|
+
# OpenLoam::RecordLocks.force_release(record) # manager override
|
|
9
9
|
#
|
|
10
10
|
# THIS IS ADVISORY: a held lock warns, it does not hard-block — optimistic
|
|
11
11
|
# locking (lock_version) is the actual guarantee that two edits can't clobber.
|
|
12
|
-
# Every query is tenant-scoped by
|
|
12
|
+
# Every query is tenant-scoped by OpenLoam::RecordLock, so a lock is invisible and
|
|
13
13
|
# unaffectable from another tenant.
|
|
14
14
|
module RecordLocks
|
|
15
15
|
DEFAULT_TTL = 5.minutes
|
|
@@ -25,7 +25,7 @@ module Loam
|
|
|
25
25
|
lock = lock_row(record)
|
|
26
26
|
return nil if lock && !lock.expired? && lock.locked_by_id != by.id
|
|
27
27
|
|
|
28
|
-
lock ||=
|
|
28
|
+
lock ||= OpenLoam::RecordLock.new(lockable_type: type_of(record), lockable_id: record.id)
|
|
29
29
|
# A fresh session token when the lock is newly created or taken over from
|
|
30
30
|
# an expired holder; kept as-is on a heartbeat by the same user.
|
|
31
31
|
lock.token = SecureRandom.hex(16) if lock.new_record? || lock.locked_by_id != by.id
|
|
@@ -71,7 +71,7 @@ module Loam
|
|
|
71
71
|
private
|
|
72
72
|
|
|
73
73
|
def lock_row(record)
|
|
74
|
-
|
|
74
|
+
OpenLoam::RecordLock.find_by(lockable_type: type_of(record), lockable_id: record.id)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def type_of(record)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Per-tenant recurring jobs. Modules self-register default schedules; an app
|
|
3
|
-
# also creates them from the admin. A runner calls
|
|
4
|
-
# periodically (wire `
|
|
3
|
+
# also creates them from the admin. A runner calls OpenLoam::Scheduler.tick
|
|
4
|
+
# periodically (wire `open_loam:scheduler:tick` to system cron — every minute) and
|
|
5
5
|
# each DUE schedule enqueues its ActiveJob.
|
|
6
6
|
#
|
|
7
7
|
# NO DOUBLE-FIRE — the whole correctness story. tick ATOMICALLY CLAIMS due
|
|
@@ -12,7 +12,7 @@ module Loam
|
|
|
12
12
|
# The mechanism is behind `claim_due`, chosen per adapter. A claim also stamps
|
|
13
13
|
# `locked_until` so a crashed worker's rows free themselves after LOCK_TTL.
|
|
14
14
|
module Scheduler
|
|
15
|
-
class UnknownJobError <
|
|
15
|
+
class UnknownJobError < OpenLoam::Error; end
|
|
16
16
|
|
|
17
17
|
class << self
|
|
18
18
|
# --- declarative registry (like broadcast_events / feature_defaults) ---
|
|
@@ -31,14 +31,14 @@ module Loam
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Materialize the registered TENANT-scope schedules as rows in `tenant`
|
|
34
|
-
# (idempotent — safe from on_tenant_created and `
|
|
34
|
+
# (idempotent — safe from on_tenant_created and `open_loam:sync`). System-scope
|
|
35
35
|
# schedules are created explicitly by the app (they are not per-tenant).
|
|
36
36
|
def sync_tenant(tenant)
|
|
37
|
-
|
|
37
|
+
OpenLoam.as_tenant(tenant) do
|
|
38
38
|
registered.each do |default|
|
|
39
39
|
next unless default[:scope] == "tenant"
|
|
40
40
|
|
|
41
|
-
job =
|
|
41
|
+
job = OpenLoam::ScheduledJob.find_or_initialize_by(key: default[:key])
|
|
42
42
|
job.name = default[:name]
|
|
43
43
|
job.job_class = default[:job_class]
|
|
44
44
|
job.schedule = default[:schedule]
|
|
@@ -56,7 +56,7 @@ module Loam
|
|
|
56
56
|
# failure never blocks the others (its lock is released so a later tick
|
|
57
57
|
# retries it).
|
|
58
58
|
def tick(now: Time.current)
|
|
59
|
-
|
|
59
|
+
OpenLoam::Telemetry.span("scheduler_tick") { run_tick(now) }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def run_tick(now)
|
|
@@ -69,7 +69,7 @@ module Loam
|
|
|
69
69
|
fired += 1
|
|
70
70
|
rescue StandardError => error
|
|
71
71
|
job.update_columns(locked_until: nil, updated_at: now)
|
|
72
|
-
logger&.error("[
|
|
72
|
+
logger&.error("[open_loam scheduler] #{job.key} failed: #{error.class}: #{error.message}")
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
fired
|
|
@@ -83,7 +83,7 @@ module Loam
|
|
|
83
83
|
|
|
84
84
|
# THE code-execution guard: a job_class must resolve to a real ActiveJob
|
|
85
85
|
# subclass AND be ALLOWLISTED — either registered here or listed in
|
|
86
|
-
#
|
|
86
|
+
# OpenLoam.schedulable_jobs. "Any ActiveJob" would let a tenant admin schedule
|
|
87
87
|
# ActiveStorage::PurgeJob, a mailer's delivery job, etc.
|
|
88
88
|
def resolve_job_class(name)
|
|
89
89
|
name = name.to_s
|
|
@@ -94,7 +94,7 @@ module Loam
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def allowed_job_class?(name)
|
|
97
|
-
registered.any? { |default| default[:job_class] == name } ||
|
|
97
|
+
registered.any? { |default| default[:job_class] == name } || OpenLoam.schedulable_jobs.include?(name)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def resolve_job_class!(name)
|
|
@@ -113,36 +113,36 @@ module Loam
|
|
|
113
113
|
if job.scope == "system"
|
|
114
114
|
klass.perform_later
|
|
115
115
|
else
|
|
116
|
-
# Carry the tenant explicitly (ActiveJob doesn't serialize
|
|
116
|
+
# Carry the tenant explicitly (ActiveJob doesn't serialize OpenLoam::Current),
|
|
117
117
|
# and enqueue inside the tenant so any enqueue-time scoping is correct.
|
|
118
|
-
|
|
118
|
+
OpenLoam.as_tenant(job.tenant) { klass.perform_later(tenant_id: job.tenant_id) }
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Atomically claim due, unlocked schedules across ALL tenants (a blessed
|
|
123
123
|
# cross-tenant scan — the runner has no tenant context, like
|
|
124
|
-
#
|
|
124
|
+
# OpenLoam::Membership.tenants_for), stamping locked_until so a concurrent tick
|
|
125
125
|
# skips them.
|
|
126
126
|
def claim_due(now)
|
|
127
|
-
lock_until = now +
|
|
128
|
-
due =
|
|
127
|
+
lock_until = now + OpenLoam::ScheduledJob::LOCK_TTL
|
|
128
|
+
due = OpenLoam::ScheduledJob.unscoped.active
|
|
129
129
|
.where("next_run_at <= ?", now)
|
|
130
130
|
.where("locked_until IS NULL OR locked_until < ?", now)
|
|
131
131
|
|
|
132
132
|
claimed = []
|
|
133
|
-
|
|
133
|
+
OpenLoam::ScheduledJob.transaction do
|
|
134
134
|
relation = postgres? ? due.lock("FOR UPDATE SKIP LOCKED") : due
|
|
135
135
|
ids = relation.pluck(:id)
|
|
136
136
|
break if ids.empty?
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
claimed =
|
|
138
|
+
OpenLoam::ScheduledJob.unscoped.where(id: ids).update_all(locked_until: lock_until)
|
|
139
|
+
claimed = OpenLoam::ScheduledJob.unscoped.where(id: ids).to_a
|
|
140
140
|
end
|
|
141
141
|
claimed
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
def postgres?
|
|
145
|
-
|
|
145
|
+
OpenLoam::ScheduledJob.connection.adapter_name.to_s.downcase.include?("postgres")
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def logger
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# The driver contract. Subclass and override the class methods.
|
|
4
4
|
#
|
|
@@ -10,10 +10,10 @@ module Loam
|
|
|
10
10
|
#
|
|
11
11
|
# index(record) / remove(record)
|
|
12
12
|
# Maintain the driver's index as records are saved and destroyed. Called
|
|
13
|
-
# from
|
|
13
|
+
# from OpenLoam::Searchable's after_save / after_destroy.
|
|
14
14
|
#
|
|
15
15
|
# reindex(model)
|
|
16
|
-
# Rebuild the index for a model (used by `
|
|
16
|
+
# Rebuild the index for a model (used by `open_loam:search:reindex` and seeds).
|
|
17
17
|
#
|
|
18
18
|
# A driver that needs no index (LIKE) leaves index/remove/reindex as no-ops.
|
|
19
19
|
class Driver
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# The default driver: a substring LIKE across the declared columns — exactly
|
|
4
|
-
# the original
|
|
4
|
+
# the original OpenLoam::Searchable behavior, now behind the seam. Portable
|
|
5
5
|
# (Arel's `matches` emits ILIKE on PostgreSQL, case-insensitive LIKE on
|
|
6
6
|
# SQLite for ASCII) and needs no index, so index/remove/reindex are no-ops.
|
|
7
7
|
#
|
|
8
8
|
# What it does NOT do — ignore accents, split on words, rank — is the reason
|
|
9
|
-
# the seam exists: swap in
|
|
9
|
+
# the seam exists: swap in OpenLoam::Search::TokenDriver (or an external engine)
|
|
10
10
|
# and every `Model.search(q)` call site keeps working unchanged.
|
|
11
11
|
class LikeDriver < Driver
|
|
12
12
|
class << self
|
|
13
13
|
def search(model, query, scope:)
|
|
14
14
|
query = query.to_s.strip
|
|
15
|
-
return scope if query.blank? || !model.
|
|
15
|
+
return scope if query.blank? || !model.open_loam_searchable?
|
|
16
16
|
|
|
17
17
|
# sanitize_sql_like escapes the LIKE wildcards so a literal % or _ in
|
|
18
18
|
# the query stays literal; the second arg to `matches` declares that
|
|
19
19
|
# escape character to the database (SQLite has none by default).
|
|
20
20
|
pattern = "%#{model.sanitize_sql_like(query)}%"
|
|
21
|
-
conditions = model.
|
|
21
|
+
conditions = model.open_loam_searchable_columns.map do |column|
|
|
22
22
|
model.arel_table[column].matches(pattern, "\\")
|
|
23
23
|
end
|
|
24
24
|
scope.where(conditions.inject(:or))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# A portable, word-level index (the flagship second driver). Each record's
|
|
4
|
-
# searchable text is normalized into tokens stored in
|
|
4
|
+
# searchable text is normalized into tokens stored in open_loam_search_tokens;
|
|
5
5
|
# a query is tokenized the same way and matched with AND semantics — a record
|
|
6
6
|
# must carry EVERY query token — so "excavator cat" finds "CAT 320 Excavator"
|
|
7
7
|
# regardless of word order or position. Plain SQL: works on SQLite and
|
|
@@ -9,21 +9,21 @@ module Loam
|
|
|
9
9
|
# order-independent). When an app outgrows even this, the same seam takes a
|
|
10
10
|
# Meilisearch/Elasticsearch driver with no call-site change.
|
|
11
11
|
#
|
|
12
|
-
# Tenant isolation is free:
|
|
12
|
+
# Tenant isolation is free: OpenLoam::SearchToken is a TenantRecord, so both the
|
|
13
13
|
# match subquery and every write are scoped to the current tenant.
|
|
14
14
|
class TokenDriver < Driver
|
|
15
15
|
class << self
|
|
16
16
|
def search(model, query, scope:)
|
|
17
17
|
tokens = tokenize(query)
|
|
18
|
-
return scope if tokens.empty? || !model.
|
|
18
|
+
return scope if tokens.empty? || !model.open_loam_searchable?
|
|
19
19
|
|
|
20
20
|
# AND semantics: keep only records carrying all N distinct query
|
|
21
21
|
# tokens. Under AND every hit has the full match count, so ranking is
|
|
22
22
|
# degenerate — ordering is left to the caller.
|
|
23
|
-
matches =
|
|
23
|
+
matches = OpenLoam::SearchToken
|
|
24
24
|
.where(searchable_type: type_for(model), token: tokens)
|
|
25
25
|
.group(:searchable_id)
|
|
26
|
-
.having("COUNT(DISTINCT token) =
|
|
26
|
+
.having("COUNT(DISTINCT token) = ?", tokens.size)
|
|
27
27
|
.select(:searchable_id)
|
|
28
28
|
|
|
29
29
|
scope.where(id: matches)
|
|
@@ -33,10 +33,10 @@ module Loam
|
|
|
33
33
|
# the record's tenant, so the token rows land in the right tenant.
|
|
34
34
|
def index(record)
|
|
35
35
|
model = record.class
|
|
36
|
-
return unless model.
|
|
36
|
+
return unless model.open_loam_searchable?
|
|
37
37
|
|
|
38
38
|
type = type_for(model)
|
|
39
|
-
|
|
39
|
+
OpenLoam::SearchToken.where(searchable_type: type, searchable_id: record.id).delete_all
|
|
40
40
|
|
|
41
41
|
tokens = tokenize(searchable_text(record))
|
|
42
42
|
return if tokens.empty?
|
|
@@ -44,17 +44,17 @@ module Loam
|
|
|
44
44
|
rows = tokens.map do |token|
|
|
45
45
|
{ tenant_id: record.tenant_id, searchable_type: type, searchable_id: record.id, token: token }
|
|
46
46
|
end
|
|
47
|
-
|
|
47
|
+
OpenLoam::SearchToken.insert_all(rows)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def remove(record)
|
|
51
|
-
|
|
51
|
+
OpenLoam::SearchToken.where(searchable_type: type_for(record.class), searchable_id: record.id).delete_all
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Rebuild every current-tenant record of a model — run inside
|
|
55
|
-
#
|
|
55
|
+
# OpenLoam.as_tenant per tenant (the open_loam:search:reindex task does that).
|
|
56
56
|
def reindex(model)
|
|
57
|
-
|
|
57
|
+
OpenLoam::SearchToken.where(searchable_type: type_for(model)).delete_all
|
|
58
58
|
model.find_each { |record| index(record) }
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -65,13 +65,13 @@ module Loam
|
|
|
65
65
|
# THE SAFETY BOUNDARY: an encrypted column is skipped. Its stored value
|
|
66
66
|
# is ciphertext (meaningless to tokenize), and tokenizing the PLAINTEXT
|
|
67
67
|
# into a searchable table would recreate exactly the leak
|
|
68
|
-
#
|
|
68
|
+
# OpenLoam::Encryptable closes — an encrypted field stays exact-match-only via
|
|
69
69
|
# its blind index. `searchable_by` already refuses to declare an
|
|
70
70
|
# encrypted column; this subtraction is the second line of that defense.
|
|
71
71
|
def searchable_text(record)
|
|
72
|
-
columns = record.class.
|
|
73
|
-
if record.class.respond_to?(:
|
|
74
|
-
columns -= record.class.
|
|
72
|
+
columns = record.class.open_loam_searchable_columns
|
|
73
|
+
if record.class.respond_to?(:open_loam_encrypted_attributes)
|
|
74
|
+
columns -= record.class.open_loam_encrypted_attributes
|
|
75
75
|
end
|
|
76
76
|
columns.map { |column| record.public_send(column) }.join(" ")
|
|
77
77
|
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# The search seam. `Model.search(q)` and the `searchable_by` declaration never
|
|
3
3
|
# change; the STRATEGY behind them is a swappable driver:
|
|
4
4
|
#
|
|
5
|
-
#
|
|
5
|
+
# OpenLoam::Search.driver = OpenLoam::Search::TokenDriver # in an initializer
|
|
6
6
|
#
|
|
7
7
|
# The default is the LIKE driver — portable and zero-setup — so nothing
|
|
8
8
|
# changes out of the box. A driver turns a text query into a subset of an
|
|
9
9
|
# already-tenant-scoped relation and maintains whatever index it needs; see
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# OpenLoam::Search::Driver for the contract. This is the same shape as
|
|
11
|
+
# OpenLoam::EventStream.broadcaster and OpenLoam::Enrichers: one seam, drivers behind
|
|
12
12
|
# it, callers untouched.
|
|
13
13
|
module Search
|
|
14
14
|
class << self
|
|
15
15
|
attr_writer :driver
|
|
16
16
|
|
|
17
17
|
def driver
|
|
18
|
-
@driver ||=
|
|
18
|
+
@driver ||= OpenLoam::Search::LikeDriver
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def search(model, query, scope:) = driver.search(model, query, scope: scope)
|