hoardable 0.14.2 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.streerc +1 -0
- data/.tool-versions +2 -2
- data/CHANGELOG.md +19 -0
- data/Gemfile +9 -10
- data/README.md +198 -177
- data/Rakefile +22 -8
- data/lib/generators/hoardable/install_generator.rb +25 -26
- data/lib/generators/hoardable/migration_generator.rb +17 -8
- data/lib/generators/hoardable/templates/install.rb.erb +2 -25
- data/lib/generators/hoardable/templates/migration.rb.erb +7 -1
- data/lib/hoardable/arel_visitors.rb +57 -0
- data/lib/hoardable/database_client.rb +41 -23
- data/lib/hoardable/engine.rb +32 -33
- data/lib/hoardable/error.rb +4 -7
- data/lib/hoardable/finder_methods.rb +1 -3
- data/lib/hoardable/has_many.rb +6 -10
- data/lib/hoardable/has_one.rb +3 -3
- data/lib/hoardable/has_rich_text.rb +14 -7
- data/lib/hoardable/model.rb +19 -16
- data/lib/hoardable/schema_dumper.rb +25 -0
- data/lib/hoardable/schema_statements.rb +33 -0
- data/lib/hoardable/scopes.rb +22 -29
- data/lib/hoardable/source_model.rb +6 -5
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +30 -31
- data/lib/hoardable.rb +21 -18
- data/sig/hoardable.rbs +37 -12
- metadata +14 -29
- data/.rubocop.yml +0 -21
@@ -1,47 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "rails/generators"
|
4
4
|
|
5
5
|
module Hoardable
|
6
6
|
# Generates an initializer file for {Hoardable} configuration and a migration with a PostgreSQL
|
7
7
|
# function.
|
8
8
|
class InstallGenerator < Rails::Generators::Base
|
9
|
-
source_root File.expand_path(
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
10
|
include Rails::Generators::Migration
|
11
|
-
delegate :supports_schema_enums?, to: :class
|
12
11
|
|
13
12
|
def create_initializer_file
|
14
|
-
create_file(
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# Hoardable.save_trash = true
|
22
|
-
TEXT
|
23
|
-
)
|
24
|
-
end
|
25
|
-
|
26
|
-
def change_schema_format_to_sql
|
27
|
-
return if supports_schema_enums?
|
28
|
-
|
29
|
-
application 'config.active_record.schema_format = :sql'
|
13
|
+
create_file("config/initializers/hoardable.rb", <<~TEXT)
|
14
|
+
# Hoardable configuration defaults are below. Learn more at https://github.com/waymondo/hoardable#configuration
|
15
|
+
#
|
16
|
+
# Hoardable.enabled = true
|
17
|
+
# Hoardable.version_updates = true
|
18
|
+
# Hoardable.save_trash = true
|
19
|
+
TEXT
|
30
20
|
end
|
31
21
|
|
32
22
|
def create_migration_file
|
33
|
-
migration_template
|
23
|
+
migration_template "install.rb.erb", "db/migrate/install_hoardable.rb"
|
34
24
|
end
|
35
25
|
|
36
26
|
def create_functions
|
37
|
-
Dir
|
38
|
-
|
39
|
-
|
40
|
-
|
27
|
+
Dir
|
28
|
+
.glob(File.join(__dir__, "functions", "*.sql"))
|
29
|
+
.each do |file_path|
|
30
|
+
file_name = file_path.match(%r{([^/]+)\.sql})[1]
|
31
|
+
template file_path, "db/functions/#{file_name}_v01.sql"
|
32
|
+
end
|
41
33
|
end
|
42
34
|
|
43
|
-
|
44
|
-
|
35
|
+
no_tasks do
|
36
|
+
def postgres_version
|
37
|
+
ActiveRecord::Base
|
38
|
+
.connection
|
39
|
+
.select_value("SELECT VERSION()")
|
40
|
+
.match(/[0-9]{1,2}([,.][0-9]{1,2})?/)[
|
41
|
+
0
|
42
|
+
].to_f
|
43
|
+
end
|
45
44
|
end
|
46
45
|
|
47
46
|
def self.next_migration_number(dir)
|
@@ -1,23 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record/migration/migration_generator"
|
5
5
|
|
6
6
|
module Hoardable
|
7
7
|
# Generates a migration to create an inherited uni-temporal table of a model including
|
8
8
|
# {Hoardable::Model}, for the storage of +versions+.
|
9
9
|
class MigrationGenerator < ActiveRecord::Generators::Base
|
10
|
-
source_root File.expand_path(
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
11
11
|
include Rails::Generators::Migration
|
12
12
|
class_option(
|
13
13
|
:foreign_key_type,
|
14
14
|
type: :string,
|
15
15
|
optional: true,
|
16
|
-
desc:
|
16
|
+
desc: "explictly set / override the foreign key type of the versions table"
|
17
17
|
)
|
18
18
|
|
19
19
|
def create_versions_table
|
20
|
-
migration_template
|
20
|
+
migration_template(
|
21
|
+
"migration.rb.erb",
|
22
|
+
"db/migrate/create_#{singularized_table_name}_versions.rb"
|
23
|
+
)
|
21
24
|
end
|
22
25
|
|
23
26
|
def create_triggers
|
@@ -34,17 +37,23 @@ module Hoardable
|
|
34
37
|
end
|
35
38
|
|
36
39
|
no_tasks do
|
40
|
+
def table_name
|
41
|
+
class_name.singularize.constantize.table_name
|
42
|
+
rescue StandardError
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
37
46
|
def foreign_key_type
|
38
47
|
options[:foreign_key_type] ||
|
39
|
-
class_name.singularize.constantize.columns.find { |col| col.name ==
|
48
|
+
class_name.singularize.constantize.columns.find { |col| col.name == primary_key }.sql_type
|
40
49
|
rescue StandardError
|
41
|
-
|
50
|
+
"bigint"
|
42
51
|
end
|
43
52
|
|
44
53
|
def primary_key
|
45
54
|
options[:primary_key] || class_name.singularize.constantize.primary_key
|
46
55
|
rescue StandardError
|
47
|
-
|
56
|
+
"id"
|
48
57
|
end
|
49
58
|
|
50
59
|
def singularized_table_name
|
@@ -2,33 +2,10 @@
|
|
2
2
|
|
3
3
|
class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
4
|
def change
|
5
|
-
|
5
|
+
<% if postgres_version < 13 %>enable_extension :pgcrypto
|
6
|
+
<% end %>create_function :hoardable_prevent_update_id
|
6
7
|
create_function :hoardable_source_set_id
|
7
8
|
create_function :hoardable_version_prevent_update
|
8
|
-
<% if supports_schema_enums? %>
|
9
9
|
create_enum :hoardable_operation, %w[update delete insert]
|
10
|
-
<% else %>
|
11
|
-
reversible do |dir|
|
12
|
-
dir.up do
|
13
|
-
execute(
|
14
|
-
<<~SQL.squish
|
15
|
-
DO $$
|
16
|
-
BEGIN
|
17
|
-
IF NOT EXISTS (
|
18
|
-
SELECT 1 FROM pg_type t WHERE t.typname = 'hoardable_operation'
|
19
|
-
) THEN
|
20
|
-
CREATE TYPE hoardable_operation AS ENUM ('update', 'delete', 'insert');
|
21
|
-
END IF;
|
22
|
-
END
|
23
|
-
$$;
|
24
|
-
SQL
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
dir.down do
|
29
|
-
execute('DROP TYPE IF EXISTS hoardable_operation;')
|
30
|
-
end
|
31
|
-
end
|
32
|
-
<% end %>
|
33
10
|
end
|
34
11
|
end
|
@@ -4,7 +4,11 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
|
|
4
4
|
def change
|
5
5
|
add_column :<%= table_name %>, :hoardable_id, :<%= foreign_key_type %>
|
6
6
|
add_index :<%= table_name %>, :hoardable_id
|
7
|
-
create_table
|
7
|
+
create_table(
|
8
|
+
:<%= singularized_table_name %>_versions,
|
9
|
+
id: false,
|
10
|
+
options: 'INHERITS (<%= table_name %>)',
|
11
|
+
) do |t|
|
8
12
|
t.jsonb :_data
|
9
13
|
t.tsrange :_during, null: false
|
10
14
|
t.uuid :_event_uuid, null: false, index: true
|
@@ -12,6 +16,8 @@ class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Mi
|
|
12
16
|
end
|
13
17
|
reversible do |dir|
|
14
18
|
dir.up do
|
19
|
+
execute('ALTER TABLE <%= singularized_table_name %>_versions ADD PRIMARY KEY (<%= primary_key %>);')
|
20
|
+
# remove the following line if you plan on seeding +hoardable_id+ outside the migration
|
15
21
|
execute('UPDATE <%= table_name %> SET hoardable_id = <%= primary_key %>;')
|
16
22
|
end
|
17
23
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Hoardable
|
2
|
+
# This is a monkey patch of JOIN related {Arel::Visitors} for PostgreSQL so that they can append
|
3
|
+
# the ONLY clause when known to be operating on a {Hoardable::Model}. Ideally, {Arel} itself would
|
4
|
+
# provide a mechanism to support this keyword.
|
5
|
+
module ArelVisitors
|
6
|
+
def visit_Arel_Nodes_FullOuterJoin(o, collector)
|
7
|
+
collector << "FULL OUTER JOIN "
|
8
|
+
hoardable_maybe_add_only(o, collector)
|
9
|
+
collector = visit o.left, collector
|
10
|
+
collector << " "
|
11
|
+
visit o.right, collector
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit_Arel_Nodes_OuterJoin(o, collector)
|
15
|
+
collector << "LEFT OUTER JOIN "
|
16
|
+
hoardable_maybe_add_only(o, collector)
|
17
|
+
collector = visit o.left, collector
|
18
|
+
collector << " "
|
19
|
+
visit o.right, collector
|
20
|
+
end
|
21
|
+
|
22
|
+
def visit_Arel_Nodes_RightOuterJoin(o, collector)
|
23
|
+
collector << "RIGHT OUTER JOIN "
|
24
|
+
hoardable_maybe_add_only(o, collector)
|
25
|
+
collector = visit o.left, collector
|
26
|
+
collector << " "
|
27
|
+
visit o.right, collector
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_Arel_Nodes_InnerJoin(o, collector)
|
31
|
+
collector << "INNER JOIN "
|
32
|
+
hoardable_maybe_add_only(o, collector)
|
33
|
+
collector = visit o.left, collector
|
34
|
+
if o.right
|
35
|
+
collector << " "
|
36
|
+
visit(o.right, collector)
|
37
|
+
else
|
38
|
+
collector
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private def hoardable_maybe_add_only(o, collector)
|
43
|
+
left = o.left
|
44
|
+
|
45
|
+
if left.is_a?(Arel::Nodes::TableAlias)
|
46
|
+
hoardable_maybe_add_only(left, collector)
|
47
|
+
else
|
48
|
+
return unless left.instance_variable_get("@klass").in?(Hoardable::REGISTRY)
|
49
|
+
return if Hoardable.instance_variable_get("@at")
|
50
|
+
|
51
|
+
collector << "ONLY "
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Arel::Visitors::PostgreSQL.prepend Hoardable::ArelVisitors
|
@@ -13,9 +13,13 @@ module Hoardable
|
|
13
13
|
delegate :version_class, to: :source_record
|
14
14
|
|
15
15
|
def insert_hoardable_version(operation, &block)
|
16
|
-
version =
|
16
|
+
version =
|
17
|
+
version_class.insert(
|
18
|
+
initialize_version_attributes(operation),
|
19
|
+
returning: source_primary_key.to_sym
|
20
|
+
)
|
17
21
|
version_id = version[0][source_primary_key]
|
18
|
-
source_record.instance_variable_set(
|
22
|
+
source_record.instance_variable_set("@hoardable_version", version_class.find(version_id))
|
19
23
|
source_record.run_callbacks(:versioned, &block)
|
20
24
|
end
|
21
25
|
|
@@ -24,40 +28,50 @@ module Hoardable
|
|
24
28
|
end
|
25
29
|
|
26
30
|
def find_or_initialize_hoardable_event_uuid
|
27
|
-
Thread.current[:hoardable_event_uuid] ||=
|
31
|
+
Thread.current[:hoardable_event_uuid] ||= (
|
32
|
+
ActiveRecord::Base.connection.query("SELECT gen_random_uuid();")[0][0]
|
33
|
+
)
|
28
34
|
end
|
29
35
|
|
30
36
|
def initialize_version_attributes(operation)
|
31
37
|
source_attributes_without_primary_key.merge(
|
32
38
|
source_record.changes.transform_values { |h| h[0] },
|
33
39
|
{
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
40
|
+
"hoardable_id" => source_record.id,
|
41
|
+
"_event_uuid" => find_or_initialize_hoardable_event_uuid,
|
42
|
+
"_operation" => operation,
|
43
|
+
"_data" => initialize_hoardable_data.merge(changes: source_record.changes),
|
44
|
+
"_during" => initialize_temporal_range
|
39
45
|
}
|
40
46
|
)
|
41
47
|
end
|
42
48
|
|
43
49
|
def has_one_find_conditions(reflection)
|
44
50
|
{
|
45
|
-
reflection.type => source_record.class.name.sub(/Version$/,
|
51
|
+
reflection.type => source_record.class.name.sub(/Version$/, ""),
|
46
52
|
reflection.foreign_key => source_record.hoardable_id,
|
47
|
-
|
53
|
+
"name" =>
|
54
|
+
(reflection.name.to_s.sub(/^rich_text_/, "") if reflection.class_name.match?(/RichText$/))
|
48
55
|
}.reject { |k, v| k.nil? || v.nil? }
|
49
56
|
end
|
50
57
|
|
51
58
|
def has_one_at_timestamp
|
52
|
-
Hoardable.instance_variable_get(
|
59
|
+
Hoardable.instance_variable_get("@at") || source_record.updated_at
|
53
60
|
rescue NameError
|
54
61
|
raise(UpdatedAtColumnMissingError, source_record.class.table_name)
|
55
62
|
end
|
56
63
|
|
57
64
|
def source_attributes_without_primary_key
|
58
|
-
source_record
|
59
|
-
|
60
|
-
|
65
|
+
source_record
|
66
|
+
.attributes
|
67
|
+
.without(source_primary_key, *generated_column_names)
|
68
|
+
.merge(
|
69
|
+
source_record
|
70
|
+
.class
|
71
|
+
.select(refreshable_column_names)
|
72
|
+
.find(source_record.id)
|
73
|
+
.slice(refreshable_column_names)
|
74
|
+
)
|
61
75
|
end
|
62
76
|
|
63
77
|
def generated_column_names
|
@@ -67,9 +81,15 @@ module Hoardable
|
|
67
81
|
end
|
68
82
|
|
69
83
|
def refreshable_column_names
|
70
|
-
@refreshable_column_names ||=
|
71
|
-
|
72
|
-
|
84
|
+
@refreshable_column_names ||=
|
85
|
+
source_record
|
86
|
+
.class
|
87
|
+
.columns
|
88
|
+
.select(&:default_function)
|
89
|
+
.reject do |column|
|
90
|
+
column.name == source_primary_key || column.name.in?(generated_column_names)
|
91
|
+
end
|
92
|
+
.map(&:name)
|
73
93
|
end
|
74
94
|
|
75
95
|
def initialize_temporal_range
|
@@ -77,9 +97,7 @@ module Hoardable
|
|
77
97
|
end
|
78
98
|
|
79
99
|
def initialize_hoardable_data
|
80
|
-
DATA_KEYS.to_h
|
81
|
-
[key, assign_hoardable_context(key)]
|
82
|
-
end
|
100
|
+
DATA_KEYS.to_h { |key| [key, assign_hoardable_context(key)] }
|
83
101
|
end
|
84
102
|
|
85
103
|
def assign_hoardable_context(key)
|
@@ -89,18 +107,18 @@ module Hoardable
|
|
89
107
|
end
|
90
108
|
|
91
109
|
def unset_hoardable_version_and_event_uuid
|
92
|
-
source_record.instance_variable_set(
|
110
|
+
source_record.instance_variable_set("@hoardable_version", nil)
|
93
111
|
return if source_record.class.connection.transaction_open?
|
94
112
|
|
95
113
|
Thread.current[:hoardable_event_uuid] = nil
|
96
114
|
end
|
97
115
|
|
98
116
|
def previous_temporal_tsrange_end
|
99
|
-
source_record.versions.only_most_recent.pluck(
|
117
|
+
source_record.versions.only_most_recent.pluck("_during").first&.end
|
100
118
|
end
|
101
119
|
|
102
120
|
def hoardable_source_epoch
|
103
|
-
return source_record.created_at if source_record.class.column_names.include?(
|
121
|
+
return source_record.created_at if source_record.class.column_names.include?("created_at")
|
104
122
|
|
105
123
|
raise CreatedAtColumnMissingError, source_record.class.table_name
|
106
124
|
end
|
data/lib/hoardable/engine.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
# An +ActiveRecord+ extension for keeping versions of records in uni-temporal inherited tables.
|
4
4
|
module Hoardable
|
5
|
+
REGISTRY = Set.new
|
6
|
+
|
5
7
|
# Symbols for use with setting contextual data, when creating versions. See
|
6
8
|
# {file:README.md#tracking-contextual-data README} for more.
|
7
9
|
DATA_KEYS = %i[meta whodunit event_uuid].freeze
|
@@ -10,60 +12,47 @@ module Hoardable
|
|
10
12
|
# README} for more.
|
11
13
|
CONFIG_KEYS = %i[enabled version_updates save_trash].freeze
|
12
14
|
|
13
|
-
VERSION_CLASS_SUFFIX =
|
15
|
+
VERSION_CLASS_SUFFIX = "Version"
|
14
16
|
private_constant :VERSION_CLASS_SUFFIX
|
15
17
|
|
16
18
|
VERSION_TABLE_SUFFIX = "_#{VERSION_CLASS_SUFFIX.tableize}"
|
17
19
|
private_constant :VERSION_TABLE_SUFFIX
|
18
20
|
|
19
|
-
DURING_QUERY =
|
21
|
+
DURING_QUERY = "_during @> ?::timestamp"
|
20
22
|
private_constant :DURING_QUERY
|
21
23
|
|
22
|
-
HOARDABLE_CALLBACKS_ENABLED =
|
23
|
-
|
24
|
-
|
24
|
+
HOARDABLE_CALLBACKS_ENABLED =
|
25
|
+
proc do |source_model|
|
26
|
+
source_model.class.hoardable_config[:enabled] &&
|
27
|
+
!source_model.class.name.end_with?(VERSION_CLASS_SUFFIX)
|
28
|
+
end.freeze
|
25
29
|
private_constant :HOARDABLE_CALLBACKS_ENABLED
|
26
30
|
|
27
|
-
HOARDABLE_SAVE_TRASH =
|
28
|
-
source_model.class.hoardable_config[:save_trash]
|
29
|
-
end.freeze
|
31
|
+
HOARDABLE_SAVE_TRASH =
|
32
|
+
proc { |source_model| source_model.class.hoardable_config[:save_trash] }.freeze
|
30
33
|
private_constant :HOARDABLE_SAVE_TRASH
|
31
34
|
|
32
|
-
HOARDABLE_VERSION_UPDATES =
|
33
|
-
source_model.class.hoardable_config[:version_updates]
|
34
|
-
end.freeze
|
35
|
+
HOARDABLE_VERSION_UPDATES =
|
36
|
+
proc { |source_model| source_model.class.hoardable_config[:version_updates] }.freeze
|
35
37
|
private_constant :HOARDABLE_VERSION_UPDATES
|
36
38
|
|
37
|
-
SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new(
|
39
|
+
SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new("7.0.4")
|
38
40
|
private_constant :SUPPORTS_ENCRYPTED_ACTION_TEXT
|
39
41
|
|
40
|
-
SUPPORTS_VIRTUAL_COLUMNS = ActiveRecord.version >= ::Gem::Version.new('7.0.0')
|
41
|
-
private_constant :SUPPORTS_VIRTUAL_COLUMNS
|
42
|
-
|
43
42
|
@context = {}
|
44
|
-
@config = CONFIG_KEYS.to_h
|
45
|
-
[key, true]
|
46
|
-
end
|
43
|
+
@config = CONFIG_KEYS.to_h { |key| [key, true] }
|
47
44
|
|
48
45
|
class << self
|
49
46
|
CONFIG_KEYS.each do |key|
|
50
|
-
define_method(key)
|
51
|
-
@config[key]
|
52
|
-
end
|
47
|
+
define_method(key) { @config[key] }
|
53
48
|
|
54
|
-
define_method("#{key}=")
|
55
|
-
@config[key] = value
|
56
|
-
end
|
49
|
+
define_method("#{key}=") { |value| @config[key] = value }
|
57
50
|
end
|
58
51
|
|
59
52
|
DATA_KEYS.each do |key|
|
60
|
-
define_method(key)
|
61
|
-
@context[key]
|
62
|
-
end
|
53
|
+
define_method(key) { @context[key] }
|
63
54
|
|
64
|
-
define_method("#{key}=")
|
65
|
-
@context[key] = value
|
66
|
-
end
|
55
|
+
define_method("#{key}=") { |value| @context[key] = value }
|
67
56
|
end
|
68
57
|
|
69
58
|
# This is a general use method for setting {file:README.md#tracking-contextual-data Contextual
|
@@ -102,10 +91,20 @@ module Hoardable
|
|
102
91
|
class Engine < ::Rails::Engine
|
103
92
|
isolate_namespace Hoardable
|
104
93
|
|
105
|
-
initializer
|
94
|
+
initializer "hoardable.action_text" do
|
106
95
|
ActiveSupport.on_load(:action_text_rich_text) do
|
107
|
-
require_relative
|
108
|
-
require_relative
|
96
|
+
require_relative "rich_text"
|
97
|
+
require_relative "encrypted_rich_text" if SUPPORTS_ENCRYPTED_ACTION_TEXT
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
initializer "hoardable.schema_statements" do
|
102
|
+
ActiveSupport.on_load(:active_record_postgresqladapter) do
|
103
|
+
# We need to control the table dumping order of tables, so revert these to just +super+
|
104
|
+
Fx::SchemaDumper::Trigger.module_eval("def tables(streams); super; end")
|
105
|
+
|
106
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(SchemaDumper)
|
107
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements.prepend(SchemaStatements)
|
109
108
|
end
|
110
109
|
end
|
111
110
|
end
|
data/lib/hoardable/error.rb
CHANGED
@@ -2,29 +2,26 @@
|
|
2
2
|
|
3
3
|
module Hoardable
|
4
4
|
# A subclass of +StandardError+ for general use within {Hoardable}.
|
5
|
-
class Error < StandardError
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
6
7
|
|
7
8
|
# An error to be raised when 'created_at' columns are missing for {Hoardable::Model}s.
|
8
9
|
class CreatedAtColumnMissingError < Error
|
9
10
|
def initialize(source_table_name)
|
10
|
-
super(
|
11
|
-
<<~LOG
|
11
|
+
super(<<~LOG)
|
12
12
|
'#{source_table_name}' does not have a 'created_at' column, so the start of the first
|
13
13
|
version’s temporal period cannot be known. Add a 'created_at' column to '#{source_table_name}'.
|
14
14
|
LOG
|
15
|
-
)
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
18
|
# An error to be raised when 'updated_at' columns are missing for {Hoardable::Model}s.
|
20
19
|
class UpdatedAtColumnMissingError < Error
|
21
20
|
def initialize(source_table_name)
|
22
|
-
super(
|
23
|
-
<<~LOG
|
21
|
+
super(<<~LOG)
|
24
22
|
'#{source_table_name}' does not have an 'updated_at' column, so Hoardable cannot look up
|
25
23
|
associated record versions with it. Add an 'updated_at' column to '#{source_table_name}'.
|
26
24
|
LOG
|
27
|
-
)
|
28
25
|
end
|
29
26
|
end
|
30
27
|
end
|
@@ -17,9 +17,7 @@ module Hoardable
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def hoardable_ids(ids)
|
20
|
-
ids.map
|
21
|
-
version_class.where(hoardable_id: id).select(primary_key).ids[0] || id
|
22
|
-
end
|
20
|
+
ids.map { |id| version_class.where(hoardable_id: id).select(primary_key).ids[0] || id }
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
data/lib/hoardable/has_many.rb
CHANGED
@@ -12,15 +12,9 @@ module Hoardable
|
|
12
12
|
@scope ||= hoardable_scope
|
13
13
|
end
|
14
14
|
|
15
|
-
private
|
16
|
-
|
17
|
-
|
18
|
-
if Hoardable.instance_variable_get('@at') && (hoardable_id = @association.owner.hoardable_id)
|
19
|
-
if @association.reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
20
|
-
@association.reflection.source_reflection.instance_variable_set(
|
21
|
-
'@active_record_primary_key', 'hoardable_id'
|
22
|
-
)
|
23
|
-
end
|
15
|
+
private def hoardable_scope
|
16
|
+
if Hoardable.instance_variable_get("@at") &&
|
17
|
+
(hoardable_id = @association.owner.hoardable_id)
|
24
18
|
@association.scope.rewhere(@association.reflection.foreign_key => hoardable_id)
|
25
19
|
else
|
26
20
|
@association.scope
|
@@ -32,7 +26,9 @@ module Hoardable
|
|
32
26
|
class_methods do
|
33
27
|
def has_many(*args, &block)
|
34
28
|
options = args.extract_options!
|
35
|
-
options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(
|
29
|
+
options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(
|
30
|
+
:hoardable
|
31
|
+
)
|
36
32
|
super(*args, **options, &block)
|
37
33
|
|
38
34
|
# This hack is needed to force Rails to not use any existing method cache so that the
|
data/lib/hoardable/has_one.rb
CHANGED
@@ -9,13 +9,13 @@ module Hoardable
|
|
9
9
|
def has_one(*args)
|
10
10
|
options = args.extract_options!
|
11
11
|
hoardable = options.delete(:hoardable)
|
12
|
-
association = super(*args, **options)
|
13
12
|
name = args.first
|
14
|
-
|
13
|
+
association = super(*args, **options).symbolize_keys[name]
|
14
|
+
return unless hoardable || (association.options[:class_name].match?(/RichText$/))
|
15
15
|
|
16
16
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
17
|
def #{name}
|
18
|
-
reflection = _reflections[
|
18
|
+
reflection = _reflections.symbolize_keys[:#{name}]
|
19
19
|
return super if reflection.klass.name.match?(/^ActionText/)
|
20
20
|
return super unless (timestamp = hoardable_client.has_one_at_timestamp)
|
21
21
|
|
@@ -6,16 +6,23 @@ module Hoardable
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
class_methods do
|
9
|
-
def has_rich_text(name,
|
10
|
-
|
11
|
-
super(name, encrypted: encrypted)
|
12
|
-
else
|
13
|
-
super(name)
|
14
|
-
end
|
9
|
+
def has_rich_text(name, hoardable: false, **opts)
|
10
|
+
super(name, **opts)
|
15
11
|
return unless hoardable
|
16
12
|
|
17
13
|
reflection_options = reflections["rich_text_#{name}"].options
|
18
|
-
|
14
|
+
|
15
|
+
# load the +ActionText+ class if it hasn’t been already
|
16
|
+
reflection_options[:class_name].constantize
|
17
|
+
|
18
|
+
reflection_options[:class_name] = reflection_options[:class_name].sub(
|
19
|
+
/^ActionText/,
|
20
|
+
"Hoardable"
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_hoardable_rich_text(name, **opts)
|
25
|
+
has_rich_text(name, hoardable: true, **opts)
|
19
26
|
end
|
20
27
|
end
|
21
28
|
end
|
data/lib/hoardable/model.rb
CHANGED
@@ -51,24 +51,27 @@ module Hoardable
|
|
51
51
|
define_model_callbacks :reverted, only: :after
|
52
52
|
define_model_callbacks :untrashed, only: :after
|
53
53
|
|
54
|
-
TracePoint
|
55
|
-
|
54
|
+
TracePoint
|
55
|
+
.new(:end) do |trace|
|
56
|
+
next unless self == trace.self
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
58
|
+
full_version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
|
59
|
+
if (namespace_match = full_version_class_name.match(/(.*)::(.*)/))
|
60
|
+
object_namespace = namespace_match[1].constantize
|
61
|
+
version_class_name = namespace_match[2]
|
62
|
+
else
|
63
|
+
object_namespace = Object
|
64
|
+
version_class_name = full_version_class_name
|
65
|
+
end
|
66
|
+
unless Object.const_defined?(full_version_class_name)
|
67
|
+
object_namespace.const_set(version_class_name, Class.new(self) { include VersionModel })
|
68
|
+
end
|
69
|
+
include SourceModel
|
70
|
+
REGISTRY.add(self)
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
+
trace.disable
|
73
|
+
end
|
74
|
+
.enable
|
72
75
|
end
|
73
76
|
end
|
74
77
|
end
|