prefixed_ids 1.8.1 → 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 +4 -4
- data/README.md +31 -33
- data/Rakefile +0 -2
- data/lib/prefixed_ids/prefix_id.rb +13 -2
- data/lib/prefixed_ids/version.rb +1 -1
- data/lib/prefixed_ids.rb +90 -17
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bac5b304f58877aefa5452f7e1532a65071b7f2eeadd6347a144326d86737cd
|
|
4
|
+
data.tar.gz: 91facf11c39db364b50ca88138429308436328f22446b19d8d6a6e2c835c51d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
38
|
+
> [!NOTE]
|
|
39
|
+
> Add `has_prefix_id` _before_ associations because it overrides `has_many` to include prefix ID helpers.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
### Prefix ID Param
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
By default, Prefixed IDs overrides `to_param` in the model to use prefix IDs.
|
|
42
44
|
|
|
43
|
-
To
|
|
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
|
-
|
|
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
|
|
68
|
+
#=> #<User>
|
|
62
69
|
```
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
> [!NOTE]
|
|
72
|
+
> `find` still finds records by primary key. For example, `User.find(1)` still works.
|
|
66
73
|
|
|
67
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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 `
|
|
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
|
@@ -19,8 +19,12 @@ module PrefixedIds
|
|
|
19
19
|
# decode returns an array
|
|
20
20
|
def decode(id, fallback: false)
|
|
21
21
|
fallback_value = fallback ? id : nil
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
@@ -32,6 +36,13 @@ module PrefixedIds
|
|
|
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
|
data/lib/prefixed_ids/version.rb
CHANGED
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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: []
|