Module: OpenLoam::Lifecycle

Defined in:
lib/open_loam/lifecycle.rb

Overview

Tenant lifecycle: what a brand-new tenant gets for free.

An app declares seeding once, in config/initializers/open_loam.rb:

OpenLoam.on_tenant_created do |tenant|
OpenLoam::FieldDefinition.find_or_create_by!(entity_type: "Equipment", name: "asset_tag") { ... }
end

The block runs inside OpenLoam.as_tenant(tenant), so tenant-scoped writes need no extra ceremony.

THE CONTRACT: callbacks MUST be idempotent. They fire once when a tenant is created, and again for EVERY existing tenant whenever bin/rails open_loam:sync runs — which is how a role/field/default added by a later release reaches tenants that already exist. Write find_or_create_by!, never create!.

Class Method Summary collapse

Class Method Details

.broadcast_events ⇒ Object

Event-name patterns (OpenLoam::Events.pattern_matches?) whose events may be pushed to the browser over SSE (OpenLoam::EventStream). DEFAULT OFF — an empty list means nothing reaches a browser; an app opts in explicitly, so a stray event never leaks by default.



82
83
84
# File 'lib/open_loam/lifecycle.rb', line 82

def self.broadcast_events
  @broadcast_events ||= []
end

.broadcast_events=(patterns) ⇒ Object



86
87
88
# File 'lib/open_loam/lifecycle.rb', line 86

def self.broadcast_events=(patterns)
  @broadcast_events = Array(patterns).map(&:to_s)
end

.config_defaults ⇒ Object

App-wide setting defaults: { "billing.currency" => "USD" }, declared in the initializer and read by OpenLoam::Configs as the baseline a key resolves to when no global row and no tenant override exist. A registry, like default_roles — declaring a default here needs no migration and no row.



70
71
72
# File 'lib/open_loam/lifecycle.rb', line 70

def self.config_defaults
  @config_defaults ||= {}
end

.config_defaults=(defaults) ⇒ Object



74
75
76
# File 'lib/open_loam/lifecycle.rb', line 74

def self.config_defaults=(defaults)
  @config_defaults = defaults.to_h.transform_keys(&:to_s)
end

.default_locale ⇒ Object



135
136
137
# File 'lib/open_loam/lifecycle.rb', line 135

def self.default_locale
  (defined?(I18n) ? I18n.default_locale : :en).to_s
end

.default_roles ⇒ Object

Role names this app expects every tenant to have — declared in the initializer (OpenLoam.default_roles = %w[manager employee]) and read by whatever seeds memberships. A registry, not a mechanism: OpenLoam does not create roles for you, because who gets which role is business logic.



58
59
60
# File 'lib/open_loam/lifecycle.rb', line 58

def self.default_roles
  @default_roles ||= []
end

.default_roles=(roles) ⇒ Object



62
63
64
# File 'lib/open_loam/lifecycle.rb', line 62

def self.default_roles=(roles)
  @default_roles = Array(roles).map(&:to_s)
end

.event_log_retention ⇒ Object

How long a captured event is kept before OpenLoam::EventLogPruneJob deletes it. nil disables pruning, leaving an unbounded log.



103
104
105
# File 'lib/open_loam/lifecycle.rb', line 103

def self.event_log_retention
  defined?(@event_log_retention) ? @event_log_retention : 90.days
end

.event_log_retention=(duration) ⇒ Object



107
108
109
# File 'lib/open_loam/lifecycle.rb', line 107

def self.event_log_retention=(duration)
  @event_log_retention = duration
end

.feature_defaults ⇒ Object

Known feature flags: { "beta_dashboard" => { default: false, description: "..." } }, declared in the initializer and read by OpenLoam::Features. A registry like the others — a flag with no row resolves to its declared default, and the admin can list EVERY known flag, not just toggled ones.



153
154
155
# File 'lib/open_loam/lifecycle.rb', line 153

def self.feature_defaults
  @feature_defaults ||= {}
end

.feature_defaults=(defaults) ⇒ Object

Normalizes both levels: outer keys to strings, and each flag's own hash to symbol keys, so { "x" => { "default" => true } } and { x: { default: true } } behave identically.



160
161
162
163
164
# File 'lib/open_loam/lifecycle.rb', line 160

