set_as_primary 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09252a0d5cef2ec2dbf697837059b9604bad6617f82e2938fb95b0e7ea54749e'
4
- data.tar.gz: 8bed32971351c098816889638e39307b44692445fb5593a6941b28010344ea26
3
+ metadata.gz: 06f7d231ec1dc1effe46cd6b516c12bcffdf98b3feadde4d0f0a5b8e193f2047
4
+ data.tar.gz: 6f21389fa3d8aa3c8a2bc512dfb26874b84c9b3cfd060afc3b9c7ba18cac9781
5
5
  SHA512:
6
- metadata.gz: 7ff0a54d3af341c41ba937311a449ebb3adf582390b4dee9880aa36e548d56433c1314836e0246680b9e8611b9643085a390f48ab9962edbe5aa31501adb5ecd
7
- data.tar.gz: b4f24ce736c8f261252e48e342896a13a0710a36fc2abf17fb3b170e4291f945ad47dc900cb0ee0c2ae839d7af0f3358c4657cf21806f1d4ae1d07178ee7ae18
6
+ metadata.gz: 625e12dd80c2c13f78d5d557504db3cfbce52124846c64d735ed71a16f4f0feb1b41f69a8da8a3c20e58ecc29e3ebff5fdd0cb229dc9c1f78495fb6ff46b56fc
7
+ data.tar.gz: b706248616ceacf293a0b0ff8c9769226ae5e088c026cc655b3937159c5606132b8bb37665e425bac213c2c95e93da43a8132631e50822f94a19f8d6a4030446
@@ -81,7 +81,7 @@ Layout/EmptyLinesAroundModuleBody:
81
81
  Style/HashSyntax:
82
82
  Enabled: true
83
83
 
84
- Layout/IndentFirstArgument:
84
+ Layout/FirstArgumentIndentation:
85
85
  Enabled: true
86
86
 
87
87
  # Method definitions after `private` or `protected` isolated calls need one
@@ -170,7 +170,7 @@ Layout/Tab:
170
170
  Enabled: true
171
171
 
172
172
  # Blank lines should not have any spaces.
173
- Layout/TrailingBlankLines:
173
+ Layout/TrailingEmptyLines:
174
174
  Enabled: true
175
175
 
176
176
  # No trailing whitespace.
@@ -197,7 +197,7 @@ Lint/RequireParentheses:
197
197
  Lint/ShadowingOuterLocalVariable:
198
198
  Enabled: true
199
199
 
200
- Lint/StringConversionInInterpolation:
200
+ Lint/RedundantStringCoercion:
201
201
  Enabled: true
202
202
 
203
203
  Lint/UriEscapeUnescape:
@@ -1,3 +1,9 @@
1
+ ## 0.1.2 (2019-02-02)
2
+ - Supported unique partial index.
3
+ - Changed generator definition. Now it also accepts `flag_name`, `owner_key`.
4
+ - Handled `force_primary` in `after_create` callback.
5
+ - Refactored the code.
6
+
1
7
  ## 0.1.1 (2019-12-29)
2
8
 
3
9
  - Added support for single model with no association.
data/Gemfile CHANGED
@@ -8,7 +8,7 @@ gemspec
8
8
  gem "activerecord", "~> 6.0.0"
9
9
 
10
10
  group :development do
11
- gem "rubocop", ">= 0.47", require: false
12
- gem "rubocop-performance", require: false
13
- gem "rubocop-rails", require: false
11
+ gem "rubocop", ">= 0.47"
12
+ gem "rubocop-performance"
13
+ gem "rubocop-rails"
14
14
  end
data/README.md CHANGED
@@ -1,12 +1,19 @@
1
1
  # SetAsPrimary
2
2
 
3
- The simplest way to handle the primary or default flag (:white_check_mark:) to
3
+ The simplest way to handle the primary or default flag to
4
4
  your Rails models.
5
5
 
6
- Supports PostgreSQL, MySQL, and SQLite.
6
+ Features:
7
+
8
+ * Supports single model (without association), model with (`belongs_to`) association, and even polymorphic associations
9
+ * Force primary
10
+ * Supports PostgreSQL's unique partial index (constraint)
11
+ * Supports PostgreSQL, MySQL, and SQLite
12
+
13
+
7
14
 
