open-loam 0.1.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 +7 -0
- data/CHANGELOG.md +71 -0
- data/LICENSE +21 -0
- data/README.md +177 -0
- data/app/jobs/loam/custom_field_reindex_job.rb +22 -0
- data/app/jobs/loam/event_delivery_job.rb +26 -0
- data/app/jobs/loam/event_redelivery_sweep_job.rb +21 -0
- data/app/jobs/loam/webhook_delivery_job.rb +59 -0
- data/app/models/loam/api_token.rb +40 -0
- data/app/models/loam/audit_record.rb +18 -0
- data/app/models/loam/auth_attempt.rb +13 -0
- data/app/models/loam/business_rule.rb +49 -0
- data/app/models/loam/business_rule_run.rb +27 -0
- data/app/models/loam/comment.rb +28 -0
- data/app/models/loam/config.rb +27 -0
- data/app/models/loam/custom_field_value.rb +18 -0
- data/app/models/loam/dashboard_widget.rb +16 -0
- data/app/models/loam/dictionary.rb +20 -0
- data/app/models/loam/dictionary_entry.rb +31 -0
- data/app/models/loam/event_delivery.rb +38 -0
- data/app/models/loam/field_definition.rb +48 -0
- data/app/models/loam/inbound_webhook_delivery.rb +24 -0
- data/app/models/loam/inbound_webhook_source.rb +55 -0
- data/app/models/loam/membership.rb +27 -0
- data/app/models/loam/mfa_credential.rb +114 -0
- data/app/models/loam/notification.rb +36 -0
- data/app/models/loam/pending_action.rb +208 -0
- data/app/models/loam/perspective.rb +104 -0
- data/app/models/loam/progress_job.rb +103 -0
- data/app/models/loam/record_lock.rb +19 -0
- data/app/models/loam/scheduled_job.rb +42 -0
- data/app/models/loam/search_token.rb +9 -0
- data/app/models/loam/sso_identity.rb +13 -0
- data/app/models/loam/sso_provider.rb +35 -0
- data/app/models/loam/tenant.rb +20 -0
- data/app/models/loam/translation.rb +17 -0
- data/app/models/loam/webhook_endpoint.rb +26 -0
- data/app/views/loam/attachments/_attachments.html.erb +16 -0
- data/app/views/loam/comments/_comments.html.erb +24 -0
- data/app/views/loam/custom_fields/_fields.html.erb +35 -0
- data/lib/generators/loam/entity/entity_generator.rb +160 -0
- data/lib/generators/loam/entity/templates/api_controller.rb +67 -0
- data/lib/generators/loam/entity/templates/controller.rb +181 -0
- data/lib/generators/loam/entity/templates/entity_test.rb +107 -0
- data/lib/generators/loam/entity/templates/migration.rb +26 -0
- data/lib/generators/loam/entity/templates/model.rb +27 -0
- data/lib/generators/loam/entity/templates/policy.rb +10 -0
- data/lib/generators/loam/entity/templates/views/_form.html.erb +63 -0
- data/lib/generators/loam/entity/templates/views/deleted.html.erb +41 -0
- data/lib/generators/loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/index.html.erb +90 -0
- data/lib/generators/loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/show.html.erb +36 -0
- data/lib/generators/loam/install/install_generator.rb +288 -0
- data/lib/generators/loam/install/templates/AGENTS.md +341 -0
- data/lib/generators/loam/install/templates/admin/api_docs_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/api_docs_index.html.erb +34 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +30 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_index.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/base_controller.rb +224 -0
- data/lib/generators/loam/install/templates/admin/business_rules_controller.rb +64 -0
- data/lib/generators/loam/install/templates/admin/business_rules_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/business_rules_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/business_rules_index.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/business_rules_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/comments_controller.rb +39 -0
- data/lib/generators/loam/install/templates/admin/configs_controller.rb +46 -0
- data/lib/generators/loam/install/templates/admin/configs_edit.html.erb +22 -0
- data/lib/generators/loam/install/templates/admin/configs_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +10 -0
- data/lib/generators/loam/install/templates/admin/dashboard_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_index.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_controller.rb +55 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_edit.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_form.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/dictionary_entries_controller.rb +47 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/events_controller.rb +42 -0
- data/lib/generators/loam/install/templates/admin/features_controller.rb +35 -0
- data/lib/generators/loam/install/templates/admin/features_index.html.erb +37 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_controller.rb +52 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_new.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/history_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/history_index.html.erb +35 -0
- data/lib/generators/loam/install/templates/admin/imports_controller.rb +75 -0
- data/lib/generators/loam/install/templates/admin/imports_new.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/imports_preview.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/imports_summary.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_new.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/loam/install/templates/admin/mfa_activated.html.erb +12 -0
- data/lib/generators/loam/install/templates/admin/mfa_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/mfa_new.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/mfa_show.html.erb +19 -0
- data/lib/generators/loam/install/templates/admin/notifications_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/notifications_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/overrides_controller.rb +14 -0
- data/lib/generators/loam/install/templates/admin/overrides_index.html.erb +29 -0
- data/lib/generators/loam/install/templates/admin/pagination.rb +23 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_index.html.erb +40 -0
- data/lib/generators/loam/install/templates/admin/perspectives_controller.rb +98 -0
- data/lib/generators/loam/install/templates/admin/perspectives_index.html.erb +38 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +19 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_index.html.erb +31 -0
- data/lib/generators/loam/install/templates/admin/record_locks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_controller.rb +69 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_edit.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_form.html.erb +16 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_index.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/search_controller.rb +49 -0
- data/lib/generators/loam/install/templates/admin/search_index.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/sessions_controller.rb +248 -0
- data/lib/generators/loam/install/templates/admin/sessions_mfa_challenge.html.erb +18 -0
- data/lib/generators/loam/install/templates/admin/sessions_new.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_controller.rb +65 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sudo_controller.rb +45 -0
- data/lib/generators/loam/install/templates/admin/sudo_new.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/translations_controller.rb +37 -0
- data/lib/generators/loam/install/templates/admin/translations_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_new.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin.css +29 -0
- data/lib/generators/loam/install/templates/api_base_controller.rb +90 -0
- data/lib/generators/loam/install/templates/guardrails_test.rb +79 -0
- data/lib/generators/loam/install/templates/import_job.rb +25 -0
- data/lib/generators/loam/install/templates/inbound_webhooks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/initializer.rb +218 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +14 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_auth_attempts.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_business_rules.rb +29 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_configs.rb +21 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +19 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionary_entries.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_event_deliveries.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_field_definitions.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_inbound_webhooks.rb +31 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_mfa_credentials.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_pending_actions.rb +26 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_perspectives.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_progress_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +16 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_scheduled_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_sso_providers.rb +34 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +9 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_users.rb +13 -0
- data/lib/generators/loam/install/templates/user.rb +21 -0
- data/lib/generators/loam/primary_key_options.rb +101 -0
- data/lib/loam/attachable.rb +27 -0
- data/lib/loam/auditable.rb +67 -0
- data/lib/loam/auth_throttle.rb +83 -0
- data/lib/loam/base32.rb +23 -0
- data/lib/loam/bulk.rb +59 -0
- data/lib/loam/business_rules/actions.rb +92 -0
- data/lib/loam/business_rules/condition.rb +102 -0
- data/lib/loam/business_rules.rb +123 -0
- data/lib/loam/commentable.rb +22 -0
- data/lib/loam/configs.rb +115 -0
- data/lib/loam/cron.rb +98 -0
- data/lib/loam/csv.rb +18 -0
- data/lib/loam/current.rb +21 -0
- data/lib/loam/custom_field_index.rb +262 -0
- data/lib/loam/custom_fields.rb +114 -0
- data/lib/loam/dashboard.rb +17 -0
- data/lib/loam/dictionaries.rb +55 -0
- data/lib/loam/durable_events.rb +174 -0
- data/lib/loam/encryptable.rb +145 -0
- data/lib/loam/encryption/cipher.rb +75 -0
- data/lib/loam/encryption/key_provider.rb +43 -0
- data/lib/loam/encryption.rb +125 -0
- data/lib/loam/engine.rb +57 -0
- data/lib/loam/enrichers.rb +105 -0
- data/lib/loam/errors.rb +49 -0
- data/lib/loam/eval.rb +47 -0
- data/lib/loam/event_stream.rb +80 -0
- data/lib/loam/eventful.rb +45 -0
- data/lib/loam/events.rb +51 -0
- data/lib/loam/export.rb +74 -0
- data/lib/loam/features.rb +75 -0
- data/lib/loam/generated_key.rb +37 -0
- data/lib/loam/import.rb +168 -0
- data/lib/loam/inbound_webhooks.rb +125 -0
- data/lib/loam/lifecycle.rb +180 -0
- data/lib/loam/locales/loam.en.yml +80 -0
- data/lib/loam/mcp/server.rb +60 -0
- data/lib/loam/mcp.rb +304 -0
- data/lib/loam/notifications.rb +36 -0
- data/lib/loam/open_api.rb +194 -0
- data/lib/loam/overrides.rb +110 -0
- data/lib/loam/pending_actions.rb +60 -0
- data/lib/loam/permissions.rb +79 -0
- data/lib/loam/perspectives.rb +62 -0
- data/lib/loam/policy.rb +106 -0
- data/lib/loam/progress.rb +31 -0
- data/lib/loam/record_locks.rb +89 -0
- data/lib/loam/scheduler.rb +153 -0
- data/lib/loam/search/driver.rb +31 -0
- data/lib/loam/search/like_driver.rb +29 -0
- data/lib/loam/search/token_driver.rb +88 -0
- data/lib/loam/search.rb +27 -0
- data/lib/loam/searchable.rb +90 -0
- data/lib/loam/soft_deletable.rb +72 -0
- data/lib/loam/sso/claims.rb +10 -0
- data/lib/loam/sso/fake_provider.rb +51 -0
- data/lib/loam/sso/http_client.rb +42 -0
- data/lib/loam/sso/oidc_provider.rb +64 -0
- data/lib/loam/sso.rb +129 -0
- data/lib/loam/telemetry.rb +43 -0
- data/lib/loam/tenant_record.rb +36 -0
- data/lib/loam/test_helpers.rb +15 -0
- data/lib/loam/totp.rb +63 -0
- data/lib/loam/translatable.rb +88 -0
- data/lib/loam/undo.rb +126 -0
- data/lib/loam/version.rb +3 -0
- data/lib/loam/webhooks.rb +34 -0
- data/lib/loam/widgets.rb +77 -0
- data/lib/loam/workflow.rb +256 -0
- data/lib/loam.rb +122 -0
- data/lib/open-loam.rb +5 -0
- data/lib/tasks/loam.rake +171 -0
- data/lib/tasks/loam_eval.rake +32 -0
- data/lib/tasks/loam_mcp.rake +20 -0
- metadata +332 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Public endpoint that receives webhooks FROM external systems at
|
|
2
|
+
# /webhooks/:token. No session, no bearer token: the HMAC signature is the auth
|
|
3
|
+
# (ActionController::API, so there is no CSRF to skip). All the verified,
|
|
4
|
+
# replay-resistant work lives in Loam::InboundWebhooks.ingest; this controller
|
|
5
|
+
# only maps its Result to an HTTP status and never leaks tenant context to the
|
|
6
|
+
# next request on this thread.
|
|
7
|
+
class InboundWebhooksController < ActionController::API
|
|
8
|
+
def receive
|
|
9
|
+
result = Loam::InboundWebhooks.ingest(
|
|
10
|
+
token: params[:token], raw_body: request.raw_post, headers: request.headers
|
|
11
|
+
)
|
|
12
|
+
Rails.logger.info("[loam inbound] #{result.status} #{result.reason}") if result.status >= 400
|
|
13
|
+
head result.status
|
|
14
|
+
ensure
|
|
15
|
+
Loam::Current.reset
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Loam configuration, tenant lifecycle hooks, and domain event subscriptions.
|
|
2
|
+
|
|
3
|
+
# Master key for field encryption at rest (Loam::Encryptable). Per-tenant keys
|
|
4
|
+
# are derived from it via HKDF, so it must be STABLE and SECRET — never commit it.
|
|
5
|
+
# Set LOAM_MASTER_KEY in the environment (or wire Rails.application.credentials)
|
|
6
|
+
# to a high-entropy value, e.g. `SecureRandom.hex(32)`. Only apps that actually
|
|
7
|
+
# `encrypts` a field need it; encryption raises a clear error if it is missing.
|
|
8
|
+
#
|
|
9
|
+
# Loam::Encryption.master_key = ENV.fetch("LOAM_MASTER_KEY")
|
|
10
|
+
#
|
|
11
|
+
# Rotating the key without re-encrypting orphans existing ciphertext — see
|
|
12
|
+
# `bin/rails loam:encryption:rotate[Model,tenant_id]`.
|
|
13
|
+
|
|
14
|
+
# Roles every tenant of this app is expected to have. A registry read by your
|
|
15
|
+
# own seeding/admin code — Loam does not create memberships for you, because
|
|
16
|
+
# who gets which role is business logic.
|
|
17
|
+
Loam.default_roles = %w[manager employee]
|
|
18
|
+
|
|
19
|
+
# Locales that CONTENT translations (Loam::Translatable) may be authored in — the
|
|
20
|
+
# admin's language switcher and the per-record Translations screen offer these.
|
|
21
|
+
# The first is the base/default; the rest overlay onto it. This is for user data
|
|
22
|
+
# (a product name), NOT developer UI strings — those stay Rails i18n. Loam ships
|
|
23
|
+
# a `loam.*` base locale (English) for its own chrome; the admin switcher sets
|
|
24
|
+
# I18n.locale too, so add `config/locales/loam.pl.yml` (etc.) to translate the UI
|
|
25
|
+
# for each locale you list here.
|
|
26
|
+
Loam.locales = %w[en]
|
|
27
|
+
|
|
28
|
+
# Observability (Loam::Telemetry). By default Loam wraps its async hot paths
|
|
29
|
+
# (scheduler tick, durable event delivery, inbound webhook ingest) in spans that
|
|
30
|
+
# emit ActiveSupport::Notifications events ("loam.span.*") — subscribe to those,
|
|
31
|
+
# or plug a real tracer in here:
|
|
32
|
+
#
|
|
33
|
+
# Loam::Telemetry.backend = ->(name, attributes, work) do
|
|
34
|
+
# OpenTelemetry.tracer_provider.tracer("loam").in_span(name, attributes: attributes.transform_keys(&:to_s)) { work.call }
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
# Feature-string permissions (Loam::Permissions) — a finer capability layer under
|
|
38
|
+
# the coarse role. Deny-by-default; `*` grants everything, a trailing `.*` is a
|
|
39
|
+
# prefix ("equipment.*"). Check with `Loam.can?("equipment.edit")`,
|
|
40
|
+
# `require_permission!("...")` in a controller, or the `can?` view helper.
|
|
41
|
+
# Orthogonal to roles (Loam::Membership) and field-level policies (Loam::Policy).
|
|
42
|
+
#
|
|
43
|
+
# Loam::Permissions.configure do
|
|
44
|
+
# role :admin, allow: "*"
|
|
45
|
+
# role :manager, allow: %w[equipment.* billing.read]
|
|
46
|
+
# role :clerk, allow: %w[equipment.read]
|
|
47
|
+
# end
|
|
48
|
+
|
|
49
|
+
# Customization WITHOUT forking (Loam::Overrides): disable or replace an entry in
|
|
50
|
+
# one of Loam's keyed registries (:widgets, :broadcast_events). A stale override
|
|
51
|
+
# (a key that no longer exists) is warned about at boot by `check!`, so a
|
|
52
|
+
# typo'd override is visible instead of silently doing nothing.
|
|
53
|
+
#
|
|
54
|
+
# Loam::Overrides.disable(:widgets, "open_progress") # drop a built-in widget
|
|
55
|
+
# Loam::Overrides.replace(:widgets, "audit_recent") { |actor| { kind: "count", value: 0 } }
|
|
56
|
+
# Loam::Overrides.disable(:broadcast_events, "loam.progress.") # stop pushing it over SSE
|
|
57
|
+
#
|
|
58
|
+
# BOUNDARY: this is only for Loam's in-gem registries. Override a VIEW, CONTROLLER,
|
|
59
|
+
# or ROUTE the standard Rails way (create a file at the same path to shadow the
|
|
60
|
+
# engine's, or `prepend` a module) — not here.
|
|
61
|
+
|
|
62
|
+
# Events pushed live to the browser over SSE (Loam::EventStream). Default off; the
|
|
63
|
+
# notification pattern is enabled here so the admin bell increments without a
|
|
64
|
+
# reload. Add patterns (e.g. "billing.") to stream domain events to live widgets —
|
|
65
|
+
# nothing reaches the browser unless it matches a pattern here (security posture).
|
|
66
|
+
Loam.broadcast_events = [ "loam.notification.", "loam.progress." ]
|
|
67
|
+
|
|
68
|
+
# Response enrichers (Loam::Enrichers): attach a computed block onto ANOTHER
|
|
69
|
+
# module's entity in admin/API responses, with no foreign-key coupling — billing
|
|
70
|
+
# can annotate an Equipment without Equipment knowing billing exists. Pass a
|
|
71
|
+
# `batch:` resolver (array -> { id => value }) so an index stays one query, not N.
|
|
72
|
+
#
|
|
73
|
+
# Loam::Enrichers.register("Equipment", key: "outstanding_balance", batch: ->(equipments) do
|
|
74
|
+
# totals = Invoice.where(equipment_id: equipments.map(&:id)).group(:equipment_id).sum(:balance)
|
|
75
|
+
# equipments.map(&:id).index_with { |id| totals.fetch(id, 0) }
|
|
76
|
+
# end)
|
|
77
|
+
|
|
78
|
+
# Search backend (Loam::Search). The default is a substring LIKE — portable and
|
|
79
|
+
# zero-setup. Opt into the word-level TokenDriver (also portable, no external
|
|
80
|
+
# service: it keeps a normalized-token index in loam_search_tokens) for
|
|
81
|
+
# order-independent, whole-word matching. `searchable_by` and every
|
|
82
|
+
# `Model.search(q)` call site are UNCHANGED — only this line differs; an external
|
|
83
|
+
# engine (Meilisearch/Elasticsearch) is a third driver behind the same seam.
|
|
84
|
+
# After switching, backfill existing rows once with `bin/rails loam:search:reindex`
|
|
85
|
+
# (new and updated records index themselves on save).
|
|
86
|
+
#
|
|
87
|
+
# Loam::Search.driver = Loam::Search::TokenDriver
|
|
88
|
+
|
|
89
|
+
# Custom-field read-model index (Loam::CustomFieldIndex). Filtering/sorting on a
|
|
90
|
+
# custom field is index-backed (a typed projection in loam_custom_field_values)
|
|
91
|
+
# instead of a per-row JSON scan. Records project themselves on save; backfill
|
|
92
|
+
# existing data once with `bin/rails loam:index:reindex` (or
|
|
93
|
+
# `Loam::CustomFieldIndex.reindex(Model)`). Query with
|
|
94
|
+
# `Loam::CustomFieldIndex.filter(Model, field_key, op, value)` — the generated
|
|
95
|
+
# entity index routes a `cf_field`/`cf_op`/`cf_value` filter through it.
|
|
96
|
+
|
|
97
|
+
# Single sign-on (Loam::Sso). OIDC is shipped end-to-end: configure a per-tenant
|
|
98
|
+
# provider under /admin/sso_providers (issuer, client_id, client_secret, email
|
|
99
|
+
# domain, default JIT role). Home-realm discovery routes a user to their tenant's
|
|
100
|
+
# IdP by email domain; a verified identity is JIT-provisioned or linked to an
|
|
101
|
+
# existing User. The client_secret is encrypted at rest, so LOAM_MASTER_KEY must
|
|
102
|
+
# be set (same key as the rest of Loam::Encryptable). SAML and SCIM are documented
|
|
103
|
+
# seams (see docs/_foundation/overview.md). Nothing to register here for real OIDC.
|
|
104
|
+
#
|
|
105
|
+
# In tests, inject the offline fake so the flow never hits the network:
|
|
106
|
+
#
|
|
107
|
+
# Loam::Sso.builder = ->(record, redirect_uri) { Loam::Sso::FakeProvider.new(record, redirect_uri: redirect_uri) }
|
|
108
|
+
|
|
109
|
+
# Roles that MUST use two-factor auth (Loam::MfaCredential). At login, a user
|
|
110
|
+
# whose role in the chosen tenant is on this list and who has not enrolled is
|
|
111
|
+
# sent to set MFA up before they can work. Resolved via Loam::Configs, so it can
|
|
112
|
+
# be a global default or a per-tenant override; empty means MFA is optional.
|
|
113
|
+
#
|
|
114
|
+
# Loam::Configs.set("security.mfa_required_roles", ["manager"], scope: :global)
|
|
115
|
+
#
|
|
116
|
+
# Auth rate-limiting / lockout (Loam::AuthThrottle) guards failed password/TOTP/
|
|
117
|
+
# sudo attempts. Defaults: lock after 10 failures within a 15-minute window.
|
|
118
|
+
# Tune via Loam::Configs (global or per-tenant):
|
|
119
|
+
#
|
|
120
|
+
# Loam::Configs.set("security.max_auth_attempts", 5, scope: :global)
|
|
121
|
+
# Loam::Configs.set("security.auth_window_minutes", 10, scope: :global)
|
|
122
|
+
#
|
|
123
|
+
# Rack::Attack (with a cache store) is the production-scale path across multiple
|
|
124
|
+
# processes; the built-in DB counter is the portable single-process default.
|
|
125
|
+
|
|
126
|
+
# App-wide setting defaults (Loam::Configs). A key resolves override → global
|
|
127
|
+
# row → this declared default, so declaring one here needs no migration and no
|
|
128
|
+
# row — a tenant reads the default until someone overrides it in the admin
|
|
129
|
+
# Settings screen (/admin/configs). Values keep their type (bool/number/string/
|
|
130
|
+
# hash), and a per-tenant override never leaks to another tenant.
|
|
131
|
+
#
|
|
132
|
+
# Loam.config_defaults = { "billing.currency" => "USD", "billing.net_terms" => 30 }
|
|
133
|
+
#
|
|
134
|
+
# To change the company-wide baseline at runtime (not per tenant), write a
|
|
135
|
+
# global row: `Loam::Configs.set("billing.currency", "EUR", scope: :global)`.
|
|
136
|
+
|
|
137
|
+
# Feature flags (Loam::Features). Declare known capabilities and their default
|
|
138
|
+
# state; a flag with no row resolves to its declared default, and managers flip
|
|
139
|
+
# a tenant's override at /admin/features. A flag gates a CAPABILITY (is this on
|
|
140
|
+
# for the tenant), orthogonal to roles/policies, which gate a PERSON.
|
|
141
|
+
#
|
|
142
|
+
# Loam.feature_defaults = {
|
|
143
|
+
# "beta_dashboard" => { default: false, description: "New dashboard, rolled out per tenant." }
|
|
144
|
+
# }
|
|
145
|
+
#
|
|
146
|
+
# In a controller: `require_feature!(:beta_dashboard)` (404s when off); in a
|
|
147
|
+
# view: `<%% if feature_on?(:beta_dashboard) %>`. Flip app-wide with
|
|
148
|
+
# `Loam::Features.enable(:beta_dashboard, scope: :global)`.
|
|
149
|
+
|
|
150
|
+
# What a brand-new tenant gets for free. Registered at file scope on purpose:
|
|
151
|
+
# inside `to_prepare` this would re-register on every code reload.
|
|
152
|
+
Loam.on_tenant_created do |tenant|
|
|
153
|
+
# Seed roles/defaults for a new tenant here. MUST be idempotent —
|
|
154
|
+
# `bin/rails loam:sync` re-runs these for every existing tenant, which is how
|
|
155
|
+
# a default added in a later release reaches tenants that already exist.
|
|
156
|
+
# Use find_or_create_by!, never create!. The block runs inside
|
|
157
|
+
# Loam.as_tenant(tenant), so tenant-scoped writes need no extra ceremony.
|
|
158
|
+
#
|
|
159
|
+
# Baseline managed lookup lists (Loam::Dictionary) — a FieldDefinition of type
|
|
160
|
+
# "dictionary" can then point a custom field at the key, rendering a select:
|
|
161
|
+
#
|
|
162
|
+
# severity = Loam::Dictionary.find_or_create_by!(key: "damage_severity") { |d| d.name = "Damage severity" }
|
|
163
|
+
# [%w[minor Minor], %w[major Major], %w[critical Critical]].each_with_index do |(value, label), i|
|
|
164
|
+
# severity.entries.find_or_create_by!(value: value) { |e| e.label = label; e.position = i }
|
|
165
|
+
# end
|
|
166
|
+
|
|
167
|
+
# Materialize the registered tenant-scope schedules (see Loam::Scheduler.register
|
|
168
|
+
# below) as rows for this tenant. A no-op until you register one.
|
|
169
|
+
Loam::Scheduler.sync_tenant(tenant)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Recurring jobs (Loam::Scheduler). Register default schedules here at file scope;
|
|
173
|
+
# on_tenant_created materializes the tenant-scope ones per tenant. A "tenant" job
|
|
174
|
+
# is enqueued with `tenant_id:` (establish it with Loam.as_tenant); a "system" job
|
|
175
|
+
# runs once with no tenant. job_class MUST be a real ActiveJob (validated — never
|
|
176
|
+
# arbitrary code). Wire the runner to system cron, every minute:
|
|
177
|
+
#
|
|
178
|
+
# * * * * * cd /path/to/app && bin/rails loam:scheduler:tick
|
|
179
|
+
#
|
|
180
|
+
# The claim is atomic, so running it from several hosts never double-fires a job.
|
|
181
|
+
#
|
|
182
|
+
# Loam::Scheduler.register(key: "nightly_digest", name: "Nightly digest",
|
|
183
|
+
# job_class: "DigestJob", schedule: "0 7 * * *", scope: "tenant")
|
|
184
|
+
#
|
|
185
|
+
# Only ALLOWLISTED job classes are schedulable (a registered one, or one listed
|
|
186
|
+
# here) — so a tenant admin can't schedule ActiveStorage::PurgeJob or a mailer:
|
|
187
|
+
#
|
|
188
|
+
# Loam.schedulable_jobs = %w[DigestJob ReindexJob]
|
|
189
|
+
|
|
190
|
+
# Domain event subscriptions. Subscribe to a single event or a whole domain
|
|
191
|
+
# (trailing dot = prefix). Register them here at file scope, not inside
|
|
192
|
+
# `to_prepare`: subscriptions are global, so a reload would add a second copy
|
|
193
|
+
# of each one and every event would be handled twice.
|
|
194
|
+
#
|
|
195
|
+
# Loam::Events.subscribe("billing.subscription.renewed") do |name, payload|
|
|
196
|
+
# BillingMailer.renewal_receipt(payload[:id]).deliver_later
|
|
197
|
+
# end
|
|
198
|
+
#
|
|
199
|
+
# Loam::Events.subscribe("rental.") do |name, payload|
|
|
200
|
+
# Rails.logger.info("[loam event] #{name} #{payload.inspect}")
|
|
201
|
+
# end
|
|
202
|
+
#
|
|
203
|
+
# Event -> in-app notification is the intended pattern for telling someone
|
|
204
|
+
# something. The subscriber runs in the publisher's tenant context, so the
|
|
205
|
+
# notification lands in the right tenant with no extra ceremony:
|
|
206
|
+
#
|
|
207
|
+
# Loam::Events.subscribe("rental.damage_report.approve") do |_name, payload|
|
|
208
|
+
# Loam::Notifications.notify_role(:manager, title: "Damage report approved")
|
|
209
|
+
# end
|
|
210
|
+
|
|
211
|
+
# Business rules (Loam::BusinessRule) are the admin-editable cousin of the
|
|
212
|
+
# subscriptions above: a manager declares, per tenant, WHEN a condition holds on
|
|
213
|
+
# a triggering record THEN run a fixed vocabulary of safe actions (notify,
|
|
214
|
+
# emit_event, set_field, block_transition). The condition is DATA — a
|
|
215
|
+
# whitelisted {field, op, value} tree, never evaluated as code — so a tenant
|
|
216
|
+
# admin can wire up automation without a deploy and without a code-injection
|
|
217
|
+
# surface. The engine subscribes itself at boot; nothing to register here.
|
|
218
|
+
# Manage rules under /admin/business_rules and see why they fired in the run log.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateLoamApiTokens < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_api_tokens<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.references :user, null: false, foreign_key: true<%= loam_type_option %>
|
|
6
|
+
t.string :token, null: false
|
|
7
|
+
t.string :label
|
|
8
|
+
t.datetime :last_used_at
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
# Tokens are looked up before a tenant is known, so the uniqueness that
|
|
12
|
+
# matters is global.
|
|
13
|
+
add_index :loam_api_tokens, :token, unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateLoamAuditRecords < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_audit_records<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :auditable_type, null: false
|
|
6
|
+
t.<%= loam_key_column_type %> :auditable_id<%= loam_key_limit_option %>, null: false
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.<%= loam_key_column_type %> :actor_id<%= loam_key_limit_option %>
|
|
9
|
+
t.text :changeset
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :loam_audit_records, %i[tenant_id auditable_type auditable_id]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateLoamAuthAttempts < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_auth_attempts<%= loam_id_option %> do |t|
|
|
4
|
+
t.string :identifier, null: false # the submitted login (normalized), NOT tenant-scoped
|
|
5
|
+
t.string :kind, null: false # password / totp / recovery / sudo
|
|
6
|
+
t.string :ip # nullable; for an optional per-ip throttle
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :loam_auth_attempts, %i[identifier created_at]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class CreateLoamBusinessRules < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_business_rules<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :entity_type # the entity this rule watches (nil = event-only)
|
|
7
|
+
t.string :trigger, null: false # an event pattern (Loam::Events.pattern_matches?)
|
|
8
|
+
t.json :condition, null: false, default: {} # a safe condition tree (data, never code)
|
|
9
|
+
t.json :actions, null: false, default: [] # an ordered list of typed action descriptors
|
|
10
|
+
t.boolean :active, null: false, default: true
|
|
11
|
+
t.integer :priority, null: false, default: 0
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
add_index :loam_business_rules, %i[tenant_id active]
|
|
15
|
+
|
|
16
|
+
create_table :loam_business_rule_runs<%= loam_id_option %> do |t|
|
|
17
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
18
|
+
t.references :business_rule, null: false, foreign_key: { to_table: :loam_business_rules }<%= loam_type_option %>
|
|
19
|
+
t.string :subject_type
|
|
20
|
+
t.<%= loam_key_column_type %> :subject_id<%= loam_key_limit_option %>
|
|
21
|
+
t.string :event_name
|
|
22
|
+
t.boolean :matched, null: false, default: false
|
|
23
|
+
t.json :actions_taken, null: false, default: []
|
|
24
|
+
t.text :error
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
add_index :loam_business_rule_runs, %i[tenant_id business_rule_id created_at]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateLoamComments < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_comments<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :commentable_type, null: false
|
|
6
|
+
t.<%= loam_key_column_type %> :commentable_id<%= loam_key_limit_option %>, null: false
|
|
7
|
+
t.references :author, null: false, foreign_key: { to_table: :users }<%= loam_type_option %>
|
|
8
|
+
t.text :body, null: false
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
add_index :loam_comments, %i[tenant_id commentable_type commentable_id]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateLoamConfigs < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_configs<%= loam_id_option %> do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
# Nullable: a NULL tenant_id is the GLOBAL row; a set one is a per-tenant
|
|
6
|
+
# override. So this is NOT a tenant-scoped table.
|
|
7
|
+
t.references :tenant, null: true, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
8
|
+
# Any JSON-able value under one key. Nullable on purpose: row existence is
|
|
9
|
+
# the "is it set" signal, and JSON null is itself a storable value.
|
|
10
|
+
t.json :value_json
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# One global row per key (partial: only where there is no tenant).
|
|
15
|
+
add_index :loam_configs, :key, unique: true, where: "tenant_id IS NULL",
|
|
16
|
+
name: "index_loam_configs_global_key"
|
|
17
|
+
# One override row per key per tenant.
|
|
18
|
+
add_index :loam_configs, %i[key tenant_id], unique: true,
|
|
19
|
+
name: "index_loam_configs_on_key_and_tenant"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateLoamCustomFieldValues < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_custom_field_values<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :indexable_type, null: false
|
|
6
|
+
t.<%= loam_key_column_type %> :indexable_id<%= loam_key_limit_option %>, null: false
|
|
7
|
+
t.string :field_key, null: false
|
|
8
|
+
t.text :value_text # canonical string form (contains/present/eq)
|
|
9
|
+
t.decimal :value_number # integer/decimal fields (range queries)
|
|
10
|
+
t.boolean :value_boolean # boolean fields
|
|
11
|
+
t.datetime :value_datetime # date/datetime fields
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
add_index :loam_custom_field_values, %i[indexable_type indexable_id field_key],
|
|
15
|
+
unique: true, name: "index_loam_cfv_unique"
|
|
16
|
+
add_index :loam_custom_field_values, %i[tenant_id indexable_type field_key value_text], name: "index_loam_cfv_text"
|
|
17
|
+
add_index :loam_custom_field_values, %i[tenant_id indexable_type field_key value_number], name: "index_loam_cfv_number"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateLoamDashboardWidgets < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_dashboard_widgets<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :widget_key, null: false # a registered Loam::Widgets key
|
|
6
|
+
t.integer :position, null: false, default: 0
|
|
7
|
+
t.boolean :active, null: false, default: true
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :loam_dashboard_widgets, %i[tenant_id widget_key], unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateLoamDictionaries < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_dictionaries<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :key, null: false # the code the field def / API references (e.g. "damage_severity")
|
|
6
|
+
t.string :name, null: false # human label for the list
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :loam_dictionaries, %i[tenant_id key], unique: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateLoamDictionaryEntries < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_dictionary_entries<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.references :dictionary, null: false, foreign_key: { to_table: :loam_dictionaries }<%= loam_type_option %>
|
|
6
|
+
t.string :value, null: false # the stored code
|
|
7
|
+
t.string :label, null: false # display text
|
|
8
|
+
t.string :color # optional hex
|
|
9
|
+
t.string :icon # optional icon name
|
|
10
|
+
t.integer :position, null: false, default: 0 # ordering
|
|
11
|
+
t.boolean :is_default, null: false, default: false
|
|
12
|
+
t.boolean :active, null: false, default: true
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :loam_dictionary_entries, %i[dictionary_id value], unique: true
|
|
16
|
+
add_index :loam_dictionary_entries, %i[tenant_id dictionary_id position]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateLoamEventDeliveries < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_event_deliveries<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :subscriber_key, null: false # a registered Loam::DurableEvents subscriber
|
|
6
|
+
t.string :event_name, null: false
|
|
7
|
+
t.json :payload, null: false # JSON-scalar event payload
|
|
8
|
+
t.string :status, null: false, default: "pending" # pending | delivered | dead
|
|
9
|
+
t.integer :attempts, null: false, default: 0
|
|
10
|
+
t.text :last_error
|
|
11
|
+
t.datetime :next_attempt_at # backoff gate for the next retry
|
|
12
|
+
t.datetime :delivered_at
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :loam_event_deliveries, %i[tenant_id status next_attempt_at]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateLoamFieldDefinitions < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_field_definitions<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :entity_type, null: false
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :field_type, null: false
|
|
8
|
+
t.json :writable_roles, default: [], null: false
|
|
9
|
+
t.json :readable_roles, default: [], null: false # empty = any member may read (used by the index oracle guard)
|
|
10
|
+
t.json :config, null: false, default: {} # type-specific settings (e.g. a dictionary field's key)
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :loam_field_definitions, %i[tenant_id entity_type name], unique: true, name: "index_loam_field_definitions_on_tenant_entity_name"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class CreateLoamInboundWebhooks < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_inbound_webhook_sources<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :token, null: false # unguessable URL id: /webhooks/:token
|
|
7
|
+
t.string :secret, null: false # HMAC key (authenticates the call)
|
|
8
|
+
t.string :signature_header, null: false, default: "X-Loam-Signature"
|
|
9
|
+
t.string :delivery_id_header # optional: external delivery-id for dedupe
|
|
10
|
+
t.string :timestamp_header # optional: enables the freshness window
|
|
11
|
+
t.integer :timestamp_tolerance # seconds; nil = default 300
|
|
12
|
+
t.string :event_name, null: false # what to publish on the bus
|
|
13
|
+
t.boolean :active, null: false, default: true
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
add_index :loam_inbound_webhook_sources, :token, unique: true
|
|
17
|
+
|
|
18
|
+
create_table :loam_inbound_webhook_deliveries<%= loam_id_option %> do |t|
|
|
19
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
20
|
+
t.references :source, null: false, foreign_key: { to_table: :loam_inbound_webhook_sources }<%= loam_type_option %>
|
|
21
|
+
t.string :external_id, null: false # delivery-id header, or a body hash
|
|
22
|
+
t.string :event_name, null: false
|
|
23
|
+
t.string :status, null: false, default: "received"
|
|
24
|
+
t.datetime :received_at
|
|
25
|
+
t.json :payload, null: false
|
|
26
|
+
t.timestamps
|
|
27
|
+
end
|
|
28
|
+
# The idempotency ledger: a replayed (source, external_id) can't be inserted twice.
|
|
29
|
+
add_index :loam_inbound_webhook_deliveries, %i[source_id external_id], unique: true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateLoamMemberships < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_memberships<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.references :user, null: false, foreign_key: true<%= loam_type_option %>
|
|
6
|
+
t.string :role, null: false
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :loam_memberships, %i[tenant_id user_id], unique: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateLoamMfaCredentials < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_mfa_credentials<%= loam_id_option %> do |t|
|
|
4
|
+
# Per USER, not per tenant — MFA belongs to the person (no tenant_id).
|
|
5
|
+
t.references :user, null: false, foreign_key: { to_table: :users }, index: { unique: true }<%= loam_type_option %>
|
|
6
|
+
t.text :totp_secret # encrypted at rest, user-scoped (Loam::Encryptable)
|
|
7
|
+
t.text :recovery_codes # JSON: [{ digest, used_at }], codes stored hashed
|
|
8
|
+
t.datetime :activated_at # nil until the user confirms a live code
|
|
9
|
+
t.bigint :last_totp_step # last accepted TOTP timestep — rejects replays
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateLoamNotifications < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_notifications<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.references :user, null: false, foreign_key: true<%= loam_type_option %>
|
|
6
|
+
t.string :title, null: false
|
|
7
|
+
t.text :body
|
|
8
|
+
t.datetime :read_at
|
|
9
|
+
t.string :source_type
|
|
10
|
+
t.<%= loam_key_column_type %> :source_id<%= loam_key_limit_option %>
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :loam_notifications, %i[tenant_id user_id read_at]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CreateLoamPendingActions < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_pending_actions<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.<%= loam_key_column_type %> :actor_id<%= loam_key_limit_option %> # who staged it (the proposer)
|
|
6
|
+
t.string :status, null: false, default: "pending"
|
|
7
|
+
t.string :action_type, null: false # create / update / destroy
|
|
8
|
+
t.string :target_type # the model being mutated
|
|
9
|
+
t.<%= loam_key_column_type %> :target_id<%= loam_key_limit_option %> # null for a staged create
|
|
10
|
+
t.text :changeset # encrypted JSON of the proposed changes
|
|
11
|
+
t.text :summary, null: false # human-readable description
|
|
12
|
+
t.string :idempotency_key, null: false # keyed HMAC; same proposal collapses to one row
|
|
13
|
+
t.<%= loam_key_column_type %> :reviewed_by_id<%= loam_key_limit_option %> # the approving/rejecting manager
|
|
14
|
+
t.datetime :reviewed_at
|
|
15
|
+
t.text :result # execution outcome, or "rejected: <reason>"
|
|
16
|
+
t.text :error # execution failure message
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
# One row per distinct proposal per tenant.
|
|
20
|
+
# Only ONE PENDING row per proposal per tenant — a rejected/executed row with
|
|
21
|
+
# the same key may coexist, so a rejected proposal can be re-staged later.
|
|
22
|
+
add_index :loam_pending_actions, %i[tenant_id idempotency_key], unique: true,
|
|
23
|
+
where: "status = 'pending'", name: "index_loam_pending_actions_pending_key"
|
|
24
|
+
add_index :loam_pending_actions, %i[tenant_id status]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateLoamPerspectives < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_perspectives<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :entity_type, null: false # which entity this view is for
|
|
6
|
+
t.string :name, null: false # human label
|
|
7
|
+
t.<%= loam_key_column_type %> :owner_id<%= loam_key_limit_option %> # the user who owns a private view
|
|
8
|
+
t.string :visibility, null: false, default: "private" # private / role / tenant
|
|
9
|
+
t.string :role # set when visibility is "role"
|
|
10
|
+
t.boolean :is_default, null: false, default: false
|
|
11
|
+
t.json :config, null: false, default: {} # { columns, filters, sort, page_size }
|
|
12
|
+
t.integer :lock_version, null: false, default: 0 # optimistic locking
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
add_index :loam_perspectives, %i[tenant_id entity_type]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateLoamProgressJobs < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_progress_jobs<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :key # optional caller-chosen id (e.g. "reindex:Gadget")
|
|
6
|
+
t.string :name, null: false # human label
|
|
7
|
+
t.string :status, null: false, default: "running" # running / completed / failed / cancelled
|
|
8
|
+
t.integer :total, null: false, default: 0
|
|
9
|
+
t.integer :completed, null: false, default: 0
|
|
10
|
+
t.text :message # latest status line
|
|
11
|
+
t.text :error # set when failed
|
|
12
|
+
t.<%= loam_key_column_type %> :actor_id<%= loam_key_limit_option %> # who started it
|
|
13
|
+
t.datetime :started_at
|
|
14
|
+
t.datetime :finished_at
|
|
15
|
+
t.json :result # structured result for a finished job (e.g. an import summary)
|
|
16
|
+
t.timestamps # updated_at is the heartbeat
|
|
17
|
+
end
|
|
18
|
+
add_index :loam_progress_jobs, %i[tenant_id status]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateLoamRecordLocks < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_record_locks<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :lockable_type, null: false
|
|
6
|
+
t.<%= loam_key_column_type %> :lockable_id<%= loam_key_limit_option %>, null: false
|
|
7
|
+
t.references :locked_by, null: false, foreign_key: { to_table: :users }<%= loam_type_option %>
|
|
8
|
+
t.string :token, null: false
|
|
9
|
+
t.datetime :expires_at, null: false # TTL; an expired lock is treated as free
|
|
10
|
+
t.timestamps # updated_at is the heartbeat
|
|
11
|
+
end
|
|
12
|
+
# One advisory lock per record.
|
|
13
|
+
add_index :loam_record_locks, %i[tenant_id lockable_type lockable_id], unique: true,
|
|
14
|
+
name: "index_loam_record_locks_on_lockable"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateLoamScheduledJobs < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :loam_scheduled_jobs<%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
t.string :key, null: false # unique per tenant
|
|
6
|
+
t.string :name, null: false
|
|
7
|
+
t.string :job_class, null: false # a whitelisted ActiveJob class name
|
|
8
|
+
t.string :schedule, null: false # "0 7 * * *" or "interval:3600"
|
|
9
|
+
t.string :timezone # IANA zone; UTC when blank
|
|
10
|
+
t.string :scope, null: false, default: "tenant" # "tenant" (per-tenant) or "system" (once)
|
|
11
|
+
t.datetime :next_run_at
|
|
12
|
+
t.datetime :last_run_at
|
|
13
|
+
t.datetime :locked_until # the atomic-claim lock (no double-fire)
|
|
14
|
+
t.boolean :active, null: false, default: true
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
add_index :loam_scheduled_jobs, %i[tenant_id key], unique: true
|
|
18
|
+
add_index :loam_scheduled_jobs, %i[active next_run_at locked_until]
|
|
19
|
+
end
|
|
20
|
+
end
|