decision_agent 0.1.1 → 0.1.2
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/README.md +138 -1000
- data/bin/decision_agent +5 -0
- data/lib/decision_agent/errors.rb +12 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +105 -0
- data/lib/decision_agent/versioning/adapter.rb +102 -0
- data/lib/decision_agent/versioning/file_storage_adapter.rb +182 -0
- data/lib/decision_agent/versioning/version_manager.rb +135 -0
- data/lib/decision_agent/web/public/app.js +318 -0
- data/lib/decision_agent/web/public/index.html +55 -0
- data/lib/decision_agent/web/public/styles.css +219 -0
- data/lib/decision_agent/web/server.rb +166 -1
- data/lib/decision_agent.rb +4 -0
- data/lib/generators/decision_agent/install/install_generator.rb +40 -0
- data/lib/generators/decision_agent/install/templates/README +47 -0
- data/lib/generators/decision_agent/install/templates/migration.rb +26 -0
- data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +60 -0
- data/spec/versioning_spec.rb +673 -0
- metadata +17 -7
|
@@ -12,7 +12,7 @@ module DecisionAgent
|
|
|
12
12
|
# Enable CORS for API calls
|
|
13
13
|
before do
|
|
14
14
|
headers["Access-Control-Allow-Origin"] = "*"
|
|
15
|
-
headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
|
|
15
|
+
headers["Access-Control-Allow-Methods"] = "GET, POST, DELETE, OPTIONS"
|
|
16
16
|
headers["Access-Control-Allow-Headers"] = "Content-Type"
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -223,8 +223,173 @@ module DecisionAgent
|
|
|
223
223
|
{ status: "ok", version: DecisionAgent::VERSION }.to_json
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# Versioning API endpoints
|
|
227
|
+
|
|
228
|
+
# Create a new version
|
|
229
|
+
post "/api/versions" do
|
|
230
|
+
content_type :json
|
|
231
|
+
|
|
232
|
+
begin
|
|
233
|
+
request_body = request.body.read
|
|
234
|
+
data = JSON.parse(request_body)
|
|
235
|
+
|
|
236
|
+
rule_id = data["rule_id"]
|
|
237
|
+
rule_content = data["content"]
|
|
238
|
+
created_by = data["created_by"] || "system"
|
|
239
|
+
changelog = data["changelog"]
|
|
240
|
+
|
|
241
|
+
version = version_manager.save_version(
|
|
242
|
+
rule_id: rule_id,
|
|
243
|
+
rule_content: rule_content,
|
|
244
|
+
created_by: created_by,
|
|
245
|
+
changelog: changelog
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
status 201
|
|
249
|
+
version.to_json
|
|
250
|
+
|
|
251
|
+
rescue => e
|
|
252
|
+
status 500
|
|
253
|
+
{ error: e.message }.to_json
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# List all versions for a rule
|
|
258
|
+
get "/api/rules/:rule_id/versions" do
|
|
259
|
+
content_type :json
|
|
260
|
+
|
|
261
|
+
begin
|
|
262
|
+
rule_id = params[:rule_id]
|
|
263
|
+
limit = params[:limit]&.to_i
|
|
264
|
+
|
|
265
|
+
versions = version_manager.get_versions(rule_id: rule_id, limit: limit)
|
|
266
|
+
|
|
267
|
+
versions.to_json
|
|
268
|
+
|
|
269
|
+
rescue => e
|
|
270
|
+
status 500
|
|
271
|
+
{ error: e.message }.to_json
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Get version history with metadata
|
|
276
|
+
get "/api/rules/:rule_id/history" do
|
|
277
|
+
content_type :json
|
|
278
|
+
|
|
279
|
+
begin
|
|
280
|
+
rule_id = params[:rule_id]
|
|
281
|
+
history = version_manager.get_history(rule_id: rule_id)
|
|
282
|
+
|
|
283
|
+
history.to_json
|
|
284
|
+
|
|
285
|
+
rescue => e
|
|
286
|
+
status 500
|
|
287
|
+
{ error: e.message }.to_json
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get a specific version
|
|
292
|
+
get "/api/versions/:version_id" do
|
|
293
|
+
content_type :json
|
|
294
|
+
|
|
295
|
+
begin
|
|
296
|
+
version_id = params[:version_id]
|
|
297
|
+
version = version_manager.get_version(version_id: version_id)
|
|
298
|
+
|
|
299
|
+
if version
|
|
300
|
+
version.to_json
|
|
301
|
+
else
|
|
302
|
+
status 404
|
|
303
|
+
{ error: "Version not found" }.to_json
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
rescue => e
|
|
307
|
+
status 500
|
|
308
|
+
{ error: e.message }.to_json
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Activate a version (rollback)
|
|
313
|
+
post "/api/versions/:version_id/activate" do
|
|
314
|
+
content_type :json
|
|
315
|
+
|
|
316
|
+
begin
|
|
317
|
+
version_id = params[:version_id]
|
|
318
|
+
request_body = request.body.read
|
|
319
|
+
data = request_body.empty? ? {} : JSON.parse(request_body)
|
|
320
|
+
performed_by = data["performed_by"] || "system"
|
|
321
|
+
|
|
322
|
+
version = version_manager.rollback(
|
|
323
|
+
version_id: version_id,
|
|
324
|
+
performed_by: performed_by
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
version.to_json
|
|
328
|
+
|
|
329
|
+
rescue => e
|
|
330
|
+
status 500
|
|
331
|
+
{ error: e.message }.to_json
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Compare two versions
|
|
336
|
+
get "/api/versions/:version_id_1/compare/:version_id_2" do
|
|
337
|
+
content_type :json
|
|
338
|
+
|
|
339
|
+
begin
|
|
340
|
+
version_id_1 = params[:version_id_1]
|
|
341
|
+
version_id_2 = params[:version_id_2]
|
|
342
|
+
|
|
343
|
+
comparison = version_manager.compare(
|
|
344
|
+
version_id_1: version_id_1,
|
|
345
|
+
version_id_2: version_id_2
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if comparison
|
|
349
|
+
comparison.to_json
|
|
350
|
+
else
|
|
351
|
+
status 404
|
|
352
|
+
{ error: "One or both versions not found" }.to_json
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
rescue => e
|
|
356
|
+
status 500
|
|
357
|
+
{ error: e.message }.to_json
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Delete a version
|
|
362
|
+
delete "/api/versions/:version_id" do
|
|
363
|
+
content_type :json
|
|
364
|
+
|
|
365
|
+
begin
|
|
366
|
+
version_id = params[:version_id]
|
|
367
|
+
|
|
368
|
+
version_manager.delete_version(version_id: version_id)
|
|
369
|
+
|
|
370
|
+
status 200
|
|
371
|
+
{ success: true, message: "Version deleted successfully" }.to_json
|
|
372
|
+
|
|
373
|
+
rescue DecisionAgent::NotFoundError => e
|
|
374
|
+
status 404
|
|
375
|
+
{ error: e.message }.to_json
|
|
376
|
+
|
|
377
|
+
rescue DecisionAgent::ValidationError => e
|
|
378
|
+
status 422
|
|
379
|
+
{ error: e.message }.to_json
|
|
380
|
+
|
|
381
|
+
rescue => e
|
|
382
|
+
status 500
|
|
383
|
+
{ error: e.message }.to_json
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
226
387
|
private
|
|
227
388
|
|
|
389
|
+
def version_manager
|
|
390
|
+
@version_manager ||= DecisionAgent::Versioning::VersionManager.new
|
|
391
|
+
end
|
|
392
|
+
|
|
228
393
|
def parse_validation_errors(error_message)
|
|
229
394
|
# Extract individual errors from the formatted error message
|
|
230
395
|
errors = []
|
data/lib/decision_agent.rb
CHANGED
|
@@ -25,5 +25,9 @@ require_relative "decision_agent/audit/logger_adapter"
|
|
|
25
25
|
|
|
26
26
|
require_relative "decision_agent/replay/replay"
|
|
27
27
|
|
|
28
|
+
require_relative "decision_agent/versioning/adapter"
|
|
29
|
+
require_relative "decision_agent/versioning/file_storage_adapter"
|
|
30
|
+
require_relative "decision_agent/versioning/version_manager"
|
|
31
|
+
|
|
28
32
|
module DecisionAgent
|
|
29
33
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
require 'rails/generators/migration'
|
|
3
|
+
|
|
4
|
+
module DecisionAgent
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
include Rails::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Installs DecisionAgent models and migrations for Rails"
|
|
12
|
+
|
|
13
|
+
def self.next_migration_number(dirname)
|
|
14
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
15
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def copy_migration
|
|
19
|
+
migration_template "migration.rb",
|
|
20
|
+
"db/migrate/create_decision_agent_tables.rb",
|
|
21
|
+
migration_version: migration_version
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def copy_models
|
|
25
|
+
copy_file "rule.rb", "app/models/rule.rb"
|
|
26
|
+
copy_file "rule_version.rb", "app/models/rule_version.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show_readme
|
|
30
|
+
readme "README" if behavior == :invoke
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def migration_version
|
|
36
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
DecisionAgent has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run the migrations:
|
|
8
|
+
|
|
9
|
+
rails db:migrate
|
|
10
|
+
|
|
11
|
+
2. The following models have been created:
|
|
12
|
+
- Rule (app/models/rule.rb)
|
|
13
|
+
- RuleVersion (app/models/rule_version.rb)
|
|
14
|
+
|
|
15
|
+
3. Start using the versioning system:
|
|
16
|
+
|
|
17
|
+
# Create a rule with a version
|
|
18
|
+
rule = Rule.create!(
|
|
19
|
+
rule_id: 'approval_rule_001',
|
|
20
|
+
ruleset: 'approval',
|
|
21
|
+
description: 'Approval decision rules'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Create a version
|
|
25
|
+
rule.create_version(
|
|
26
|
+
content: { /* your rule JSON */ },
|
|
27
|
+
created_by: 'admin',
|
|
28
|
+
changelog: 'Initial version'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Or use the VersionManager directly
|
|
32
|
+
manager = DecisionAgent::Versioning::VersionManager.new
|
|
33
|
+
manager.save_version(
|
|
34
|
+
rule_id: 'approval_rule_001',
|
|
35
|
+
rule_content: { /* your rule JSON */ },
|
|
36
|
+
created_by: 'admin'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
4. Optional: Mount the DecisionAgent routes in config/routes.rb:
|
|
40
|
+
|
|
41
|
+
# This is planned for a future release
|
|
42
|
+
# mount DecisionAgent::Engine => '/decision_agent'
|
|
43
|
+
|
|
44
|
+
For more information, visit:
|
|
45
|
+
https://github.com/samaswin87/decision_agent
|
|
46
|
+
|
|
47
|
+
===============================================================================
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CreateDecisionAgentTables < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
# Rules table
|
|
4
|
+
create_table :rules do |t|
|
|
5
|
+
t.string :rule_id, null: false, index: { unique: true }
|
|
6
|
+
t.string :ruleset, null: false
|
|
7
|
+
t.text :description
|
|
8
|
+
t.string :status, default: 'active'
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Rule versions table
|
|
13
|
+
create_table :rule_versions do |t|
|
|
14
|
+
t.string :rule_id, null: false, index: true
|
|
15
|
+
t.integer :version_number, null: false
|
|
16
|
+
t.text :content, null: false # JSON rule definition
|
|
17
|
+
t.string :created_by, null: false, default: 'system'
|
|
18
|
+
t.text :changelog
|
|
19
|
+
t.string :status, null: false, default: 'draft' # draft, active, archived
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_index :rule_versions, [:rule_id, :version_number], unique: true
|
|
24
|
+
add_index :rule_versions, [:rule_id, :status]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class Rule < ApplicationRecord
|
|
2
|
+
has_many :rule_versions, primary_key: :rule_id, foreign_key: :rule_id, dependent: :destroy
|
|
3
|
+
|
|
4
|
+
validates :rule_id, presence: true, uniqueness: true
|
|
5
|
+
validates :ruleset, presence: true
|
|
6
|
+
validates :status, inclusion: { in: %w[active inactive archived] }
|
|
7
|
+
|
|
8
|
+
scope :active, -> { where(status: 'active') }
|
|
9
|
+
scope :by_ruleset, ->(ruleset) { where(ruleset: ruleset) }
|
|
10
|
+
|
|
11
|
+
# Get the active version for this rule
|
|
12
|
+
def active_version
|
|
13
|
+
rule_versions.find_by(status: 'active')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get all versions ordered by version number
|
|
17
|
+
def versions
|
|
18
|
+
rule_versions.order(version_number: :desc)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Create a new version
|
|
22
|
+
def create_version(content:, created_by: 'system', changelog: nil)
|
|
23
|
+
DecisionAgent::Versioning::VersionManager.new.save_version(
|
|
24
|
+
rule_id: rule_id,
|
|
25
|
+
rule_content: content,
|
|
26
|
+
created_by: created_by,
|
|
27
|
+
changelog: changelog
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class RuleVersion < ApplicationRecord
|
|
2
|
+
belongs_to :rule, primary_key: :rule_id, foreign_key: :rule_id, optional: true
|
|
3
|
+
|
|
4
|
+
validates :rule_id, presence: true
|
|
5
|
+
validates :version_number, presence: true, uniqueness: { scope: :rule_id }
|
|
6
|
+
validates :content, presence: true
|
|
7
|
+
validates :status, inclusion: { in: %w[draft active archived] }
|
|
8
|
+
validates :created_by, presence: true
|
|
9
|
+
|
|
10
|
+
scope :active, -> { where(status: 'active') }
|
|
11
|
+
scope :for_rule, ->(rule_id) { where(rule_id: rule_id).order(version_number: :desc) }
|
|
12
|
+
scope :latest, -> { order(version_number: :desc).limit(1) }
|
|
13
|
+
|
|
14
|
+
before_create :set_next_version_number
|
|
15
|
+
|
|
16
|
+
# Parse the JSON content
|
|
17
|
+
def parsed_content
|
|
18
|
+
JSON.parse(content, symbolize_names: true)
|
|
19
|
+
rescue JSON::ParserError
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Set content from a hash
|
|
24
|
+
def content_hash=(hash)
|
|
25
|
+
self.content = hash.to_json
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Activate this version (deactivates others)
|
|
29
|
+
def activate!
|
|
30
|
+
transaction do
|
|
31
|
+
# Deactivate all other versions for this rule
|
|
32
|
+
self.class.where(rule_id: rule_id, status: 'active')
|
|
33
|
+
.where.not(id: id)
|
|
34
|
+
.update_all(status: 'archived')
|
|
35
|
+
|
|
36
|
+
# Activate this version
|
|
37
|
+
update!(status: 'active')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Compare with another version
|
|
42
|
+
def compare_with(other_version)
|
|
43
|
+
DecisionAgent::Versioning::VersionManager.new.compare(
|
|
44
|
+
version_id_1: id,
|
|
45
|
+
version_id_2: other_version.id
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_next_version_number
|
|
52
|
+
return if version_number.present?
|
|
53
|
+
|
|
54
|
+
last_version = self.class.where(rule_id: rule_id)
|
|
55
|
+
.order(version_number: :desc)
|
|
56
|
+
.first
|
|
57
|
+
|
|
58
|
+
self.version_number = last_version ? last_version.version_number + 1 : 1
|
|
59
|
+
end
|
|
60
|
+
end
|