8
15
  [Demo Rails application](https://cryptic-lake-90495.herokuapp.com/) |
9
- [Demo Rails application GitHub repository](https://github.com/mechanicles/set_as_primary_rails_app)
16
+ [Source code of Demo Rails application](https://github.com/mechanicles/set_as_primary_rails_app)
10
17
 
11
18
  [![Build Status](https://travis-ci.org/mechanicles/set_as_primary.svg?branch=master)](https://travis-ci.org/mechanicles/set_as_primary)
12
19
 
@@ -33,7 +40,7 @@ Address, etc., which belong to the User/Person model or polymorphic model. There
33
40
  you might need to set a primary email address, primary phone number, or default
34
41
  address for a user, and this gem helps you to do that.
35
42
 
36
- It also supports single model with no association's context.
43
+ It also supports a single model with no association context.
37
44
 
38
45
  Examples:
39
46
 
@@ -57,20 +64,20 @@ class Address < ApplicationRecord
57
64
  set_as_primary :primary, owner_key: :owner
58
65
  end
59
66
 
60
- # Single model with no owner/association's context.
67
+ # Single model with no owner/association context.
61
68
  class Post < ApplicationRecord
62
69
  include SetAsPrimary
63
70
 
64
- set_as_primary
71
+ set_as_primary :primary
65
72
  end
66
73
  ```
67
74
 
68
75
  You need to include `SetAsPrimary` module in your model where you want to handle the primary flag.
69
76
  Then to `set_as_primary` class helper method, pass your primary flag attribute. You might need to pass
70
- association key `owner_key` if you wan to consider owner's (association's) context.
77
+ association key `owner_key` if you wan to consider owner (association) context.
71
78
 
72
- **NOTE:** Default primary flag attribute is `primary`, and you can use another one too like `default` but
73
- make sure that flag should be present in the table and should be a boolean column type.
79
+ **Note:** Default primary flag attribute is `primary`, and you can use another one too like `default` but
80
+ make sure that flag should be present in the table and should be a boolean data type column.
74
81
 
75
82
  #### Migration
76
83
 
@@ -78,20 +85,55 @@ If your table does not have the primary flag column, then you can add it by runn
78
85
  following command in your rails project:
79
86
 
80
87
  ```ssh
81
- rails generate set_as_primary your_table_name
88
+ rails generate set_as_primary your_table_name flag_name
89
+ ```
90
+
91
+ Example:
92
+
93
+ If you want to add a `primary` column to your `posts` table, then you can run command like this:
94
+
95
+ ```shell
96
+ rails generate set_as_primary posts primary
97
+ ```
98
+
99
+ Then migration gets created like this:
100
+
101
+ ```ruby
102
+ class AddPrimaryColumnToPosts < ActiveRecord::Migration[6.0]
103
+ def change
104
+ add_column :posts, :primary, :boolean, default: false, null: false
105
+ # NOTE: Please uncomment following line if you want only one 'true' (constraint) in the table.
106
+ # add_index :posts, :primary, unique: true, where: "(posts.primary IS TRUE)"
107
+ end
108
+ end
109
+ ```
110
+
111
+ If you want to create a primary column to `email_addresses` table, then you can run command like this:
112
+
113
+ ```shell
114
+ rails generate set_as_primary email_addresses primary user
82
115
  ```
83
116
 
84
- If you run above command for `email_addresses` table, then it creates
85
- migration like this:
117
+ Then it creates migration like this:
86
118
 
87
119
  ```ruby
88
120
  class AddPrimaryColumnToEmailAddresses < ActiveRecord::Migration[6.0]
89
121
  def change
90
122
  add_column :email_addresses, :primary, :boolean, default: false, null: false
123
+ # NOTE: Please uncomment following line if you want only one 'true' (constraint) in the table.
124
+ # add_index :email_addresses, %i[user_id, primary], unique: true, where: "(email_addresses.primary IS TRUE)"
91
125
  end
92
126
  end
93
127
  ```
94
- Don't forget to run `rails db:migrate` to create actual column in the table.
128
+ You might have seen extra commented lines there. These lines are there for handling the unique constraint. Currently, these lines get created only for `PostgreSQL` adapter as it supports partial index.
129
+
130
+ Please note that here we have passed an extra option `user` in the command that is nothing but the owner/association name. This extra option helps to handle the unique partial index.
131
+
132
+ **Note:** Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+. But I also found that SQLite gives an error so currently this gem only supports PostgreSQL's unique partial index constraint.
133
+
134
+ **Even if we don't have constraint (only one 'true' constraint in the table), this gem takes care of it so don't worry about the constraint.**
135
+
136
+ Once migration file gets created, don't forget to run `rails db:migrate` to create an actual column in the table.
95
137
 
96
138
  #### force_primary
97
139
 
@@ -114,8 +156,6 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
114
156
  `rake test` to run the tests. You can also run `bin/console` for an interactive
115
157
  prompt that will allow you to experiment.
116
158
 
117
- To install this gem onto your local machine, run `bundle exec rake install`.
118
-
119
159
  ## Contributing
120
160
 
121
161
  Bug reports and pull requests are welcome on GitHub at
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #!/usr/bin/env ruby
2
4
 
3
5
  require "bundler/setup"
data/bin/setup CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #!/usr/bin/env bash
2
4
  set -euo pipefail
3
5
  IFS=$'\n\t'
@@ -6,21 +6,54 @@ module SetAsPrimary
6
6
  module Generators
7
7
  class SetAsPrimaryGenerator < Rails::Generators::Base
8
8
  include Rails::Generators::Migration
9
-
10
9
  source_root File.expand_path("templates", __dir__)
11
- argument :table_name, type: :string
12
10
 
13
- desc "Adds a boolean column 'primary' to the given table."
11
+ desc "Adds a user defined boolean column to the given table."
12
+
13
+ argument :table_name, type: :string
14
+ argument :flag_name, type: :string
15
+ argument :owner_key, type: :string, default: ""
14
16
 
15
17
  def copy_migration
16
18
  migration_template "migration.rb", "db/migrate/add_primary_column_to_#{table_name}.rb",
17
- migration_version: migration_version
19
+ migration_version: migration_version,
20
+ index_on: index_on,
21
+ support_partial_index: support_partial_index
18
22
  end
19
23
 
20
24
  def migration_version
21
25
  "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
22
26
  end
23
27
 
28
+ def index_on
29
+ if owner_key.present?
30
+ klass = table_name.classify.constantize
31
+ owner_association = klass.reflect_on_association(owner_key.to_sym)
32
+
33
+ if owner_association.nil?
34
+ raise ActiveRecord::AssociationNotFoundError.new(klass, owner_key)
35
+ end
36
+
37
+ owner_id_key = "#{owner_key}_id"
38
+
39
+ if owner_association.options[:polymorphic]
40
+ owner_type_key = "#{owner_key}_type"
41
+ "%i[#{owner_id_key}, #{owner_type_key}, #{flag_name}]"
42
+ else
43
+ "%i[#{owner_id_key}, #{flag_name}]"
44
+ end
45
+ else
46
+ ":#{flag_name}"
47
+ end
48
+ end
49
+
50
+ def support_partial_index
51
+ # NOTE: Partial indexes are only supported for PostgreSQL and SQLite 3.8.0+.
52
+ # Also found that, even if we use SQLite 3.8.0+, we still get a syntax error.
53
+ # So currently we have ignored SQLite.
54
+ ActiveRecord::Base.connection.adapter_name.downcase.to_sym == :postgresql
55
+ end
56
+
24
57
  def self.next_migration_number(dirname)
25
58
  ActiveRecord::Generators::Base.next_migration_number(dirname)
26
59
  end
@@ -1,6 +1,10 @@
1
1
  class AddPrimaryColumnTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- add_column :<%= table_name.to_sym %>, :primary, :boolean, default: false, null: false
3
+ add_column :<%= table_name %>, :<%= flag_name %>, :boolean, default: false, null: false
4
+ <%- if support_partial_index -%>
5
+ # NOTE: Please uncomment following line if you want only one 'true' (constraint) in the table.
6
+ # add_index :<%= table_name %>, <%= index_on %>, unique: true, where: "(<%= table_name %>.<%= flag_name %> IS TRUE)"
7
+ <%- end -%>
4
8
  end
5
9
  end
6
10
 
@@ -10,22 +10,22 @@ module SetAsPrimary
10
10
 
11
11
  included do
12
12
  before_save :unset_old_primary
13
- before_save :force_primary, if: -> { self.class._force_primary }
13
+ before_save :force_primary, if: -> { _klass._force_primary }
14
+ after_destroy :force_primary, if: -> { _klass._force_primary }
14
15
 
15
16
  instance_eval do
16
17
  class_attribute :_primary_flag_attribute, :_owner_key, :_force_primary
17
18
 
18
19
  def set_as_primary(primary_flag_attribute = :primary, options = {})
19
20
  if primary_flag_attribute.is_a?(Hash)
20
- options = primary_flag_attribute
21
- primary_flag_attribute = :primary
21
+ options = primary_flag_attribute; primary_flag_attribute = :primary
22
22
  end
23
23
 
24
24
  configuration = { owner_key: nil, force_primary: true }
25
25
 
26
26
  configuration.update(options) if options.is_a?(Hash)
27
27
 
28
- handle_setup_errors(primary_flag_attribute, configuration)
28
+ _handle_setup_errors(primary_flag_attribute, configuration)
29
29
 
30
30
  self._primary_flag_attribute = primary_flag_attribute
31
31
  self._owner_key = configuration[:owner_key]
@@ -33,9 +33,9 @@ module SetAsPrimary
33
33
  end
34
34
 
35
35
  private
36
- def handle_setup_errors(primary_flag_attribute, configuration)
36
+ def _handle_setup_errors(primary_flag_attribute, configuration)
37
37
  if !primary_flag_attribute.is_a?(Symbol)
38
- raise SetAsPrimary::Error, "Wrong attribute! Please provide attribute in symbol type."
38
+ raise SetAsPrimary::Error, "wrong argument type (expected Symbol)"
39
39
  end
40
40
 
41
41
  owner_key = configuration[:owner_key]
@@ -49,40 +49,49 @@ module SetAsPrimary
49
49
 
50
50
  private
51
51
  def unset_old_primary
52
- return unless self.public_send(self.class._primary_flag_attribute)
53
-
54
- scope = self.class.where(scope_options) if scope_options.present?
52
+ return unless public_send(_klass._primary_flag_attribute)
55
53
 
54
+ scope = _klass
55
+ scope = scope.where(_scope_options) if _scope_options.present?
56
56
  scope = scope.where("id != ?", id) unless new_record?
57
57
 
58
- scope.update_all(self.class._primary_flag_attribute => false)
58
+ scope.update_all(_klass._primary_flag_attribute => false)
59
59
  end
60
60
 
61
61
  def force_primary
62
- count = self.class.where(scope_options).count
63
-
64
- if (count == 1 && !new_record?) || (count == 0 && new_record?)
65
- self.public_send("#{self.class._primary_flag_attribute}=", true)
62
+ scope = _klass
63
+ scope = scope.where(_scope_options) if _scope_options.present?
64
+ count = scope.count
65
+
66
+ if count == 1 && destroyed?
67
+ object = scope.first
68
+ object.update_columns(_klass._primary_flag_attribute => true)
69
+ elsif (count == 1 && !new_record?) || (count == 0 && new_record?)
70
+ public_send("#{_klass._primary_flag_attribute}=", true)
66
71
  end
67
72
  end
68
73
 
69
- def scope_options
70
- return nil if self.class._owner_key.nil?
74
+ def _scope_options
75
+ return nil if _klass._owner_key.nil?
71
76
 
72
- @scope_option ||= if self.class.reflect_on_association(self.class._owner_key).options[:polymorphic]
73
- polymorphic_condition_options
74
- else
75
- owner_id = "#{self.class._owner_key}_id".to_sym
76
- { owner_id => self.public_send(owner_id) }
77
- end
77
+ @_scope_options ||= if _klass.reflect_on_association(_klass._owner_key).options[:polymorphic]
78
+ _polymorphic_condition_options
79
+ else
80
+ owner_id = "#{_klass._owner_key}_id".to_sym
81
+ { owner_id => public_send(owner_id) }
82
+ end
78
83
  end
79
84
 
80
- def polymorphic_condition_options
85
+ def _polymorphic_condition_options
81
86
  owner = self.public_send(self.class._owner_key)
82
87
 
83
88
  {
84
- "#{self.class._owner_key}_id".to_sym => owner.id,
85
- "#{self.class._owner_key}_type".to_sym => owner.class.name
89
+ "#{_klass._owner_key}_id".to_sym => owner.id,
90
+ "#{_klass._owner_key}_type".to_sym => owner.class.name
86
91
  }
87
92
  end
93
+
94
+ def _klass
95
+ self.class
96
+ end
88
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SetAsPrimary
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: set_as_primary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Santosh Wadghule
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-29 00:00:00.000000000 Z
11
+ date: 2020-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport