approval_engine 1.0.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 +138 -0
- data/MIT-LICENSE +20 -0
- data/README.md +249 -0
- data/app/controllers/approval_engine/application_controller.rb +10 -0
- data/app/controllers/approval_engine/approvals_controller.rb +29 -0
- data/app/helpers/approval_engine/application_helper.rb +27 -0
- data/app/jobs/approval_engine/application_job.rb +4 -0
- data/app/jobs/approval_engine/process_outbox_job.rb +100 -0
- data/app/jobs/approval_engine/timeout_sweep_job.rb +19 -0
- data/app/models/approval_engine/application_record.rb +5 -0
- data/app/models/approval_engine/approval.rb +144 -0
- data/app/models/approval_engine/approval_plan.rb +60 -0
- data/app/models/approval_engine/audit_log.rb +28 -0
- data/app/models/approval_engine/consensus.rb +37 -0
- data/app/models/approval_engine/delegation.rb +34 -0
- data/app/models/approval_engine/history.rb +62 -0
- data/app/models/approval_engine/outbox_event.rb +47 -0
- data/app/models/approval_engine/step.rb +271 -0
- data/app/models/approval_engine/template_step.rb +22 -0
- data/app/models/approval_engine/track.rb +127 -0
- data/app/models/approval_engine/track_template.rb +23 -0
- data/app/models/approval_engine/trigger_rule.rb +17 -0
- data/app/models/concerns/approval_engine/approvable.rb +183 -0
- data/app/services/approval_engine/approval_builder.rb +172 -0
- data/app/services/approval_engine/iteration_builder.rb +44 -0
- data/app/services/approval_engine/rule_evaluator.rb +119 -0
- data/app/views/approval_engine/approvals/index.html.erb +38 -0
- data/app/views/approval_engine/approvals/show.html.erb +55 -0
- data/app/views/layouts/approval_engine/dashboard.html.erb +49 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20260614103434_create_approval_engine_core_tables.rb +107 -0
- data/db/migrate/20260614103435_create_approval_engine_infrastructure_tables.rb +35 -0
- data/db/migrate/20260614104809_create_approval_engine_blueprint_tables.rb +60 -0
- data/db/migrate/20260616000001_add_trigger_rule_to_approvals.rb +14 -0
- data/db/migrate/20260617000001_add_approvals_required_to_approvals.rb +11 -0
- data/db/migrate/20260617000002_add_delivery_tracking_to_outbox_events.rb +11 -0
- data/lib/approval_engine/approval_exposure.rb +66 -0
- data/lib/approval_engine/configuration.rb +67 -0
- data/lib/approval_engine/engine.rb +17 -0
- data/lib/approval_engine/model_extensions.rb +20 -0
- data/lib/approval_engine/version.rb +3 -0
- data/lib/approval_engine.rb +12 -0
- data/lib/generators/approval_engine/install/install_generator.rb +34 -0
- data/lib/generators/approval_engine/install/templates/POST_INSTALL +55 -0
- data/lib/generators/approval_engine/install/templates/approval_engine.rb +21 -0
- data/lib/generators/approval_engine/views/templates/approvals/index.html.erb +17 -0
- data/lib/generators/approval_engine/views/templates/approvals_controller.rb +32 -0
- data/lib/generators/approval_engine/views/views_generator.rb +33 -0
- metadata +129 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module ApprovalEngine
|
|
2
|
+
# Extended onto ActiveRecord::Base so every model gains the class macro,
|
|
3
|
+
# the way `acts_as_*` gems do. Calling it mixes in the Approvable concern.
|
|
4
|
+
module ModelExtensions
|
|
5
|
+
# Arm a model with approvals.
|
|
6
|
+
#
|
|
7
|
+
# has_approvals # auto-routes on create (default)
|
|
8
|
+
# has_approvals(on: [:create, :update]) # also route on update
|
|
9
|
+
# has_approvals(on: []) # opt out; trigger manually
|
|
10
|
+
#
|
|
11
|
+
# `on:` accepts any of ApprovalEngine::Approvable::LIFECYCLE_EVENTS
|
|
12
|
+
# (:create, :update, :destroy), routing the conventional "<model>.created" etc.
|
|
13
|
+
# For domain transitions (e.g. "invoice.rejected"), the engine isn't lifecycle
|
|
14
|
+
# bound at all — just call `run_approval!(event:)` from your own action.
|
|
15
|
+
def has_approvals(on: [ :create ])
|
|
16
|
+
include ApprovalEngine::Approvable
|
|
17
|
+
self.approval_trigger_events = Array(on).freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "approval_engine/version"
|
|
2
|
+
require "approval_engine/configuration"
|
|
3
|
+
require "approval_engine/approval_exposure"
|
|
4
|
+
require "approval_engine/model_extensions"
|
|
5
|
+
require "approval_engine/engine"
|
|
6
|
+
|
|
7
|
+
module ApprovalEngine
|
|
8
|
+
# Base class for every error the engine raises itself, so a host can rescue
|
|
9
|
+
# ApprovalEngine::Error to catch them all. (Step transitions still raise the
|
|
10
|
+
# Rails-idiomatic ActiveRecord::RecordInvalid.)
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module ApprovalEngine
|
|
5
|
+
module Generators
|
|
6
|
+
# Copies the engine migrations into the host app and drops a configured
|
|
7
|
+
# initializer.
|
|
8
|
+
#
|
|
9
|
+
# rails generate approval_engine:install
|
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
desc "Installs ApprovalEngine: copies migrations and creates an initializer."
|
|
16
|
+
|
|
17
|
+
def self.next_migration_number(dirname)
|
|
18
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_initializer
|
|
22
|
+
template "approval_engine.rb", "config/initializers/approval_engine.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def copy_migrations
|
|
26
|
+
rake "approval_engine:install:migrations"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show_readme
|
|
30
|
+
readme "POST_INSTALL" if behavior == :invoke
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
ApprovalEngine installed.
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run the migrations:
|
|
8
|
+
|
|
9
|
+
rails db:migrate
|
|
10
|
+
|
|
11
|
+
2. Edit config/initializers/approval_engine.rb and set BOTH:
|
|
12
|
+
* actor_class — your approver class (e.g. "User")
|
|
13
|
+
* current_tenant_method — REQUIRED. Until it's set, auto-routing on
|
|
14
|
+
create silently does nothing. Single-tenant
|
|
15
|
+
apps can return a constant.
|
|
16
|
+
|
|
17
|
+
3. Make your actor class resolve approval groups:
|
|
18
|
+
|
|
19
|
+
class User < ApplicationRecord
|
|
20
|
+
def self.resolve_approval_group(group_name, target)
|
|
21
|
+
where(role: group_name)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
4. Arm a model and declare its safe surface (a rule can only read attributes
|
|
26
|
+
you expose here):
|
|
27
|
+
|
|
28
|
+
class Invoice < ApplicationRecord
|
|
29
|
+
has_approvals
|
|
30
|
+
|
|
31
|
+
exposes_for_approval do
|
|
32
|
+
attribute :amount, type: :decimal
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def after_approved
|
|
36
|
+
PaymentService.disburse_funds!(self)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
5. Create an ACTIVE template + a matching rule (see the README quickstart),
|
|
41
|
+
then CONFIRM the wiring before relying on it:
|
|
42
|
+
|
|
43
|
+
invoice.preview_approval(event: "invoice.created").triggered?
|
|
44
|
+
# false? -> check event_name match, the template's `active` status,
|
|
45
|
+
# the rule condition, and that its vars are exposed attributes.
|
|
46
|
+
|
|
47
|
+
6. (Optional) Copy the approval views to customise them:
|
|
48
|
+
|
|
49
|
+
rails generate approval_engine:views
|
|
50
|
+
|
|
51
|
+
7. (Optional) Mount the ops dashboard in config/routes.rb:
|
|
52
|
+
|
|
53
|
+
mount ApprovalEngine::Engine => "/approval_engine"
|
|
54
|
+
|
|
55
|
+
===============================================================================
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ApprovalEngine.configure do |config|
|
|
2
|
+
# ActiveJob queue used to relay the transactional outbox (host callbacks and
|
|
3
|
+
# notifications). Defaults to :default; a dedicated queue is recommended.
|
|
4
|
+
config.outbox_queue = :default
|
|
5
|
+
|
|
6
|
+
# How the engine resolves the current tenant for strict data isolation.
|
|
7
|
+
# Return anything that responds to #id, e.g. with acts_as_tenant:
|
|
8
|
+
# config.current_tenant_method = -> { Current.account }
|
|
9
|
+
# IMPORTANT: while this is nil, auto-routing on create silently no-ops (the
|
|
10
|
+
# engine can't scope the rules). Single-tenant apps can return a constant:
|
|
11
|
+
# config.current_tenant_method = -> { Struct.new(:id).new("default") }
|
|
12
|
+
config.current_tenant_method = nil
|
|
13
|
+
|
|
14
|
+
# Your application's actor class (who approves). It must define:
|
|
15
|
+
# def self.resolve_approval_group(group_name, target) -> [actors]
|
|
16
|
+
config.actor_class = "User"
|
|
17
|
+
|
|
18
|
+
# Fail closed by default: a malformed dynamic rule quarantines the approval
|
|
19
|
+
# instead of crashing the approval. Set to true in development/test to raise.
|
|
20
|
+
config.raise_on_rule_errors = false
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# Copied by `rails generate approval_engine:views`. Style this however you like. %>
|
|
2
|
+
<h1>My approvals</h1>
|
|
3
|
+
|
|
4
|
+
<% if @pending_steps.empty? %>
|
|
5
|
+
<p>Nothing is awaiting your approval.</p>
|
|
6
|
+
<% else %>
|
|
7
|
+
<ul>
|
|
8
|
+
<% @pending_steps.each do |step| %>
|
|
9
|
+
<li>
|
|
10
|
+
<strong><%= step.target %></strong> —
|
|
11
|
+
<%= step.name || "Step" %> (layer <%= step.layer %>, iteration <%= step.iteration %>)
|
|
12
|
+
<%= button_to "Approve", approve_approval_path(step), method: :patch %>
|
|
13
|
+
<%= button_to "Reject", reject_approval_path(step), method: :patch %>
|
|
14
|
+
</li>
|
|
15
|
+
<% end %>
|
|
16
|
+
</ul>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copied by `rails generate approval_engine:views`.
|
|
2
|
+
#
|
|
3
|
+
# This file is yours now — restyle it, rename it, add authorization (Pundit,
|
|
4
|
+
# etc.). ApprovalEngine provides the mechanism (step.approve! / reject!); the
|
|
5
|
+
# user-facing experience is entirely up to you.
|
|
6
|
+
class ApprovalsController < ApplicationController
|
|
7
|
+
def index
|
|
8
|
+
# actionable_by includes steps delegated to you, and preloads each step's
|
|
9
|
+
# target so the view can show *what* is awaiting approval without N+1s.
|
|
10
|
+
@pending_steps = ApprovalEngine::Step.actionable_by(current_user)
|
|
11
|
+
.includes(track: { approval: :target })
|
|
12
|
+
.order(:created_at)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def approve
|
|
16
|
+
act(:approve!, notice: "Approval recorded.")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reject
|
|
20
|
+
act(:reject!, notice: "Rejection recorded.")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def act(method, notice:)
|
|
26
|
+
step = ApprovalEngine::Step.pending.find(params[:id])
|
|
27
|
+
step.public_send(method, by: current_user, comment: params[:comment])
|
|
28
|
+
redirect_back fallback_location: approvals_path, notice: notice
|
|
29
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
30
|
+
redirect_back fallback_location: approvals_path, alert: e.message
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module ApprovalEngine
|
|
4
|
+
module Generators
|
|
5
|
+
# Copies an example approvals controller and views into the host app. They
|
|
6
|
+
# are intentionally unstyled and minimal — you own them, theme them, rename
|
|
7
|
+
# them. The engine ships the mechanism; the customer-facing UI is yours.
|
|
8
|
+
#
|
|
9
|
+
# rails generate approval_engine:views
|
|
10
|
+
class ViewsGenerator < Rails::Generators::Base
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Copies example approval controller and views into your app to customise."
|
|
14
|
+
|
|
15
|
+
def copy_controller
|
|
16
|
+
copy_file "approvals_controller.rb", "app/controllers/approvals_controller.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_views
|
|
20
|
+
directory "approvals", "app/views/approvals"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def routes_hint
|
|
24
|
+
say "\nAdd routes for the copied controller, for example:", :green
|
|
25
|
+
say <<~RUBY
|
|
26
|
+
resources :approvals, only: :index do
|
|
27
|
+
member { patch :approve; patch :reject }
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: approval_engine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Harry-kp
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.0.8
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 7.0.8
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: shiny_json_logic
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.3'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.3'
|
|
46
|
+
description: |-
|
|
47
|
+
A mountable Rails engine for human-in-the-loop approval flows: an
|
|
48
|
+
append-only ledger, dynamic JSON-Logic routing, consensus (any/all/majority),
|
|
49
|
+
delegation, and a transactional outbox — without forcing Redis or Sidekiq.
|
|
50
|
+
email:
|
|
51
|
+
- chaudharyharshit9@gmail.com
|
|
52
|
+
executables: []
|
|
53
|
+
extensions: []
|
|
54
|
+
extra_rdoc_files: []
|
|
55
|
+
files:
|
|
56
|
+
- CHANGELOG.md
|
|
57
|
+
- MIT-LICENSE
|
|
58
|
+
- README.md
|
|
59
|
+
- app/controllers/approval_engine/application_controller.rb
|
|
60
|
+
- app/controllers/approval_engine/approvals_controller.rb
|
|
61
|
+
- app/helpers/approval_engine/application_helper.rb
|
|
62
|
+
- app/jobs/approval_engine/application_job.rb
|
|
63
|
+
- app/jobs/approval_engine/process_outbox_job.rb
|
|
64
|
+
- app/jobs/approval_engine/timeout_sweep_job.rb
|
|
65
|
+
- app/models/approval_engine/application_record.rb
|
|
66
|
+
- app/models/approval_engine/approval.rb
|
|
67
|
+
- app/models/approval_engine/approval_plan.rb
|
|
68
|
+
- app/models/approval_engine/audit_log.rb
|
|
69
|
+
- app/models/approval_engine/consensus.rb
|
|
70
|
+
- app/models/approval_engine/delegation.rb
|
|
71
|
+
- app/models/approval_engine/history.rb
|
|
72
|
+
- app/models/approval_engine/outbox_event.rb
|
|
73
|
+
- app/models/approval_engine/step.rb
|
|
74
|
+
- app/models/approval_engine/template_step.rb
|
|
75
|
+
- app/models/approval_engine/track.rb
|
|
76
|
+
- app/models/approval_engine/track_template.rb
|
|
77
|
+
- app/models/approval_engine/trigger_rule.rb
|
|
78
|
+
- app/models/concerns/approval_engine/approvable.rb
|
|
79
|
+
- app/services/approval_engine/approval_builder.rb
|
|
80
|
+
- app/services/approval_engine/iteration_builder.rb
|
|
81
|
+
- app/services/approval_engine/rule_evaluator.rb
|
|
82
|
+
- app/views/approval_engine/approvals/index.html.erb
|
|
83
|
+
- app/views/approval_engine/approvals/show.html.erb
|
|
84
|
+
- app/views/layouts/approval_engine/dashboard.html.erb
|
|
85
|
+
- config/routes.rb
|
|
86
|
+
- db/migrate/20260614103434_create_approval_engine_core_tables.rb
|
|
87
|
+
- db/migrate/20260614103435_create_approval_engine_infrastructure_tables.rb
|
|
88
|
+
- db/migrate/20260614104809_create_approval_engine_blueprint_tables.rb
|
|
89
|
+
- db/migrate/20260616000001_add_trigger_rule_to_approvals.rb
|
|
90
|
+
- db/migrate/20260617000001_add_approvals_required_to_approvals.rb
|
|
91
|
+
- db/migrate/20260617000002_add_delivery_tracking_to_outbox_events.rb
|
|
92
|
+
- lib/approval_engine.rb
|
|
93
|
+
- lib/approval_engine/approval_exposure.rb
|
|
94
|
+
- lib/approval_engine/configuration.rb
|
|
95
|
+
- lib/approval_engine/engine.rb
|
|
96
|
+
- lib/approval_engine/model_extensions.rb
|
|
97
|
+
- lib/approval_engine/version.rb
|
|
98
|
+
- lib/generators/approval_engine/install/install_generator.rb
|
|
99
|
+
- lib/generators/approval_engine/install/templates/POST_INSTALL
|
|
100
|
+
- lib/generators/approval_engine/install/templates/approval_engine.rb
|
|
101
|
+
- lib/generators/approval_engine/views/templates/approvals/index.html.erb
|
|
102
|
+
- lib/generators/approval_engine/views/templates/approvals_controller.rb
|
|
103
|
+
- lib/generators/approval_engine/views/views_generator.rb
|
|
104
|
+
homepage: https://github.com/Harry-kp/approval_engine
|
|
105
|
+
licenses:
|
|
106
|
+
- MIT
|
|
107
|
+
metadata:
|
|
108
|
+
source_code_uri: https://github.com/Harry-kp/approval_engine
|
|
109
|
+
changelog_uri: https://github.com/Harry-kp/approval_engine/blob/main/CHANGELOG.md
|
|
110
|
+
bug_tracker_uri: https://github.com/Harry-kp/approval_engine/issues
|
|
111
|
+
rubygems_mfa_required: 'true'
|
|
112
|
+
rdoc_options: []
|
|
113
|
+
require_paths:
|
|
114
|
+
- lib
|
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: 3.1.0
|
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
requirements: []
|
|
126
|
+
rubygems_version: 3.6.9
|
|
127
|
+
specification_version: 4
|
|
128
|
+
summary: Multi-tenant, immutable-ledger approval flows for Rails.
|
|
129
|
+
test_files: []
|