saseo 0.5.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.md +60 -4
- data/db/saseo/migrate/20151201191940_add_changeset_column.rb +5 -0
- data/db/saseo/migrate/20151201212857_drop_old_data.rb +9 -0
- data/db/saseo/migrate/20151201221009_use_time_zones.rb +9 -0
- data/db/saseo/migrate/20151201224236_use_big_int_for_item_id.rb +9 -0
- data/db/saseo/schema.rb +3 -3
- data/db/saseo_source/migrate/20151028181502_initial_schema.rb +96 -16
- data/lib/generators/saseo/trigger_generator.rb +1 -1
- data/lib/saseo/config/defaults.rb +3 -1
- data/lib/saseo/extensions/active_record/adapter/mixin.rb +19 -0
- data/lib/saseo/extensions/active_record/adapter.rb +8 -0
- data/lib/saseo/extensions/active_record/migration/mixin.rb +29 -0
- data/lib/saseo/extensions/active_record/migration.rb +8 -0
- data/lib/saseo/extensions/active_record.rb +2 -8
- data/lib/saseo/models/base.rb +10 -9
- data/lib/saseo/models/source/base.rb +2 -0
- data/lib/saseo/models/source/version.rb +13 -0
- data/lib/saseo/models/version.rb +117 -1
- data/lib/saseo/persistence/persistor.rb +3 -15
- data/lib/saseo/publishing/data_change_message.rb +7 -9
- data/lib/saseo/version.rb +1 -1
- data/lib/saseo/whodunnit.rb +4 -2
- data/saseo.example.yml +2 -1
- data/saseo.gemspec +2 -0
- metadata +38 -3
- data/lib/saseo/extensions/active_record/adapter_mixin.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67445e3765a335a0ec968f0f052890632e27a2bf
|
4
|
+
data.tar.gz: 98cec85032a774a25e3ae7e5c2906dad60903ab1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95b70a7ea7208c7f1329009752fa4fb1a320bc1c9ee814bdc131acdd6743bbf1ff0e722a7df8252262077970359a3f31a71a23b86949c3815c1881362cf77f66
|
7
|
+
data.tar.gz: 6bfabc3a0858e4eb251850ef98753a3efb0766ec41f6f8f978edbc75e960d3f907e0fd20b5b024a9c4faa251b7fb8895ae598beded0e1585397aa3f407503ee2
|
data/.travis.yml
CHANGED
@@ -12,7 +12,7 @@ rvm:
|
|
12
12
|
- ruby-head
|
13
13
|
- jruby-head
|
14
14
|
script:
|
15
|
-
- bundle exec rake DATABASE=saseo_source
|
15
|
+
- bundle exec rake DATABASE=saseo_source RACK_ENV=test db:create db:migrate && bundle exec rake DATABASE=saseo RACK_ENV=test db:create db:migrate && DATABASE=saseo bundle exec rake
|
16
16
|
addons:
|
17
17
|
postgresql: "9.4"
|
18
18
|
code_climate:
|
data/README.md
CHANGED
@@ -9,6 +9,62 @@ RabbitMQ based PaperTrail replacement
|
|
9
9
|
[![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org)
|
10
10
|
|
11
11
|
|
12
|
+
## Overview
|
13
|
+
Saseo has two primary responsibilities:
|
14
|
+
|
15
|
+
1. Publishing all changes from a database
|
16
|
+
1. Persisting the changes to a separate data store
|
17
|
+
|
18
|
+
|
19
|
+
### Publishing
|
20
|
+
Saseo uses triggers to temporarily store changes to a local staging table. Saseo provides a stateless executable, `saseo_publisher`, that reads the records from that table, publishes them to a RabbitMQ Headers Exchange, then deletes them from the local staging table.
|
21
|
+
|
22
|
+
__Because the messages are published in this way, the data is available to *ANY AND ALL* clients that can connect to RabbitMQ.__
|
23
|
+
|
24
|
+
#### Published Message Format
|
25
|
+
|
26
|
+
| Attribute | Details | Header* | Payload** |
|
27
|
+
|-----------|---------|--------|---------|
|
28
|
+
| table_name| name of the table the record is from | X | |
|
29
|
+
| action| `INSERT`, `UPDATE`, or `DELETE`| X | |
|
30
|
+
| whodunnit | the user or other entity responsible for the change | X | |
|
31
|
+
| item_id | value, if any, of the id field from the record| X | |
|
32
|
+
| item_uuid | value, if any, of the id field from the record| X | |
|
33
|
+
| transaction_id | transaction_id from the source DB | X | |
|
34
|
+
| locale | I18n.locale of the publisher| X | |
|
35
|
+
| database | source database name for the record| X | |
|
36
|
+
| schema | source schema name for the record| X | |
|
37
|
+
| id | uuid of the saseo version| | X |
|
38
|
+
| old_data | JSON representation of the record before the change| | X |
|
39
|
+
| new_data | JSON representation of the record after the change| | X |
|
40
|
+
| action_timestamp | timestamp from the transaction| | X |
|
41
|
+
|
42
|
+
> _* Header: attribute that can be subscribed to_
|
43
|
+
> _** Payload: attribute that can NOT be subscribed to, only accessible once the message is received_
|
44
|
+
|
45
|
+
### Persistence
|
46
|
+
Saseo provides a stateless executable, `saseo_consumer`, that subscribes to RabbitMQ to pick up the canges published by `saseo_publisher` and persists them to a database specified by config. Saseo currently creates one table for every tracked table.
|
47
|
+
|
48
|
+
|
49
|
+
#### Saseo table structure
|
50
|
+
| Column | Type | Modifiers |
|
51
|
+
|----------------------------------|--------------------------|-------------------------------------|
|
52
|
+
| id | uuid | not null default uuid_generate_v4() |
|
53
|
+
| transaction_id | bigint | not null |
|
54
|
+
| item_id | bigint | |
|
55
|
+
| item_uuid | uuid | |
|
56
|
+
| action | character varying | not null |
|
57
|
+
| action_timestamp | timestamp with time zone | not null |
|
58
|
+
| whodunnit | character varying | not null |
|
59
|
+
| changeset | jsonb | not null |
|
60
|
+
| new_data | jsonb | |
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
### Sample Setup
|
65
|
+
![](https://raw.githubusercontent.com/avantcredit/saseo/master/sample_saseo_setup.png)
|
66
|
+
|
67
|
+
|
12
68
|
## Installation
|
13
69
|
|
14
70
|
Add this line to your application's Gemfile:
|
@@ -25,19 +81,19 @@ Or install it yourself as:
|
|
25
81
|
|
26
82
|
$ gem install saseo
|
27
83
|
|
84
|
+
|
28
85
|
## Usage
|
29
86
|
|
30
87
|
TODO: Write usage instructions here
|
31
88
|
|
32
|
-
|
33
|
-
![](https://raw.githubusercontent.com/avantcredit/saseo/master/sample_saseo_setup.png)
|
89
|
+
|
34
90
|
## Development
|
35
91
|
|
36
92
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
37
93
|
|
38
94
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
39
95
|
|
40
|
-
## Contributing
|
41
96
|
|
42
|
-
|
97
|
+
## Contributing
|
43
98
|
|
99
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/avantcredit/saseo.
|
data/db/saseo/schema.rb
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
#
|
12
12
|
# It's strongly recommended that you check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
14
|
+
ActiveRecord::Schema.define(version: 20151201224236) do
|
15
15
|
|
16
16
|
# These are extensions that must be enabled in order to support this database
|
17
17
|
enable_extension "plpgsql"
|
@@ -19,14 +19,14 @@ ActiveRecord::Schema.define(version: 20151116235958) do
|
|
19
19
|
|
20
20
|
create_table "saseo_versions", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t|
|
21
21
|
t.integer "transaction_id", limit: 8, null: false
|
22
|
-
t.integer "item_id"
|
22
|
+
t.integer "item_id", limit: 8
|
23
23
|
t.uuid "item_uuid"
|
24
24
|
t.string "table_name", null: false
|
25
25
|
t.string "action", null: false
|
26
26
|
t.datetime "action_timestamp", null: false
|
27
27
|
t.string "whodunnit", null: false
|
28
|
-
t.jsonb "old_data"
|
29
28
|
t.jsonb "new_data"
|
29
|
+
t.jsonb "changeset", null: false
|
30
30
|
end
|
31
31
|
|
32
32
|
add_index "saseo_versions", ["table_name", "action_timestamp", "item_id"], name: "saseo_item_id_idx", using: :btree
|
@@ -1,22 +1,102 @@
|
|
1
1
|
class InitialSchema < ActiveRecord::Migration
|
2
2
|
def up
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
|
4
|
+
uuid_extension_sql = 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
|
5
|
+
|
6
|
+
audit_table_sql = <<SQL
|
7
|
+
CREATE schema IF NOT EXISTS saseo;
|
8
|
+
DROP TABLE IF EXISTS saseo.saseo_source_versions;
|
9
|
+
|
10
|
+
CREATE TABLE saseo.saseo_source_versions (
|
11
|
+
id uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
|
12
|
+
transaction_id bigint NOT NULL,
|
13
|
+
table_name text NOT NULL,
|
14
|
+
action_timestamp timestamp WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
15
|
+
action text NOT NULL CHECK (action IN ('INSERT','DELETE','UPDATE')),
|
16
|
+
whodunnit text NOT NULL,
|
17
|
+
old_data jsonb,
|
18
|
+
new_data jsonb
|
19
|
+
);
|
20
|
+
|
21
|
+
SQL
|
22
|
+
# originally copied from:
|
23
|
+
# https://wiki.postgresql.org/wiki/Audit_trigger
|
24
|
+
#
|
25
|
+
# modified to use NOTIFY
|
26
|
+
#
|
27
|
+
# a trigger must be added to every table
|
28
|
+
# CREATE TRIGGER identities_saseo_trigger AFTER INSERT OR UPDATE OR DELETE ON identities FOR EACH ROW EXECUTE PROCEDURE saseo.audit_func();
|
29
|
+
whodunnit_sql = <<SQL
|
30
|
+
CREATE OR REPLACE FUNCTION saseo.whodunnit() RETURNS TEXT AS $body$
|
31
|
+
BEGIN
|
32
|
+
return current_setting('saseo.whodunnit');
|
33
|
+
EXCEPTION WHEN undefined_object THEN
|
34
|
+
return current_user;
|
35
|
+
END;
|
36
|
+
$body$
|
37
|
+
LANGUAGE plpgsql
|
38
|
+
SQL
|
39
|
+
audit_sql = <<SQL
|
40
|
+
CREATE OR REPLACE FUNCTION saseo.audit_func() RETURNS TRIGGER AS $body$
|
41
|
+
DECLARE
|
42
|
+
v_old_data json;
|
43
|
+
v_new_data json;
|
44
|
+
v_whodunnit text;
|
45
|
+
v_audit_uuid uuid;
|
46
|
+
BEGIN
|
47
|
+
v_whodunnit := saseo.whodunnit();
|
48
|
+
v_audit_uuid := uuid_generate_v4();
|
49
|
+
|
50
|
+
IF (TG_OP = 'UPDATE') THEN
|
51
|
+
v_old_data := row_to_json(OLD);
|
52
|
+
v_new_data := row_to_json(NEW);
|
53
|
+
|
54
|
+
INSERT INTO saseo.saseo_source_versions VALUES(v_audit_uuid, txid_current(), TG_TABLE_NAME, DEFAULT, TG_OP, v_whodunnit, v_old_data::JSONB, v_new_data::JSONB);
|
55
|
+
|
56
|
+
RETURN NEW;
|
57
|
+
ELSIF (TG_OP = 'DELETE') THEN
|
58
|
+
v_old_data := row_to_json(OLD);
|
59
|
+
|
60
|
+
INSERT INTO saseo.saseo_source_versions VALUES(v_audit_uuid, txid_current(), TG_TABLE_NAME, DEFAULT, TG_OP, v_whodunnit, v_old_data::JSONB, NULL);
|
61
|
+
|
62
|
+
RETURN OLD;
|
63
|
+
ELSIF (TG_OP = 'INSERT') THEN
|
64
|
+
v_new_data := row_to_json(NEW);
|
65
|
+
|
66
|
+
INSERT INTO saseo.saseo_source_versions VALUES(v_audit_uuid, txid_current(), TG_TABLE_NAME, DEFAULT, TG_OP, v_whodunnit, NULL, v_new_data::JSONB);
|
67
|
+
|
68
|
+
RETURN NEW;
|
69
|
+
ELSE
|
70
|
+
RAISE WARNING '[SASEO.AUDIT_FUNC] - Other action occurred: %, at %',TG_OP,now();
|
71
|
+
RETURN NULL;
|
72
|
+
END IF;
|
73
|
+
|
74
|
+
EXCEPTION
|
75
|
+
WHEN data_exception THEN
|
76
|
+
RAISE WARNING '[SASEO.AUDIT_FUNC] - UDF ERROR [DATA EXCEPTION] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
|
77
|
+
RETURN NULL;
|
78
|
+
WHEN unique_violation THEN
|
79
|
+
RAISE WARNING '[SASEO.AUDIT_FUNC] - UDF ERROR [UNIQUE] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
|
80
|
+
RETURN NULL;
|
81
|
+
WHEN OTHERS THEN
|
82
|
+
RAISE WARNING '[SASEO.AUDIT_FUNC] - UDF ERROR [OTHER] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
|
83
|
+
RETURN NULL;
|
84
|
+
END;
|
85
|
+
$body$
|
86
|
+
LANGUAGE plpgsql
|
87
|
+
SQL
|
88
|
+
execute(uuid_extension_sql)
|
89
|
+
execute(audit_table_sql)
|
90
|
+
execute(whodunnit_sql)
|
91
|
+
execute(audit_sql)
|
15
92
|
end
|
16
93
|
|
17
94
|
def down
|
18
|
-
|
19
|
-
|
20
|
-
execute
|
95
|
+
execute('DROP FUNCTION IF EXISTS saseo.audit_func() CASCADE')
|
96
|
+
execute('DROP FUNCTION IF EXISTS saseo.whodunnit() CASCADE')
|
97
|
+
execute('DROP TABLE IF EXISTS saseo.saseo_source_versions CASCADE')
|
98
|
+
execute('DROP SCHEMA IF EXISTS saseo CASCADE')
|
99
|
+
execute('DROP EXTENSION IF EXISTS "uuid-ossp" CASCADE')
|
100
|
+
|
21
101
|
end
|
22
|
-
end
|
102
|
+
end
|
@@ -15,7 +15,7 @@ module Saseo
|
|
15
15
|
def create_migration_file
|
16
16
|
tables = table_names.dup
|
17
17
|
tables = Saseo::Models::Source::Base.connection.tables if tables.include? 'all'
|
18
|
-
tables -= [Saseo.config.source_table_name
|
18
|
+
tables -= [Saseo.config.source_table_name]
|
19
19
|
|
20
20
|
@table_names = tables
|
21
21
|
add_saseo_migration("add_saseo_trigger")
|
@@ -7,11 +7,13 @@ module Saseo
|
|
7
7
|
SOURCE_DATABASE_CONFIG_PATH = 'config/database.yml'
|
8
8
|
DATABASE_URL = nil
|
9
9
|
DATABASE_CONFIG_PATH = 'saseo/config/saseo_database.yml'
|
10
|
-
|
10
|
+
TABLE_PREFIX = nil
|
11
|
+
TABLE_SUFFIX = nil
|
11
12
|
SOURCE_TABLE_SCHEMA = 'saseo'
|
12
13
|
SOURCE_TABLE_NAME = 'saseo_source_versions'
|
13
14
|
CONSUMER_PHILOTIC_SUBSCRIPTION = 'saseo_audit'
|
14
15
|
IGNORE_FIELDS_CONFIG_PATH = nil
|
16
|
+
LOCALE = nil
|
15
17
|
|
16
18
|
def defaults
|
17
19
|
@defaults ||= Hash[Saseo::Config::Defaults.constants.map do |c|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_record/base'
|
2
|
+
require 'saseo/whodunnit'
|
3
|
+
|
4
|
+
module Saseo
|
5
|
+
module Extensions
|
6
|
+
module ActiveRecord
|
7
|
+
module Adapter
|
8
|
+
module Mixin
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def begin_db_transaction
|
12
|
+
execute 'BEGIN'
|
13
|
+
Saseo::Whodunnit.set_db_whodunnit
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'saseo/extensions/active_record/detector'
|
2
|
+
|
3
|
+
if Saseo::Extensions::ActiveRecord::Detector.active_record_detected? && Saseo::Extensions::ActiveRecord::Detector.active_record_version >= 3
|
4
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
5
|
+
require 'saseo/extensions/active_record/adapter/mixin'
|
6
|
+
|
7
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend ::Saseo::Extensions::ActiveRecord::Adapter::Mixin
|
8
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'active_record/base'
|
2
|
+
require 'saseo/whodunnit'
|
3
|
+
require 'saseo/config'
|
4
|
+
|
5
|
+
module Saseo
|
6
|
+
module Extensions
|
7
|
+
module ActiveRecord
|
8
|
+
module Migration
|
9
|
+
module Mixin
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def create_table(table_name, options = {}, &block)
|
13
|
+
super
|
14
|
+
add_trigger(table_name) if connection.schema_exists?(Saseo.config.source_table_schema)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_trigger(table_name)
|
18
|
+
drop_trigger(table_name)
|
19
|
+
execute("CREATE TRIGGER #{table_name}_saseo_trigger AFTER INSERT OR UPDATE OR DELETE ON #{table_name} FOR EACH ROW EXECUTE PROCEDURE saseo.audit_func()")
|
20
|
+
end
|
21
|
+
|
22
|
+
def drop_trigger(table_name)
|
23
|
+
execute("DROP TRIGGER IF EXISTS #{table_name}_saseo_trigger ON #{table_name} CASCADE")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'saseo/extensions/active_record/detector'
|
2
|
+
|
3
|
+
if Saseo::Extensions::ActiveRecord::Detector.active_record_detected? && Saseo::Extensions::ActiveRecord::Detector.active_record_version >= 3
|
4
|
+
require 'active_record/migration'
|
5
|
+
require 'saseo/extensions/active_record/migration/mixin'
|
6
|
+
|
7
|
+
::ActiveRecord::Migration.prepend ::Saseo::Extensions::ActiveRecord::Migration::Mixin
|
8
|
+
end
|
@@ -1,8 +1,2 @@
|
|
1
|
-
require 'saseo/extensions/active_record/
|
2
|
-
|
3
|
-
if Saseo::Extensions::ActiveRecord::Detector.active_record_detected? && Saseo::Extensions::ActiveRecord::Detector.active_record_version >= 3
|
4
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
5
|
-
require 'saseo/extensions/active_record/adapter_mixin'
|
6
|
-
|
7
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend ::Saseo::Extensions::ActiveRecord::AdapterMixin
|
8
|
-
end
|
1
|
+
require 'saseo/extensions/active_record/adapter'
|
2
|
+
require 'saseo/extensions/active_record/migration'
|
data/lib/saseo/models/base.rb
CHANGED
@@ -51,18 +51,23 @@ module Saseo
|
|
51
51
|
end
|
52
52
|
|
53
53
|
validates :transaction_id, presence: true
|
54
|
-
validates :table_name, presence: true
|
55
54
|
validates :action_timestamp, presence: true
|
56
55
|
validates :action, presence: true, inclusion: {in: %w[INSERT UPDATE DELETE]}
|
57
56
|
validates :whodunnit, presence: true
|
58
|
-
|
59
|
-
validates :new_data, presence: true, unless: ->(version) { version.old_data.present? }
|
57
|
+
|
60
58
|
|
61
59
|
validate :validate_has_changes
|
62
60
|
|
63
61
|
establish_connection database_config
|
64
62
|
|
65
|
-
def
|
63
|
+
def has_changeset?
|
64
|
+
changeset.present?
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_changeset
|
68
|
+
old_data = (self.old_data && self.old_data.stringify_keys)
|
69
|
+
new_data = (self.new_data && self.new_data.stringify_keys)
|
70
|
+
|
66
71
|
old_keys = old_data ? old_data.keys : []
|
67
72
|
new_keys = new_data ? new_data.keys : []
|
68
73
|
(old_keys | new_keys).reduce({}) do |changes, key|
|
@@ -73,13 +78,9 @@ module Saseo
|
|
73
78
|
end
|
74
79
|
end
|
75
80
|
|
76
|
-
def has_changes?
|
77
|
-
changes.any?
|
78
|
-
end
|
79
|
-
|
80
81
|
private
|
81
82
|
def validate_has_changes
|
82
|
-
errors.add(:new_data, 'can not be the same as old_data') unless
|
83
|
+
errors.add(:new_data, 'can not be the same as old_data') unless has_changeset?
|
83
84
|
end
|
84
85
|
end
|
85
86
|
end
|
@@ -6,6 +6,19 @@ module Saseo
|
|
6
6
|
module Source
|
7
7
|
class Version < Saseo::Models::Source::Base
|
8
8
|
self.table_name = "#{Saseo.config.source_table_schema}.#{Saseo.config.source_table_name}"
|
9
|
+
|
10
|
+
validates :old_data, presence: true, unless: ->(version) { version.new_data.present? }
|
11
|
+
validates :new_data, presence: true, unless: ->(version) { version.old_data.present? }
|
12
|
+
|
13
|
+
alias_method :changeset, :build_changeset
|
14
|
+
|
15
|
+
def database
|
16
|
+
self.class.connection.current_database
|
17
|
+
end
|
18
|
+
|
19
|
+
def schema
|
20
|
+
self.class.connection.current_schema
|
21
|
+
end
|
9
22
|
end
|
10
23
|
end
|
11
24
|
end
|
data/lib/saseo/models/version.rb
CHANGED
@@ -1,10 +1,126 @@
|
|
1
1
|
require 'saseo/models/base'
|
2
2
|
require 'saseo/config'
|
3
3
|
|
4
|
+
require 'active_record/migration'
|
5
|
+
|
4
6
|
module Saseo
|
5
7
|
module Models
|
6
8
|
class Version < Saseo::Models::Base
|
7
|
-
self.
|
9
|
+
self.abstract_class = true
|
10
|
+
|
11
|
+
attr_reader :old_data
|
12
|
+
|
13
|
+
validates :changeset, presence: true
|
14
|
+
before_validation :build_changeset!
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def column_definitions
|
19
|
+
{
|
20
|
+
id: {type: :uuid, args: {default: 'uuid_generate_v4()'}},
|
21
|
+
transaction_id: {type: :integer, args: [:transaction_id, {limit: 8, null: false}]},
|
22
|
+
item_id: {type: :integer, args: [:item_id, limit: 8]},
|
23
|
+
item_uuid: {type: :uuid, args: [:item_uuid]},
|
24
|
+
action: {type: :string, args: [:action, {null: false}]},
|
25
|
+
action_timestamp: {type: :column, args: [:action_timestamp, 'TIMESTAMP WITH TIME ZONE', {null: false}]},
|
26
|
+
whodunnit: {type: :string, args: [:whodunnit, {null: false}]},
|
27
|
+
changeset: {type: :jsonb, args: [:changeset, {null: false}]},
|
28
|
+
new_data: {type: :jsonb, args: [:new_data]},
|
29
|
+
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def migration_indexes
|
34
|
+
quoted_table_name = migration.connection.quote_table_name(table_name)
|
35
|
+
|
36
|
+
# use raw execute for indexes to let PostgreSQL generate index names
|
37
|
+
[
|
38
|
+
"CREATE INDEX ON #{quoted_table_name} (transaction_id)",
|
39
|
+
"CREATE INDEX ON #{quoted_table_name} (item_id, action_timestamp)",
|
40
|
+
"CREATE INDEX ON #{quoted_table_name} (item_uuid, action_timestamp)",
|
41
|
+
"CREATE INDEX ON #{quoted_table_name} USING GIN(changeset)",
|
42
|
+
"CREATE INDEX ON #{quoted_table_name} USING GIN(new_data)",
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
def migration
|
47
|
+
@migration ||= ActiveRecord::Migration.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def class_for_table(table_name)
|
51
|
+
table_class_name = "#{Saseo.config.table_prefix}#{table_name}#{Saseo.config.table_suffix}".classify
|
52
|
+
|
53
|
+
return Saseo::Models::Version.const_get(table_class_name) if Saseo::Models::Version.const_defined?(table_class_name)
|
54
|
+
Class.new(Saseo::Models::Version).tap do |klass|
|
55
|
+
Saseo::Models::Version.const_set(table_class_name, klass)
|
56
|
+
klass.table_name = table_name
|
57
|
+
klass.ensure_table!
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def unmarshal_from_message(message)
|
63
|
+
class_for_table(message.table_name).new.tap do |version|
|
64
|
+
|
65
|
+
column_definitions.each_key do |attr|
|
66
|
+
version.send("#{attr}=", message.send(attr)) if message.respond_to?(attr)
|
67
|
+
end
|
68
|
+
|
69
|
+
# ActiveRecord 3 doesn't handle jsonb columns properly
|
70
|
+
version.old_data = message.old_data && Oj.dump(message.old_data)
|
71
|
+
version.new_data = message.new_data && Oj.dump(message.new_data)
|
72
|
+
|
73
|
+
version
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def ensure_table!
|
78
|
+
ActiveRecord::Base.establish_connection database_config
|
79
|
+
return if connection.table_exists? table_name
|
80
|
+
|
81
|
+
create_table!
|
82
|
+
create_table_indexes!
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_table!
|
86
|
+
migration.transaction do
|
87
|
+
migration.create_table table_name, create_table_options do |t|
|
88
|
+
|
89
|
+
column_definitions.each_pair do |column, definition|
|
90
|
+
next if column == :id
|
91
|
+
t.send(definition[:type], *definition[:args])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_table_options
|
98
|
+
{id: column_definitions[:id][:type], force: false}.merge(column_definitions[:id][:args])
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_table_indexes!
|
102
|
+
migration_indexes.each do |index|
|
103
|
+
migration.execute index
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def changeset
|
109
|
+
self[:changeset] || build_changeset!
|
110
|
+
end
|
111
|
+
|
112
|
+
def old_data=(val)
|
113
|
+
@old_data = val.is_a?(String) ? Oj.load(val) : val
|
114
|
+
end
|
115
|
+
|
116
|
+
def table_name
|
117
|
+
self.class.table_name
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
def build_changeset!
|
122
|
+
self.changeset = build_changeset
|
123
|
+
end
|
8
124
|
end
|
9
125
|
end
|
10
126
|
end
|
@@ -34,25 +34,13 @@ module Saseo
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def persist_message!(message)
|
37
|
-
save_version!(
|
38
|
-
end
|
39
|
-
|
40
|
-
def build_version(message)
|
41
|
-
Saseo::Models::Version.new.tap do |version|
|
42
|
-
|
43
|
-
version_attributes.each do |attr|
|
44
|
-
version.send("#{attr}=", message.send(attr))
|
45
|
-
end
|
46
|
-
|
47
|
-
# ActiveRecord 3 doesn't handle jsonb columns properly
|
48
|
-
version.old_data = message.old_data && Oj.dump(message.old_data)
|
49
|
-
version.new_data = message.new_data && Oj.dump(message.new_data)
|
50
|
-
end
|
37
|
+
save_version!(Saseo::Models::Version.unmarshal_from_message(message))
|
51
38
|
end
|
52
39
|
|
53
40
|
def save_version!(version)
|
54
|
-
if version.
|
41
|
+
if version.has_changeset?
|
55
42
|
begin
|
43
|
+
version.class.ensure_table!
|
56
44
|
version.save!
|
57
45
|
logger.debug { "saved version: #{version.id}" }
|
58
46
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'philotic/message'
|
2
|
+
require 'saseo/config'
|
2
3
|
require 'oj'
|
3
4
|
|
4
5
|
module Saseo
|
@@ -8,7 +9,7 @@ module Saseo
|
|
8
9
|
COMPONENT = :saseo
|
9
10
|
MESSAGE_TYPE = :'saseo.record_audit'
|
10
11
|
|
11
|
-
attr_routable :table_name, :action, :whodunnit, :item_id, :item_uuid, :transaction_id
|
12
|
+
attr_routable :table_name, :action, :whodunnit, :item_id, :item_uuid, :transaction_id, :locale, :database, :schema
|
12
13
|
attr_payload :id, :old_data, :new_data, :action_timestamp
|
13
14
|
|
14
15
|
def initialize(version)
|
@@ -17,13 +18,16 @@ module Saseo
|
|
17
18
|
@id = version.id
|
18
19
|
@transaction_id = version.transaction_id
|
19
20
|
@table_name = version.table_name
|
21
|
+
@locale = Saseo.config.locale
|
22
|
+
@database = version.database
|
23
|
+
@schema = version.schema
|
20
24
|
@action = version.action
|
21
25
|
@whodunnit = version.whodunnit
|
22
26
|
@action_timestamp = version.action_timestamp
|
23
27
|
|
24
28
|
# ActiveRecord 3 doesn't handle jsonb columns properly
|
25
|
-
@old_data =
|
26
|
-
@new_data =
|
29
|
+
@old_data = version.old_data
|
30
|
+
@new_data = version.new_data
|
27
31
|
|
28
32
|
@item_id = @new_data ? @new_data['id'] : @old_data['id']
|
29
33
|
@item_uuid = @new_data ? @new_data['uuid'] : @old_data['uuid']
|
@@ -32,12 +36,6 @@ module Saseo
|
|
32
36
|
@philotic_message_type = MESSAGE_TYPE
|
33
37
|
|
34
38
|
end
|
35
|
-
|
36
|
-
|
37
|
-
def ensure_json_load(val)
|
38
|
-
return val unless val.is_a? String
|
39
|
-
Oj.load val
|
40
|
-
end
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
data/lib/saseo/version.rb
CHANGED
data/lib/saseo/whodunnit.rb
CHANGED
@@ -26,11 +26,13 @@ module Saseo
|
|
26
26
|
|
27
27
|
def set_db_whodunnit(who = nil)
|
28
28
|
who ||= whodunnit
|
29
|
-
connection.execute
|
29
|
+
connection && connection.execute("SET saseo.whodunnit TO '#{connection.quote_string who.to_s}'")
|
30
30
|
end
|
31
31
|
|
32
32
|
def connection
|
33
|
-
|
33
|
+
if Saseo::Extensions::ActiveRecord::Detector.active_record_detected? && ActiveRecord::Base.connected?
|
34
|
+
ActiveRecord::Base.connection
|
35
|
+
end
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
data/saseo.example.yml
CHANGED
@@ -4,7 +4,8 @@ defaults: &defaults
|
|
4
4
|
source_database_config_path: /path/to/source/db/config
|
5
5
|
database_url: postgres://user:pass@host:port/saseo
|
6
6
|
source_database_config_path: /path/to/saseo/db/config
|
7
|
-
|
7
|
+
table_prefix: saseo_
|
8
|
+
table_suffix: _versions
|
8
9
|
source_table_schema: audits
|
9
10
|
source_table_name: audits
|
10
11
|
|
data/saseo.gemspec
CHANGED
@@ -22,6 +22,8 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_development_dependency 'bundler', '~> 1.10'
|
23
23
|
spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.4'
|
24
24
|
spec.add_development_dependency 'database_cleaner'
|
25
|
+
spec.add_development_dependency 'factory_girl_rails'
|
26
|
+
spec.add_development_dependency 'faker'
|
25
27
|
spec.add_development_dependency 'generator_spec'
|
26
28
|
spec.add_development_dependency 'rake', '~> 10.0'
|
27
29
|
spec.add_development_dependency 'rspec'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saseo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Keyes
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: awesome_print
|
@@ -66,6 +66,34 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: factory_girl_rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: faker
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: generator_spec
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -231,6 +259,10 @@ files:
|
|
231
259
|
- db/saseo/migrate/20151028181502_initial_schema.rb
|
232
260
|
- db/saseo/migrate/20151108210618_add_indexes.rb
|
233
261
|
- db/saseo/migrate/20151116235958_allow_null_ids.rb
|
262
|
+
- db/saseo/migrate/20151201191940_add_changeset_column.rb
|
263
|
+
- db/saseo/migrate/20151201212857_drop_old_data.rb
|
264
|
+
- db/saseo/migrate/20151201221009_use_time_zones.rb
|
265
|
+
- db/saseo/migrate/20151201224236_use_big_int_for_item_id.rb
|
234
266
|
- db/saseo/schema.rb
|
235
267
|
- db/saseo_source/migrate/20151028181502_initial_schema.rb
|
236
268
|
- db/saseo_source/schema.rb
|
@@ -247,8 +279,11 @@ files:
|
|
247
279
|
- lib/saseo/config/saseo_source_database.yml
|
248
280
|
- lib/saseo/extensions.rb
|
249
281
|
- lib/saseo/extensions/active_record.rb
|
250
|
-
- lib/saseo/extensions/active_record/
|
282
|
+
- lib/saseo/extensions/active_record/adapter.rb
|
283
|
+
- lib/saseo/extensions/active_record/adapter/mixin.rb
|
251
284
|
- lib/saseo/extensions/active_record/detector.rb
|
285
|
+
- lib/saseo/extensions/active_record/migration.rb
|
286
|
+
- lib/saseo/extensions/active_record/migration/mixin.rb
|
252
287
|
- lib/saseo/models/base.rb
|
253
288
|
- lib/saseo/models/source/base.rb
|
254
289
|
- lib/saseo/models/source/version.rb
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'active_record/base'
|
2
|
-
require 'saseo/whodunnit'
|
3
|
-
|
4
|
-
module Saseo
|
5
|
-
module Extensions
|
6
|
-
module ActiveRecord
|
7
|
-
module AdapterMixin
|
8
|
-
extend self
|
9
|
-
|
10
|
-
def begin_db_transaction
|
11
|
-
execute 'BEGIN'
|
12
|
-
Saseo::Whodunnit.set_db_whodunnit
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|