prefixed_ids 1.8.0 → 1.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73928771de7b068333b2db44b844d9018cd0a583067268374eafca46426c8bc8
4
- data.tar.gz: fc5d49d74a69e210378c8c7ec2d980aa39fce0e61882a96207e4f03beb4ee2c0
3
+ metadata.gz: 1bac5b304f58877aefa5452f7e1532a65071b7f2eeadd6347a144326d86737cd
4
+ data.tar.gz: 91facf11c39db364b50ca88138429308436328f22446b19d8d6a6e2c835c51d8
5
5
  SHA512:
6
- metadata.gz: 1ddaffaaf511acab48e02dee0664a9d6310e24306e36792800e5afeba9e58b0c6a8e0413f2fd0da521d2e900d8dda2c5d8c8dea36b4a67b0b47566971620f28b
7
- data.tar.gz: 5d2406555e427e660ddbe71a8c3c9448ab27e54acf0427f6e232c655224524173ed4492c6352a36be191a62b91652efbf1556c6588f26d836da853cc9da99152
6
+ metadata.gz: 48048178e689a688bae0f048a8b08fe246dc2b60de2a339e3458117d2945b15757b1c1888b655e2306883a78e345256cf23f565b29a94f72972cf4b8c7043883
7
+ data.tar.gz: 5ae3f646bd1df6a314e15c12a732257b1eb242058ec7775d75a60f9862ff5034e20d2eed190d88e4190456109d76754e4d56e7c970ef2a0e229d2d4aea9a1156
data/README.md CHANGED
@@ -18,6 +18,7 @@ This gem works by hashing the record's original `:id` attribute using [`Hashids`
18
18
  Inspired by [Stripe's prefixed IDs](https://stripe.com/docs/api) in their API.
19
19
 
20
20
  ## 🚀 Installation
21
+
21
22
  Add this line to your application's Gemfile:
22
23
 
23
24
  ```ruby
@@ -34,39 +35,50 @@ class User < ApplicationRecord
34
35
  end
35
36
  ```
36
37
 
37
- This will generate a value like `user_1234abcd`.
38
+ > [!NOTE]
39
+ > Add `has_prefix_id` _before_ associations because it overrides `has_many` to include prefix ID helpers.
38
40
 
39
- Note: You should add `has_prefix_id` before associations because it overrides `has_many` to add prefix ID lookups.
41
+ ### Prefix ID Param
40
42
 
41
- ##### Prefix ID Param
43
+ By default, Prefixed IDs overrides `to_param` in the model to use prefix IDs.
42
44
 
43
- To retrieve the prefix ID, simply call:
45
+ To get the prefix ID for a record:
44
46
 
45
47
  ```ruby
46
48
  @user.to_param
49
+ #=> "user_12345abcd"
47
50
  ```
48
51
 
49
52
  If `to_param` override is disabled:
50
53
 
51
54
  ```ruby
52
55
  @user.prefix_id
56
+ #=> "user_12345abcd"
53
57
  ```
54
58
 
55
59
  ##### Query by Prefixed ID
56
60
 
57
- To query using the prefixed ID, you can use either `find`, `find_by_prefix_id`, or `find_by_prefix_id!`:
61
+ By default, prefixed_ids overrides `find` and `to_param` to seamlessly URLs automatically.
58
62
 
59
63
  ```ruby
64
+ User.first.to_param
65
+ #=> "user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe"
66
+
60
67
  User.find("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe")
61
- User.find_by_prefix_id("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe")
68
+ #=> #<User>
62
69
  ```
63
70
 
64
- ⚠️ Note that `find` still finds records by the primary key. Eg. `localhost/users/1` still works.
65
- If you're targeting security issues by masking the ID, make sure to use `find_by_prefix_id` and [add a salt](#salt).
71
+ > [!NOTE]
72
+ > `find` still finds records by primary key. For example, `User.find(1)` still works.
66
73
 
67
- We also override `to_param` by default so it'll be used in URLs automatically.
74
+ You can also use `find_by_prefix_id` or `find_by_prefix_id!` when the `find` override is disabled:
75
+
76
+ ```ruby
77
+ User.find_by_prefix_id("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe") # Returns a User or nil
78
+ User.find_by_prefix_id!("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe") # Raises an exception if not found
79
+ ```
68
80
 
69
- To disable find and to_param overrides, simply pass in the options:
81
+ To disable `find` and `to_param` overrides, pass the following options:
70
82
 
71
83
  ```ruby
72
84
  class User < ApplicationRecord
@@ -74,9 +86,12 @@ class User < ApplicationRecord
74
86
  end
75
87
  ```
76
88
 
89
+ > [!NOTE]
90
+ > If you're aiming to mask primary key IDs, make sure to use `find_by_prefix_id` and [add a salt](#salt). Prefix IDs are not a replacement for authorization, so always scope lookups to records the user is allowed to access.
91
+
77
92
  ##### Salt
78
93
 
79
- A salt is a secret value that makes it impossible to reverse engineer IDs. We recommend adding a salt to make your Prefix IDs unguessable.
94
+ Without a salt, anyone can decode your Prefix IDs since they're only based on the table name. Adding a secret salt makes them much harder to decode or guess, but this is obfuscation, not encryption.
80
95
 
81
96
  ###### Global Salt
82
97
 
@@ -93,36 +108,19 @@ class User
93
108
  end
94
109
  ```
95
110
 
96
- ### Generic Lookup By Prefix ID
111
+ ### Find Any Model By Prefix ID
97
112
 
98
- Imagine you have a prefixed ID but you don't know which model it belongs to.
113
+ Imagine you have a prefixed ID but you don't know which model it belongs to:
99
114
 
100
115
  ```ruby
101
116
  PrefixedIds.find("user_5vJjbzXq9KrLEMm3")
102
117
  #=> #<User>
118
+
103
119
  PrefixedIds.find("acct_2iAnOP0xGDYk6dpe")
104
120
  #=> #<Account>
105
121
  ```
106
122
 
107
- ### Exists
108
-
109
- You can check if a record exists by its prefixed ID:
110
-
111
- ```ruby
112
- User.exists?("user_5vJjbzXq9KrLEMm3")
113
- #=> true
114
- ```
115
-
116
- If given anything other than a prefixed ID, or `override_exists` is set to false, it will fall back to its original `exists?` method.
117
-
118
- ```ruby
119
- class User < ApplicationRecord
120
- has_prefix_id :user, override_exists: false
121
- end
122
-
123
- User.exists?("user_5vJjbzXq9KrLEMm3")
124
- #=> false
125
- ```
123
+ This works similarly to GlobalIDs.
126
124
 
127
125
  ### Customizing Prefix IDs
128
126
 
@@ -138,7 +136,7 @@ By default, `find` will accept both Prefix IDs and regular IDs. Setting `fallbac
138
136
 
139
137
  ## Development
140
138
 
141
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
139
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/rails console` for an interactive prompt that will allow you to experiment.
142
140
 
143
141
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
144
142
 
data/Rakefile CHANGED
@@ -3,8 +3,6 @@ require "bundler/setup"
3
3
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
- load "rails/tasks/statistics.rake"
7
-
8
6
  require "bundler/gem_tasks"
9
7
 
10
8
  require "rake/testtask"
@@ -19,19 +19,30 @@ module PrefixedIds
19
19
  # decode returns an array
20
20
  def decode(id, fallback: false)
21
21
  fallback_value = fallback ? id : nil
22
- _, id_without_prefix = PrefixedIds.split_id(id, @delimiter)
23
- decoded_hashid = @hashids.decode(id_without_prefix)
22
+ prefix, id_without_prefix = PrefixedIds.split_id(id, @delimiter)
23
+
24
+ # Reject IDs whose prefix isn't ours, even if the hash would decode
25
+ return fallback_value unless prefix == @prefix
26
+
27
+ decoded_hashid = decode_hashid(id_without_prefix)
24
28
 
25
29
  if !valid?(decoded_hashid)
26
30
  fallback_value
27
31
  else
28
- _, id, *composite = decoded_hashid
29
- composite.empty? ? id : composite
32
+ _, *ids = decoded_hashid
33
+ (ids.size == 1) ? ids.first : ids
30
34
  end
31
35
  end
32
36
 
33
37
  private
34
38
 
39
+ # Bad characters from user input shouldn't blow up finders
40
+ def decode_hashid(hashid)
41
+ @hashids.decode(hashid)
42
+ rescue Hashids::InputError
43
+ []
44
+ end
45
+
35
46
  def valid?(decoded_hashid)
36
47
  decoded_hashid.size >= 2 && decoded_hashid.first == TOKEN
37
48
  end
@@ -1,3 +1,3 @@
1
1
  module PrefixedIds
2
- VERSION = "1.8.0"
2
+ VERSION = "1.9.0"
3
3
  end
data/lib/prefixed_ids.rb CHANGED
@@ -16,18 +16,37 @@ module PrefixedIds
16
16
  mattr_accessor :models, default: {}
17
17
 
18
18
  def self.find(prefix_id)
19
- prefix, _ = split_id(prefix_id)
19
+ prefix, = split_id(prefix_id)
20
20
  models.fetch(prefix).find_by_prefix_id(prefix_id)
21
21
  rescue KeyError
22
22
  raise Error, "Unable to find model with prefix `#{prefix}`. Available prefixes are: #{models.keys.join(", ")}"
23
23
  end
24
24
 
25
+ # Returns a decoded ID from a prefix_id
26
+ def self.decode_prefix_id(prefix_id)
27
+ return prefix_id if prefix_id.nil? || prefix_id.is_a?(Integer)
28
+ return prefix_id unless models.keys.any? { |key| prefix_id.to_s.start_with?("#{key}#{delimiter}") }
29
+
30
+ model_name, _ = split_id(prefix_id)
31
+
32
+ models.fetch(model_name).decode_prefix_id(prefix_id)
33
+ end
34
+
25
35
  # Splits a prefixed ID into its prefix and ID
26
36
  def self.split_id(prefix_id, delimiter = PrefixedIds.delimiter)
27
37
  prefix, _, id = prefix_id.to_s.rpartition(delimiter)
28
38
  [prefix, id]
29
39
  end
30
40
 
41
+ def self.register_prefix(prefix, model:)
42
+ # Compare names since code reloading creates a new class for the same model
43
+ if (existing_model = PrefixedIds.models[prefix]) && existing_model.name != model.name
44
+ raise Error, "Prefix #{prefix} already defined for model #{existing_model}"
45
+ end
46
+
47
+ PrefixedIds.models[prefix] = model
48
+ end
49
+
31
50
  # Adds `has_prefix_id` method
32
51
  module Rails
33
52
  extend ActiveSupport::Concern
@@ -35,6 +54,7 @@ module PrefixedIds
35
54
  included do
36
55
  class_attribute :_prefix_id
37
56
  class_attribute :_prefix_id_fallback
57
+ class_attribute :_prefix_id_override_exists
38
58
  end
39
59
 
40
60
  class_methods do
@@ -42,20 +62,46 @@ module PrefixedIds
42
62
  include Attribute
43
63
  include Finder if override_find
44
64
  include ToParam if override_param
45
- include Exists if override_exists
65
+
46
66
  self._prefix_id = PrefixId.new(self, prefix, **options)
47
67
  self._prefix_id_fallback = fallback
68
+ self._prefix_id_override_exists = override_exists
48
69
 
49
70
  # Register with PrefixedIds to support PrefixedIds#find
50
- PrefixedIds.models[prefix.to_s] = self
71
+ PrefixedIds.register_prefix(prefix.to_s, model: self)
51
72
  end
52
73
  end
53
74
  end
54
75
 
76
+ # Decodes prefix IDs passed to `exists?` unless disabled with `override_exists: false`
77
+ module Exists
78
+ # Only Strings are decoded. Rails calls exists? internally with Integer IDs and conditions, so those pass through
79
+ def exists?(conditions = :none)
80
+ return super unless _prefix_id_override_exists && _prefix_id.present? && conditions.is_a?(String)
81
+
82
+ id = _prefix_id.decode(conditions, fallback: _prefix_id_fallback)
83
+ id.nil? ? false : super(id)
84
+ end
85
+ end
86
+
55
87
  # Included when a module uses `has_prefix_id`
56
88
  module Attribute
57
89
  extend ActiveSupport::Concern
58
90
 
91
+ included do
92
+ extend Exists
93
+ end
94
+
95
+ # Methods added to relations and has_many associations
96
+ module RelationMethods
97
+ include Exists
98
+
99
+ # Without arguments, returns prefix IDs for the records in the relation
100
+ def prefix_ids(ids = nil)
101
+ klass.prefix_ids(ids || pluck(:id))
102
+ end
103
+ end
104
+
59
105
  class_methods do
60
106
  def find_by_prefix_id(id)
61
107
  find_by(id: _prefix_id.decode(id))
@@ -80,6 +126,16 @@ module PrefixedIds
80
126
  def decode_prefix_ids(ids)
81
127
  ids.map { |id| decode_prefix_id(id) }
82
128
  end
129
+
130
+ def relation
131
+ super.tap { |r| r.extend RelationMethods }
132
+ end
133
+
134
+ def has_many(*args, &block)
135
+ options = args.extract_options!
136
+ options[:extend] = Array(options[:extend]).push(RelationMethods)
137
+ super(*args, **options, &block)
138
+ end
83
139
  end
84
140
 
85
141
  def prefix_id
@@ -98,6 +154,7 @@ module PrefixedIds
98
154
  prefix_ids = ids.flatten.map do |id|
99
155
  prefix_id = _prefix_id.decode(id, fallback: _prefix_id_fallback)
100
156
  raise Error, "#{id} is not a valid prefix_id" if !_prefix_id_fallback && prefix_id.nil?
157
+
101
158
  prefix_id
102
159
  end
103
160
  prefix_ids = [prefix_ids] if ids.first.is_a?(Array)
@@ -109,6 +166,36 @@ module PrefixedIds
109
166
  super.tap { |r| r.extend ClassMethods }
110
167
  end
111
168
 
169
+ def belongs_to(*args, **options, &block)
170
+ association = super
171
+
172
+ name = args.first
173
+ reflection = association[name] || association[name.to_s]
174
+
175
+ return association if reflection.polymorphic?
176
+
177
+ # Skip helpers if the associated class can't be loaded yet, so Rails only raises when the association is used
178
+ klass = begin
179
+ reflection.klass
180
+ rescue NameError
181
+ nil
182
+ end
183
+ return association if klass.nil? || klass._prefix_id.blank?
184
+
185
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
186
+ def #{name}_prefix_id
187
+ #{klass}._prefix_id.encode(#{reflection.foreign_key})
188
+ end
189
+
190
+ def #{name}_prefix_id=(prefix_id)
191
+ decoded_id = #{klass}._prefix_id.decode(prefix_id, fallback: #{klass}._prefix_id_fallback)
192
+ send("#{reflection.foreign_key}=", decoded_id)
193
+ end
194
+ CODE
195
+
196
+ association
197
+ end
198
+
112
199
  def has_many(*args, &block)
113
200
  options = args.extract_options!
114
201
  options[:extend] = Array(options[:extend]).push(ClassMethods)
@@ -124,18 +211,4 @@ module PrefixedIds
124
211
  _prefix_id.encode(id)
125
212
  end
126
213
  end
127
-
128
- module Exists
129
- extend ActiveSupport::Concern
130
-
131
- class_methods do
132
- def exists?(id)
133
- if _prefix_id.present? && id.is_a?(String)
134
- super(_prefix_id.decode(id))
135
- else
136
- super
137
- end
138
- end
139
- end
140
- end
141
214
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prefixed_ids
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -65,7 +64,6 @@ metadata:
65
64
  homepage_uri: https://github.com/excid3/prefixed_ids
66
65
  source_code_uri: https://github.com/excid3/prefixed_ids
67
66
  changelog_uri: https://github.com/excid3/prefixed_ids
68
- post_install_message:
69
67
  rdoc_options: []
70
68
  require_paths:
71
69
  - lib
@@ -80,8 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
78
  - !ruby/object:Gem::Version
81
79
  version: '0'
82
80
  requirements: []
83
- rubygems_version: 3.5.13
84
- signing_key:
81
+ rubygems_version: 4.0.20
85
82
  specification_version: 4
86
83
  summary: Prefixed IDs generates IDs with friendly prefixes for your models
87
84
  test_files: []