def self.feature_defaults=(defaults)
  @feature_defaults = defaults.to_h.each_with_object({}) do |(name, spec), out|
    out[name.to_s] = spec.to_h.transform_keys(&:to_sym)
  end
end

.locale ⇒ Object

The current request/job locale — content reads overlay onto it. Request state like the tenant (set in a before_action, reset with OpenLoam::Current).



141
142
143
# File 'lib/open_loam/lifecycle.rb', line 141

def self.locale
  (OpenLoam::Current.locale || default_locale).to_s
end

.locale=(code) ⇒ Object



145
146
147
# File 'lib/open_loam/lifecycle.rb', line 145

def self.locale=(code)
  OpenLoam::Current.locale = code&.to_s
end

.locales ⇒ Object

The locales content translations (OpenLoam::Translatable) may be authored in — declared in the initializer (OpenLoam.locales = %w[en de pl]), so the admin knows which languages to offer. A registry like the others; defaults to the single default locale.



115
116
117
# File 'lib/open_loam/lifecycle.rb', line 115

def self.locales
  @locales ||= [ default_locale ]
end

.locales=(codes) ⇒ Object



119
120
121
# File 'lib/open_loam/lifecycle.rb', line 119

def self.locales=(codes)
  @locales = Array(codes).map(&:to_s)
end

.on_tenant_created(&block) ⇒ Object



24
25
26
27
# File 'lib/open_loam/lifecycle.rb', line 24

def self.on_tenant_created(&block)
  tenant_created_callbacks << block
  block
end

.run_tenant_created(tenant) ⇒ Object

The single execution path for a tenant's callbacks — used both by OpenLoam::Tenant's after_create_commit and by sync_tenants!, so "runs inside as_tenant, in declaration order" can never drift between the two.

Exceptions propagate: a failing callback fails the tenant creation (Rails re-raises from after_commit) or the sync run, loudly, like every other OpenLoam guardrail.



36
37
38
39
40
# File 'lib/open_loam/lifecycle.rb', line 36

def self.run_tenant_created(tenant)
  OpenLoam.as_tenant(tenant) do
    tenant_created_callbacks.each { |callback| callback.call(tenant) }
  end
end

.schedulable_jobs ⇒ Object

Job classes an app EXPLICITLY allows the scheduler to run, beyond the ones it registers via OpenLoam::Scheduler.register. An allowlist (not "any ActiveJob") so a tenant admin can't schedule ActiveStorage::PurgeJob or a mailer. Declared in the initializer: OpenLoam.schedulable_jobs = %w[DigestJob].



127
128
129
# File 'lib/open_loam/lifecycle.rb', line 127

def self.schedulable_jobs
  @schedulable_jobs ||= []
end

.schedulable_jobs=(names) ⇒ Object



131
132
133
# File 'lib/open_loam/lifecycle.rb', line 131

def self.schedulable_jobs=(names)
  @schedulable_jobs = Array(names).map(&:to_s)
end

.sync_tenants! ⇒ Object

Re-runs every on_tenant_created callback for every existing tenant. Idempotent by contract (see above) — safe to run on every deploy. Returns the number of tenants synced.



45
46
47
48
49
50
51
52
# File 'lib/open_loam/lifecycle.rb', line 45

def self.sync_tenants!
  count = 0
  OpenLoam::Tenant.find_each do |tenant|
    run_tenant_created(tenant)
    count += 1
  end
  count
end

.tenant_created_callbacks ⇒ Object

Registered blocks, in declaration order. Registration returns the block itself so a caller (typically a test) can deregister it again.



20
21
22
# File 'lib/open_loam/lifecycle.rb', line 20

def self.tenant_created_callbacks
  @tenant_created_callbacks ||= []
end

.uncaptured_events ⇒ Object

Event-name patterns EXCLUDED from the event log (OpenLoam::EventLog), which captures everything else. An empty list captures everything; there is no opt-in spelling. Progress ticks fire once per row during a bulk import.



93
94
95
# File 'lib/open_loam/lifecycle.rb', line 93

def self.uncaptured_events
  @uncaptured_events ||= [ "open_loam.progress." ]
end

.uncaptured_events=(patterns) ⇒ Object



97
98
99
# File 'lib/open_loam/lifecycle.rb', line 97

def self.uncaptured_events=(patterns)
  @uncaptured_events = Array(patterns).map(&:to_s)
end