better_record 0.1.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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +27 -0
- data/app/assets/config/better_record_manifest.js +2 -0
- data/app/controllers/better_record/application_controller.rb +5 -0
- data/app/helpers/better_record/application_helper.rb +4 -0
- data/app/jobs/better_record/application_job.rb +4 -0
- data/app/mailers/better_record/application_mailer.rb +6 -0
- data/app/models/better_record/audit.rb +50 -0
- data/app/models/better_record/base.rb +157 -0
- data/app/models/better_record/table_size.rb +77 -0
- data/config/routes.rb +2 -0
- data/lib/better_record.rb +37 -0
- data/lib/better_record/associations.rb +75 -0
- data/lib/better_record/batches.rb +27 -0
- data/lib/better_record/engine.rb +5 -0
- data/lib/better_record/migration.rb +101 -0
- data/lib/better_record/nullify_blank_attributes.rb +8 -0
- data/lib/better_record/railtie.rb +4 -0
- data/lib/better_record/reflection.rb +21 -0
- data/lib/better_record/relation.rb +62 -0
- data/lib/better_record/version.rb +3 -0
- data/lib/generators/create_helper_functions/USAGE +9 -0
- data/lib/generators/create_helper_functions/create_helper_functions_generator.rb +61 -0
- data/lib/tasks/db/create_audits.rake +0 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b1f24408013032454f5f6384cfa1723d81fd5b741c544f35ce95352fe4c334a8
|
4
|
+
data.tar.gz: 5476435865403fef0645f2502eaf10a412fe27d222b43568e5eb7f557f60f558
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 307dc6781a0ae40f1d66ebb7f9e8ea7a4e4fec739554a314c0dceb4302b0f3bbaa928dfacf7b8649ec99b87bebd243ac107446d4ad4b61d5e91ef054d7156e6c
|
7
|
+
data.tar.gz: 3fc5c79c31830dacda3dc341c18584ed0c30ede6aa915011c40bcad19acbb78a3c65c97b4ea3754324077c1edce7215bb6c3837a4e35f0382de9036e3d1199b6
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018 Sampson Crowley
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# BetterRecord
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'better_record'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install better_record
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'BetterRecord'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'test'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: :test
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
class Audit < Base
|
3
|
+
# == Constants ============================================================
|
4
|
+
ACTIONS = {
|
5
|
+
D: 'DELETE',
|
6
|
+
I: 'INSERT',
|
7
|
+
U: 'UPDATE',
|
8
|
+
T: 'TRUNCATE',
|
9
|
+
}.with_indifferent_access
|
10
|
+
|
11
|
+
# == Attributes ===========================================================
|
12
|
+
self.table_name = 'audit.logged_actions'
|
13
|
+
|
14
|
+
# == Extensions ===========================================================
|
15
|
+
|
16
|
+
# == Relationships ========================================================
|
17
|
+
belongs_to :audited,
|
18
|
+
polymorphic: :true,
|
19
|
+
primary_type: :table_name,
|
20
|
+
foreign_key: :row_id,
|
21
|
+
foreign_type: :table_name
|
22
|
+
# == Validations ==========================================================
|
23
|
+
|
24
|
+
# == Scopes ===============================================================
|
25
|
+
|
26
|
+
# == Callbacks ============================================================
|
27
|
+
|
28
|
+
# == Class Methods ========================================================
|
29
|
+
def self.default_print
|
30
|
+
[
|
31
|
+
:event_id,
|
32
|
+
:row_id,
|
33
|
+
:table_name,
|
34
|
+
:app_user_id,
|
35
|
+
:app_user_type,
|
36
|
+
:action_type,
|
37
|
+
:changed_columns
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
# == Instance Methods =====================================================
|
42
|
+
def changed_columns
|
43
|
+
(self.changed_fields || {}).keys.join(', ').presence || 'N/A'
|
44
|
+
end
|
45
|
+
|
46
|
+
def action_type
|
47
|
+
ACTIONS[action] || 'UNKNOWN'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
class Base < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
|
5
|
+
# == Constants ============================================================
|
6
|
+
|
7
|
+
# == Attributes ===========================================================
|
8
|
+
|
9
|
+
# == Extensions ===========================================================
|
10
|
+
|
11
|
+
# == Relationships ========================================================
|
12
|
+
if (ha = BetterRecord.has_audits_by_default)
|
13
|
+
has_many :audits,
|
14
|
+
class_name: 'BetterRecord::Audit',
|
15
|
+
primary_type: :table_name,
|
16
|
+
foreign_key: :row_id,
|
17
|
+
foreign_type: :table_name,
|
18
|
+
as: :audits
|
19
|
+
end
|
20
|
+
|
21
|
+
# == Validations ==========================================================
|
22
|
+
before_validation :set_booleans
|
23
|
+
|
24
|
+
# == Scopes ===============================================================
|
25
|
+
|
26
|
+
# == Callbacks ============================================================
|
27
|
+
|
28
|
+
# == Class Methods ========================================================
|
29
|
+
def self.boolean_columns
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.column_comments(prefix = '')
|
34
|
+
longest_name = 0
|
35
|
+
column_names.each {|nm| longest_name = nm.length if nm.length > longest_name}
|
36
|
+
longest_name += 1
|
37
|
+
str = ''
|
38
|
+
columns.each do |col|
|
39
|
+
unless col.name == 'id'
|
40
|
+
spaces = "#{' ' * (longest_name - col.name.length)}"
|
41
|
+
is_required = "#{col.null == false ? ', required' : ''}"
|
42
|
+
is_default = "#{col.default ? ", default: #{col.default}" : ''}"
|
43
|
+
str << "#{prefix}##{spaces}#{col.name}: :#{col.type}#{is_required}\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
str
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.default_print
|
50
|
+
column_names
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find_or_retry_by(*args)
|
54
|
+
found = nil
|
55
|
+
retries = 0
|
56
|
+
begin
|
57
|
+
raise ActiveRecord::RecordNotFound unless found = find_by(*args)
|
58
|
+
return found
|
59
|
+
rescue
|
60
|
+
sleep retries += 1
|
61
|
+
retry if (retries) < 5
|
62
|
+
end
|
63
|
+
found
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.queue_adapter_inline?
|
67
|
+
@@queue_adapter ||= Rails.application.config.active_job.queue_adapter
|
68
|
+
@@queue_adapter == :inline
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.table_name_has_schema?
|
72
|
+
@@table_name_has_schema ||= (table_name =~ /\w+\.\w+/)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.table_name_without_schema
|
76
|
+
@@table_name_without_schema ||= (table_name =~ /\w+\.\w+/) ? table_name.split('.').last : table_name
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.table_schema
|
80
|
+
@@table_schema ||= table_name_has_schema? ? table_name.split('.').first : 'public'
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.table_size
|
84
|
+
BetterRecord::TableSize.unscoped.find_by(name: table_name_without_schema, schema: table_schema)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.transaction(*args)
|
88
|
+
super(*args) do
|
89
|
+
if Current.user
|
90
|
+
ip = Current.ip_address ? "'#{Current.ip_address}'" : 'NULL'
|
91
|
+
|
92
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
93
|
+
CREATE TEMP TABLE IF NOT EXISTS
|
94
|
+
"_app_user" (user_id integer, user_type text, ip_address inet);
|
95
|
+
|
96
|
+
UPDATE
|
97
|
+
_app_user
|
98
|
+
SET
|
99
|
+
user_id=#{Current.user.id},
|
100
|
+
user_type='#{Current.user.class.to_s}',
|
101
|
+
ip_address=#{ip};
|
102
|
+
|
103
|
+
INSERT INTO
|
104
|
+
_app_user (user_id, user_type, ip_address)
|
105
|
+
SELECT
|
106
|
+
#{Current.user.id},
|
107
|
+
'#{Current.user.class.to_s}',
|
108
|
+
#{ip}
|
109
|
+
WHERE NOT EXISTS (SELECT * FROM _app_user);
|
110
|
+
SQL
|
111
|
+
end
|
112
|
+
|
113
|
+
yield
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# == Instance Methods =====================================================
|
118
|
+
def queue_adapter_inline?
|
119
|
+
self.class.queue_adapter_inline?
|
120
|
+
end
|
121
|
+
|
122
|
+
%w(path url).each do |cat|
|
123
|
+
self.__send__ :define_method, :"rails_blob_#{cat}" do |*args|
|
124
|
+
Rails.application.routes.url_helpers.__send__ :"rails_blob_#{cat}", *args
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def purge(attached)
|
129
|
+
attached.__send__ queue_adapter_inline? ? :purge : :purge_later
|
130
|
+
end
|
131
|
+
|
132
|
+
def boolean_columns
|
133
|
+
self.class.boolean_columns
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
# def table_name_has_schema?
|
138
|
+
# self.class.table_name_has_schema?
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# def table_schema
|
142
|
+
# self.class.table_schema
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# def table_name_without_schema
|
146
|
+
# self.class.table_name_without_schema
|
147
|
+
# end
|
148
|
+
|
149
|
+
def set_booleans
|
150
|
+
self.class.boolean_columns.each do |nm|
|
151
|
+
self.__send__("#{nm}=", __send__("#{nm}=", !!Boolean.parse(__send__ nm)))
|
152
|
+
end
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
class TableSize < Base
|
3
|
+
# == Constants ============================================================
|
4
|
+
CREATE_TABLE_SQL = <<-SQL
|
5
|
+
DROP TABLE IF EXISTS table_sizes;
|
6
|
+
CREATE TEMPORARY TABLE table_sizes AS (
|
7
|
+
SELECT
|
8
|
+
*,
|
9
|
+
pg_size_pretty(total_bytes) AS total,
|
10
|
+
pg_size_pretty(idx_bytes) AS idx,
|
11
|
+
pg_size_pretty(toast_bytes) AS toast,
|
12
|
+
pg_size_pretty(tbl_bytes) AS tbl
|
13
|
+
FROM (
|
14
|
+
SELECT
|
15
|
+
*,
|
16
|
+
total_bytes - idx_bytes - COALESCE(toast_bytes,0) AS tbl_bytes
|
17
|
+
FROM (
|
18
|
+
SELECT c.oid,nspname AS schema, relname AS name
|
19
|
+
, c.reltuples AS row_estimate
|
20
|
+
, pg_total_relation_size(c.oid) AS total_bytes
|
21
|
+
, pg_indexes_size(c.oid) AS idx_bytes
|
22
|
+
, pg_total_relation_size(reltoastrelid) AS toast_bytes
|
23
|
+
FROM pg_class c
|
24
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
25
|
+
WHERE relkind = 'r'
|
26
|
+
) table_sizes
|
27
|
+
) table_sizes
|
28
|
+
)
|
29
|
+
SQL
|
30
|
+
|
31
|
+
# == Attributes ===========================================================
|
32
|
+
self.primary_key = :oid
|
33
|
+
self.table_name = 'table_sizes'
|
34
|
+
|
35
|
+
# == Extensions ===========================================================
|
36
|
+
|
37
|
+
# == Relationships ========================================================
|
38
|
+
|
39
|
+
# == Validations ==========================================================
|
40
|
+
|
41
|
+
# == Scopes ===============================================================
|
42
|
+
default_scope { where(schema: [ :public ]) }
|
43
|
+
# == Callbacks ============================================================
|
44
|
+
|
45
|
+
# == Class Methods ========================================================
|
46
|
+
def self.load_schema(reload = false)
|
47
|
+
unless @loaded_schema && !reload
|
48
|
+
connection.execute CREATE_TABLE_SQL
|
49
|
+
@loaded_schema = true
|
50
|
+
end
|
51
|
+
|
52
|
+
super()
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.find_by(*args)
|
56
|
+
load_schema(true)
|
57
|
+
super *args
|
58
|
+
end
|
59
|
+
|
60
|
+
# def self.default_print
|
61
|
+
# [
|
62
|
+
# :table_schema,
|
63
|
+
# :table_name,
|
64
|
+
# :
|
65
|
+
# ]
|
66
|
+
# end
|
67
|
+
|
68
|
+
# == Instance Methods =====================================================
|
69
|
+
def changed_columns
|
70
|
+
(self.changed_fields || {}).keys.join(', ').presence || 'N/A'
|
71
|
+
end
|
72
|
+
|
73
|
+
def action_type
|
74
|
+
ACTIONS[action] || 'UNKNOWN'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require "active_support"
|
2
|
+
|
3
|
+
class Boolean
|
4
|
+
def self.parse(value)
|
5
|
+
ActiveRecord::Type::Boolean.new.cast(value)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Object
|
10
|
+
def yes_no_to_s
|
11
|
+
!!self == self ? (self ? 'yes' : 'no') : to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module BetterRecord
|
16
|
+
class << self
|
17
|
+
attr_accessor :default_polymorphic_method, :db_audit_schema, :has_audits_by_default
|
18
|
+
end
|
19
|
+
self.default_polymorphic_method = :polymorphic_name
|
20
|
+
self.db_audit_schema = ENV.fetch('DB_AUDIT_SCHEMA') { 'auditing' }
|
21
|
+
self.has_audits_by_default = Boolean.parse(ENV.fetch('BR_ADD_HAS_MANY') { false })
|
22
|
+
end
|
23
|
+
|
24
|
+
Dir.glob("#{File.expand_path(__dir__)}/better_record/*").each do |d|
|
25
|
+
require d
|
26
|
+
end
|
27
|
+
|
28
|
+
ActiveSupport.on_load(:active_record) do
|
29
|
+
module ActiveRecord
|
30
|
+
include BetterRecord::Associations
|
31
|
+
include BetterRecord::Batches
|
32
|
+
include BetterRecord::Migration
|
33
|
+
include BetterRecord::Reflection
|
34
|
+
include BetterRecord::Relation
|
35
|
+
end
|
36
|
+
include BetterRecord::NullifyBlankAttributes
|
37
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
module Associations
|
3
|
+
class AssociationScope #:nodoc:
|
4
|
+
def self.get_bind_values(owner, chain)
|
5
|
+
binds = []
|
6
|
+
last_reflection = chain.last
|
7
|
+
|
8
|
+
binds << last_reflection.join_id_for(owner)
|
9
|
+
if last_reflection.type
|
10
|
+
binds << owner.class.__send__(last_reflection.options[:primary_type].presence || BetterRecord.default_polymorphic_method.presence || :polymorphic_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
chain.each_cons(2).each do |reflection, next_reflection|
|
14
|
+
if reflection.type
|
15
|
+
binds << next_reflection.klass.__send__(reflection.options[:primary_type].presence || next_reflection[:primary_type].presence || BetterRecord.default_polymorphic_method.presence || :polymorphic_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
binds
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def last_chain_scope(scope, reflection, owner)
|
23
|
+
join_keys = reflection.join_keys
|
24
|
+
key = join_keys.key
|
25
|
+
foreign_key = join_keys.foreign_key
|
26
|
+
|
27
|
+
table = reflection.aliased_table
|
28
|
+
value = transform_value(owner[foreign_key])
|
29
|
+
scope = apply_scope(scope, table, key, value)
|
30
|
+
|
31
|
+
if reflection.type
|
32
|
+
polymorphic_type = transform_value(owner.class.__send__(reflection.options[:primary_type].presence || BetterRecord.default_polymorphic_method.presence || :polymorphic_name))
|
33
|
+
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
|
34
|
+
end
|
35
|
+
|
36
|
+
scope
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_chain_scope(scope, reflection, next_reflection)
|
40
|
+
join_keys = reflection.join_keys
|
41
|
+
key = join_keys.key
|
42
|
+
foreign_key = join_keys.foreign_key
|
43
|
+
|
44
|
+
table = reflection.aliased_table
|
45
|
+
foreign_table = next_reflection.aliased_table
|
46
|
+
constraint = table[key].eq(foreign_table[foreign_key])
|
47
|
+
|
48
|
+
if reflection.type
|
49
|
+
value = transform_value(next_reflection.klass.__send__(reflection.options[:primary_type].presence || BetterRecord.default_polymorphic_method.presence || :polymorphic_name))
|
50
|
+
scope = apply_scope(scope, table, reflection.type, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
scope.joins!(join(foreign_table, constraint))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module Builder
|
58
|
+
class Association
|
59
|
+
def self.valid_options(options)
|
60
|
+
VALID_OPTIONS + [ :primary_type ] + Association.extensions.flat_map(&:valid_options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class BelongsToAssociation
|
66
|
+
end
|
67
|
+
|
68
|
+
class BelongsToPolymorphicAssociation < BelongsToAssociation
|
69
|
+
def klass
|
70
|
+
type = owner[reflection.foreign_type]
|
71
|
+
type.presence && type.capitalize.singularize.constantize
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
module Batches
|
3
|
+
def split_batches(options = {}, &block)
|
4
|
+
options.assert_valid_keys(:start, :batch_size, :preserve_order)
|
5
|
+
if block_given? && arel.orders.present? && options[:preserve_order]
|
6
|
+
relation = self
|
7
|
+
offset = options[:start] || 0
|
8
|
+
batch_size = options[:batch_size] || 1000
|
9
|
+
|
10
|
+
total = relation.count(:*)
|
11
|
+
records = relation.limit(batch_size).offset(offset).to_a
|
12
|
+
while records.any?
|
13
|
+
records_size = records.size
|
14
|
+
|
15
|
+
block.call records
|
16
|
+
|
17
|
+
break if records_size < batch_size
|
18
|
+
offset += batch_size
|
19
|
+
records = relation.limit(batch_size).offset(offset).to_a
|
20
|
+
end
|
21
|
+
nil
|
22
|
+
else
|
23
|
+
find_in_batches(options.except(:preserve_order), &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
module Migration
|
3
|
+
def audit_table(table_name, rows = nil, query_text = nil, skip_columns = nil)
|
4
|
+
reversible do |d|
|
5
|
+
d.up do
|
6
|
+
if(rows && rows.is_a?(Array))
|
7
|
+
skip_columns = rows
|
8
|
+
rows = true
|
9
|
+
query_text = true
|
10
|
+
end
|
11
|
+
|
12
|
+
if(rows.nil?)
|
13
|
+
execute "SELECT #{BetterRecord.db_audit_schema}.audit_table('#{table_name}');"
|
14
|
+
else
|
15
|
+
rows = !!rows ? 't' : 'f'
|
16
|
+
query_text = !!query_text ? 't' : 'f'
|
17
|
+
skip_columns = skip_columns.present? ? "'{#{skip_columns.join(',')}}'" : 'ARRAY[]'
|
18
|
+
execute "SELECT #{BetterRecord.db_audit_schema}.audit_table('#{table_name}', BOOLEAN '#{rows}', BOOLEAN '#{query_text}', #{skip_columns}::text[]);"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
d.down do
|
23
|
+
execute "DROP TRIGGER audit_trigger_row ON #{table_name};"
|
24
|
+
execute "DROP TRIGGER audit_trigger_stm ON #{table_name};"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def login_triggers(table_name, password_col = 'password', email_col = 'email')
|
30
|
+
table_name = table_name.to_s
|
31
|
+
|
32
|
+
reversible do |d|
|
33
|
+
d.up do
|
34
|
+
password_text = ''
|
35
|
+
|
36
|
+
if !!password_col
|
37
|
+
password_text = <<-SQL
|
38
|
+
IF (NEW.#{password_col} IS NOT NULL)
|
39
|
+
AND (
|
40
|
+
(TG_OP = 'INSERT') OR ( NEW.#{password_col} IS DISTINCT FROM OLD.#{password_col} )
|
41
|
+
) THEN
|
42
|
+
NEW.#{password_col} = hash_password(NEW.#{password_col});
|
43
|
+
ELSE
|
44
|
+
IF (TG_OP IS DISTINCT FROM 'INSERT') THEN
|
45
|
+
NEW.#{password_col} = OLD.#{password_col};
|
46
|
+
ELSE
|
47
|
+
NEW.#{password_col} = NULL;
|
48
|
+
END IF;
|
49
|
+
END IF;
|
50
|
+
|
51
|
+
SQL
|
52
|
+
end
|
53
|
+
|
54
|
+
email_text = ''
|
55
|
+
|
56
|
+
if !!email_col
|
57
|
+
email_text = <<-SQL
|
58
|
+
IF (TG_OP = 'INSERT') OR ( NEW.#{email_col} IS DISTINCT FROM OLD.#{email_col} ) THEN
|
59
|
+
NEW.#{email_col} = validate_email(NEW.#{email_col});
|
60
|
+
END IF;
|
61
|
+
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
|
65
|
+
execute <<-SQL
|
66
|
+
CREATE OR REPLACE FUNCTION #{table_name.singularize}_changed()
|
67
|
+
RETURNS TRIGGER AS
|
68
|
+
$BODY$
|
69
|
+
BEGIN
|
70
|
+
#{password_text}
|
71
|
+
#{email_text}
|
72
|
+
RETURN NEW;
|
73
|
+
END;
|
74
|
+
$BODY$
|
75
|
+
language 'plpgsql';
|
76
|
+
SQL
|
77
|
+
|
78
|
+
execute <<-SQL
|
79
|
+
CREATE TRIGGER #{table_name}_on_insert
|
80
|
+
BEFORE INSERT ON #{table_name}
|
81
|
+
FOR EACH ROW
|
82
|
+
EXECUTE PROCEDURE #{table_name.singularize}_changed();
|
83
|
+
SQL
|
84
|
+
|
85
|
+
execute <<-SQL
|
86
|
+
CREATE TRIGGER #{table_name}_on_update
|
87
|
+
BEFORE UPDATE ON #{table_name}
|
88
|
+
FOR EACH ROW
|
89
|
+
EXECUTE PROCEDURE #{table_name.singularize}_changed();
|
90
|
+
|
91
|
+
SQL
|
92
|
+
end
|
93
|
+
|
94
|
+
d.down do
|
95
|
+
execute "DROP TRIGGER IF EXISTS #{table_name}_on_insert ON #{table_name};"
|
96
|
+
execute "DROP TRIGGER IF EXISTS #{table_name}_on_update ON #{table_name};"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
module Reflection
|
3
|
+
class AbstractReflection
|
4
|
+
def join_scope(table, foreign_klass)
|
5
|
+
predicate_builder = predicate_builder(table)
|
6
|
+
scope_chain_items = join_scopes(table, predicate_builder)
|
7
|
+
klass_scope = klass_join_scope(table, predicate_builder)
|
8
|
+
|
9
|
+
if type
|
10
|
+
klass_scope.where!(type => foreign_klass.__send__(options[:primary_type] || :table_name))
|
11
|
+
end
|
12
|
+
|
13
|
+
scope_chain_items.inject(klass_scope, &:merge!)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class RuntimeReflection < AbstractReflection # :nodoc:
|
18
|
+
delegate :scope, :type, :constraints, :get_join_keys, :options, to: :@reflection
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module BetterRecord
|
2
|
+
module Relation
|
3
|
+
# pluck_in_batches: yields an array of *columns that is at least size
|
4
|
+
# batch_size to a block.
|
5
|
+
#
|
6
|
+
# Special case: if there is only one column selected than each batch
|
7
|
+
# will yield an array of columns like [:column, :column, ...]
|
8
|
+
# rather than [[:column], [:column], ...]
|
9
|
+
# Arguments
|
10
|
+
# columns -> an arbitrary selection of columns found on the table.
|
11
|
+
# batch_size -> How many items to pluck at a time
|
12
|
+
# &block -> A block that processes an array of returned columns.
|
13
|
+
# Array is, at most, size batch_size
|
14
|
+
#
|
15
|
+
# Returns
|
16
|
+
# nothing is returned from the function
|
17
|
+
def pluck_in_batches(*columns, batch_size: 1000)
|
18
|
+
if columns.empty?
|
19
|
+
raise "There must be at least one column to pluck"
|
20
|
+
end
|
21
|
+
|
22
|
+
# the :id to start the query at
|
23
|
+
batch_start = nil
|
24
|
+
|
25
|
+
# It's cool. We're only taking in symbols
|
26
|
+
# no deep clone needed
|
27
|
+
select_columns = columns.dup
|
28
|
+
|
29
|
+
# Find index of :id in the array
|
30
|
+
remove_id_from_results = false
|
31
|
+
id_index = columns.index(primary_key.to_sym)
|
32
|
+
|
33
|
+
# :id is still needed to calculate offsets
|
34
|
+
# add it to the front of the array and remove it when yielding
|
35
|
+
if id_index.nil?
|
36
|
+
id_index = 0
|
37
|
+
select_columns.unshift(primary_key)
|
38
|
+
|
39
|
+
remove_id_from_results = true
|
40
|
+
end
|
41
|
+
|
42
|
+
loop do
|
43
|
+
relation = self.reorder(table[primary_key].asc).limit(batch_size)
|
44
|
+
relation = relation.where(table[primary_key].gt(batch_start)) if batch_start
|
45
|
+
items = relation.pluck(*select_columns)
|
46
|
+
|
47
|
+
break if items.empty?
|
48
|
+
|
49
|
+
# Use the last id to calculate where to offset queries
|
50
|
+
last_item = items.last
|
51
|
+
batch_start = last_item.is_a?(Array) ? last_item[id_index] : last_item
|
52
|
+
|
53
|
+
# Remove :id column if not in *columns
|
54
|
+
items.map! { |row| row[1..-1] } if remove_id_from_results
|
55
|
+
|
56
|
+
yield items
|
57
|
+
|
58
|
+
break if items.size < batch_size
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
class CreateHelperFunctionsGenerator < ActiveRecord::Generators::Base
|
6
|
+
source_root File.expand_path('templates', __dir__)
|
7
|
+
argument :name, type: :string, default: 'testes'
|
8
|
+
class_option :audit_schema, type: :string, default: 'audit'
|
9
|
+
class_option :eject, type: :boolean, default: false
|
10
|
+
|
11
|
+
def copy_initializer
|
12
|
+
template 'gitattributes', '.gitattributes'
|
13
|
+
template 'gitignore', '.gitignore'
|
14
|
+
template 'jsbeautifyrc', '.jsbeautifyrc'
|
15
|
+
template 'pryrc', '.pryrc'
|
16
|
+
template 'ruby-version', '.ruby-version'
|
17
|
+
template 'postgres-audit-trigger.psql', 'db/postgres-audit-trigger.psql'
|
18
|
+
if !!options['eject']
|
19
|
+
template 'initializer.rb', 'config/initializers/better_record.rb'
|
20
|
+
else
|
21
|
+
template 'initializer.rb', 'config/initializers/better_record.rb'
|
22
|
+
end
|
23
|
+
migration_template "migration.rb", "#{migration_path}/create_database_helper_functions.rb", migration_version: migration_version, force: true
|
24
|
+
application ''
|
25
|
+
|
26
|
+
eager_line = 'config.eager_load_paths += Dir["#{config.root}/lib/modules/**/"]'
|
27
|
+
|
28
|
+
gsub_file 'config/application.rb', /([ \t]*?#{Regexp.escape(eager_line)}[ \t0-9\.]*?)\n/mi do |match|
|
29
|
+
""
|
30
|
+
end
|
31
|
+
|
32
|
+
gsub_file 'config/application.rb', /#{Regexp.escape("config.load_defaults")}[ 0-9\.]+\n/mi do |match|
|
33
|
+
"#{match} #{'config.eager_load_paths += Dir["#{config.root}/lib/modules/**/"]'}\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
gsub_file 'app/models/application_record.rb', /(#{Regexp.escape("class ApplicationRecord < ActiveRecord::Base")})/mi do |match|
|
37
|
+
p match
|
38
|
+
"class ApplicationRecord < BetterRecord::Base"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def audit_schema
|
43
|
+
@audit_schema ||= options['audit_schema'].presence || 'audit'
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def migration_path
|
49
|
+
if Rails.version >= '5.0.3'
|
50
|
+
db_migrate_path
|
51
|
+
else
|
52
|
+
@migration_path ||= File.join("db", "migrate")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def migration_version
|
57
|
+
if Rails.version.start_with? '5'
|
58
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: better_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sampson Crowley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: |2
|
42
|
+
This app extends active record to allow you to change the polymorphic type value in relationships.
|
43
|
+
It also extends active record batching functions to be able to order records while batching.
|
44
|
+
- As a bonus. the same 'split_batches' function is made available to Arrays
|
45
|
+
It also adds optional auditing functions and password hashing functions, as well as migration helpers
|
46
|
+
email:
|
47
|
+
- sampsonsprojects@gmail.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- MIT-LICENSE
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- app/assets/config/better_record_manifest.js
|
56
|
+
- app/controllers/better_record/application_controller.rb
|
57
|
+
- app/helpers/better_record/application_helper.rb
|
58
|
+
- app/jobs/better_record/application_job.rb
|
59
|
+
- app/mailers/better_record/application_mailer.rb
|
60
|
+
- app/models/better_record/audit.rb
|
61
|
+
- app/models/better_record/base.rb
|
62
|
+
- app/models/better_record/table_size.rb
|
63
|
+
- config/routes.rb
|
64
|
+
- lib/better_record.rb
|
65
|
+
- lib/better_record/associations.rb
|
66
|
+
- lib/better_record/batches.rb
|
67
|
+
- lib/better_record/engine.rb
|
68
|
+
- lib/better_record/migration.rb
|
69
|
+
- lib/better_record/nullify_blank_attributes.rb
|
70
|
+
- lib/better_record/railtie.rb
|
71
|
+
- lib/better_record/reflection.rb
|
72
|
+
- lib/better_record/relation.rb
|
73
|
+
- lib/better_record/version.rb
|
74
|
+
- lib/generators/create_helper_functions/USAGE
|
75
|
+
- lib/generators/create_helper_functions/create_helper_functions_generator.rb
|
76
|
+
- lib/tasks/db/create_audits.rake
|
77
|
+
homepage: https://github.com/SampsonCrowley/multi_app_active_record
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.7.6
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Fix functions that are lacking in Active record to be compatible with multi-app
|
101
|
+
databases
|
102
|
+
test_files: []
|