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 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,2 @@
1
+ //= link_directory ../javascripts/better_record .js
2
+ //= link_directory ../stylesheets/better_record .css
@@ -0,0 +1,5 @@
1
+ module BetterRecord
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module BetterRecord
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module BetterRecord
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module BetterRecord
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -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,2 @@
1
+ BetterRecord::Engine.routes.draw do
2
+ end
@@ -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,5 @@
1
+ module BetterRecord
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace BetterRecord
4
+ end
5
+ 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,8 @@
1
+ module BetterRecord
2
+ module NullifyBlankAttributes
3
+ def write_attribute(attr_name, value)
4
+ new_value = value == false ? false : value.presence
5
+ super(attr_name, new_value)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module BetterRecord
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ 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,3 @@
1
+ module BetterRecord
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate create_helper_functions --schema_name audit
6
+
7
+ This will create:
8
+ db/migrate/*_create_database_helper_functions.rb
9
+ db/postgres-audit-trigger.psql
@@ -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: []