encoded_id-rails 1.0.0.rc1 → 1.0.0.rc7
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/CHANGELOG.md +97 -18
- data/LICENSE.txt +1 -1
- data/README.md +81 -473
- data/context/encoded_id-rails.md +651 -0
- data/context/encoded_id.md +437 -0
- data/lib/encoded_id/rails/active_record_finders.rb +54 -0
- data/lib/encoded_id/rails/annotated_id.rb +14 -9
- data/lib/encoded_id/rails/annotated_id_parser.rb +9 -1
- data/lib/encoded_id/rails/coder.rb +55 -10
- data/lib/encoded_id/rails/composite_id_base.rb +39 -0
- data/lib/encoded_id/rails/configuration.rb +66 -10
- data/lib/encoded_id/rails/encoder_methods.rb +30 -7
- data/lib/encoded_id/rails/finder_methods.rb +11 -0
- data/lib/encoded_id/rails/model.rb +60 -7
- data/lib/encoded_id/rails/path_param.rb +8 -0
- data/lib/encoded_id/rails/persists.rb +55 -9
- data/lib/encoded_id/rails/query_methods.rb +21 -4
- data/lib/encoded_id/rails/railtie.rb +13 -0
- data/lib/encoded_id/rails/salt.rb +8 -0
- data/lib/encoded_id/rails/slugged_id.rb +14 -9
- data/lib/encoded_id/rails/slugged_id_parser.rb +9 -1
- data/lib/encoded_id/rails/slugged_path_param.rb +8 -0
- data/lib/encoded_id/rails.rb +11 -6
- data/lib/generators/encoded_id/rails/install_generator.rb +36 -2
- data/lib/generators/encoded_id/rails/templates/{encoded_id.rb → hashids_encoded_id.rb} +49 -5
- data/lib/generators/encoded_id/rails/templates/sqids_encoded_id.rb +116 -0
- metadata +16 -24
- data/.devcontainer/Dockerfile +0 -17
- data/.devcontainer/compose.yml +0 -10
- data/.devcontainer/devcontainer.json +0 -12
- data/.standard.yml +0 -3
- data/Appraisals +0 -9
- data/Gemfile +0 -24
- data/Rakefile +0 -20
- data/Steepfile +0 -4
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/rails_7.2.gemfile +0 -19
- data/gemfiles/rails_8.0.gemfile +0 -19
- data/lib/encoded_id/rails/version.rb +0 -7
- data/rbs_collection.yaml +0 -24
- data/sig/encoded_id/rails.rbs +0 -141
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
# EncodedId::Rails - Rails Integration Technical Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`encoded_id-rails` provides seamless Rails integration for the `encoded_id` gem, enabling ActiveRecord models to use obfuscated IDs in URLs while maintaining standard Rails conventions. It offers multiple integration strategies from basic encoding to full ActiveRecord finder method overrides.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **ActiveRecord Integration**: Works seamlessly with Rails models
|
|
10
|
+
- **URL-Friendly IDs**: Automatic `to_param` overrides for encoded IDs in routes
|
|
11
|
+
- **Slugged IDs**: Human-readable slugs combined with encoded IDs (e.g., `john-doe--user_p5w9-z27j`)
|
|
12
|
+
- **Annotated IDs**: Model type prefixes for clarity (e.g., `user_p5w9-z27j`)
|
|
13
|
+
- **Finder Methods**: Find records using encoded IDs with familiar ActiveRecord syntax
|
|
14
|
+
- **Database Persistence**: Optional storage of encoded IDs for performance with automatic validations
|
|
15
|
+
- **Per-Model Configuration**: Different encoding strategies per model with inheritance support
|
|
16
|
+
- **ActiveRecord Finder Overrides**: Seamless integration with `find`, `find_by_id`, etc.
|
|
17
|
+
- **Record Duplication Safety**: Automatic encoded ID cache clearing on `dup`
|
|
18
|
+
|
|
19
|
+
## Quick Module Reference
|
|
20
|
+
|
|
21
|
+
| Module | Purpose | Key Method |
|
|
22
|
+
|--------|---------|------------|
|
|
23
|
+
| `EncodedId::Rails::Model` | Core functionality | `#encoded_id` |
|
|
24
|
+
| `EncodedId::Rails::PathParam` | URLs use encoded IDs | `#to_param` |
|
|
25
|
+
| `EncodedId::Rails::SluggedPathParam` | URLs use slugged IDs | `#to_param` |
|
|
26
|
+
| `EncodedId::Rails::ActiveRecordFinders` | Transparent finder overrides | `find()` |
|
|
27
|
+
| `EncodedId::Rails::Persists` | Database persistence with validations | `set_normalized_encoded_id!` |
|
|
28
|
+
|
|
29
|
+
## Installation & Setup
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Add to Gemfile
|
|
33
|
+
gem 'encoded_id-rails'
|
|
34
|
+
|
|
35
|
+
# Install
|
|
36
|
+
bundle install
|
|
37
|
+
|
|
38
|
+
# Generate configuration (prompts for encoder choice: sqids or hashids)
|
|
39
|
+
rails generate encoded_id:rails:install
|
|
40
|
+
|
|
41
|
+
# Or specify encoder directly
|
|
42
|
+
rails generate encoded_id:rails:install --encoder=sqids
|
|
43
|
+
# or
|
|
44
|
+
rails generate encoded_id:rails:install --encoder=hashids
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The generator creates `config/initializers/encoded_id.rb` with encoder-specific configuration:
|
|
48
|
+
- **Sqids**: No salt required, ready to use
|
|
49
|
+
- **Hashids**: Requires salt configuration (generator includes commented template)
|
|
50
|
+
|
|
51
|
+
## Core Modules
|
|
52
|
+
|
|
53
|
+
### EncodedId::Rails::Model
|
|
54
|
+
|
|
55
|
+
The base module that provides core functionality.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class User < ApplicationRecord
|
|
59
|
+
include EncodedId::Rails::Model
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Instance Methods
|
|
64
|
+
|
|
65
|
+
##### encoded_id
|
|
66
|
+
Returns the encoded ID with optional annotation prefix.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
user = User.find(123)
|
|
70
|
+
user.encoded_id # => "user_p5w9-z27j"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
##### slugged_encoded_id
|
|
74
|
+
Returns encoded ID with human-readable slug.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class User < ApplicationRecord
|
|
78
|
+
include EncodedId::Rails::Model
|
|
79
|
+
|
|
80
|
+
def name_for_encoded_id_slug
|
|
81
|
+
full_name.parameterize
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
user.slugged_encoded_id # => "john-doe--user_p5w9-z27j"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
##### annotation_for_encoded_id
|
|
89
|
+
Override to customize the annotation prefix (defaults to `model_name.underscore`).
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
def annotation_for_encoded_id
|
|
93
|
+
"usr" # => "usr_p5w9-z27j"
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
##### clear_encoded_id_cache!
|
|
98
|
+
Manually clear memoized encoded ID values. Called automatically on `reload` and `dup`.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
user.clear_encoded_id_cache!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Class Methods
|
|
105
|
+
|
|
106
|
+
##### find_by_encoded_id(encoded_id)
|
|
107
|
+
Find record by encoded ID (returns nil if not found).
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
User.find_by_encoded_id("user_p5w9-z27j") # With annotation
|
|
111
|
+
User.find_by_encoded_id("p5w9-z27j") # Just the hash
|
|
112
|
+
User.find_by_encoded_id("john-doe--user_p5w9-z27j") # Slugged
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
##### find_by_encoded_id!(encoded_id)
|
|
116
|
+
Same as above but raises `ActiveRecord::RecordNotFound` if not found.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
User.find_by_encoded_id!("user_p5w9-z27j")
|
|
120
|
+
# Raises ActiveRecord::RecordNotFound if not found
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
##### find_all_by_encoded_id(encoded_id)
|
|
124
|
+
Find multiple records when encoded ID contains multiple IDs (returns nil if none found).
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# If encoded ID represents [78, 45]
|
|
128
|
+
User.find_all_by_encoded_id("z2j7-0dmw")
|
|
129
|
+
# => [#<User id: 78>, #<User id: 45>]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
##### find_all_by_encoded_id!(encoded_id)
|
|
133
|
+
Same as above but raises `ActiveRecord::RecordNotFound` if:
|
|
134
|
+
- No records found
|
|
135
|
+
- Number of records doesn't match number of decoded IDs
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
User.find_all_by_encoded_id!("z2j7-0dmw")
|
|
139
|
+
# Raises if records not found or count mismatch
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
##### where_encoded_id(*encoded_ids)
|
|
143
|
+
Returns ActiveRecord relation for chaining. Can take multiple IDs.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
User.where_encoded_id("user_p5w9-z27j").where(active: true)
|
|
147
|
+
User.where_encoded_id("id1", "id2", "id3")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
##### encode_encoded_id(id, **options)
|
|
151
|
+
Encode a specific ID using model's configuration (optionally override options).
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
User.encode_encoded_id(123) # => "p5w9-z27j"
|
|
155
|
+
User.encode_encoded_id(123, id_length: 12) # Override length
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
##### decode_encoded_id(encoded_id)
|
|
159
|
+
Decode an encoded ID using model's configuration.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
User.decode_encoded_id("user_p5w9-z27j") # => [123]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### EncodedId::Rails::PathParam
|
|
166
|
+
|
|
167
|
+
Makes models use encoded IDs in URL helpers.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class User < ApplicationRecord
|
|
171
|
+
include EncodedId::Rails::Model
|
|
172
|
+
include EncodedId::Rails::PathParam
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
user.to_param # => "user_p5w9-z27j"
|
|
176
|
+
|
|
177
|
+
# In routes
|
|
178
|
+
link_to "View", user_path(user) # => "/users/user_p5w9-z27j"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### EncodedId::Rails::SluggedPathParam
|
|
182
|
+
|
|
183
|
+
Uses slugged encoded IDs in URLs.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
class User < ApplicationRecord
|
|
187
|
+
include EncodedId::Rails::Model
|
|
188
|
+
include EncodedId::Rails::SluggedPathParam
|
|
189
|
+
|
|
190
|
+
def name_for_encoded_id_slug
|
|
191
|
+
full_name.parameterize
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
user.to_param # => "john-doe--user_p5w9-z27j"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### EncodedId::Rails::ActiveRecordFinders
|
|
199
|
+
|
|
200
|
+
Overrides standard ActiveRecord finders to handle encoded IDs transparently.
|
|
201
|
+
|
|
202
|
+
**IMPORTANT**: Only use with integer primary keys. Do NOT use with string-based primary keys (UUIDs).
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
class Product < ApplicationRecord
|
|
206
|
+
include EncodedId::Rails::Model
|
|
207
|
+
include EncodedId::Rails::ActiveRecordFinders
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Now these all work with encoded IDs
|
|
211
|
+
Product.find("product_p5w9-z27j")
|
|
212
|
+
Product.find_by_id("product_p5w9-z27j")
|
|
213
|
+
Product.where(id: "product_p5w9-z27j")
|
|
214
|
+
|
|
215
|
+
# Still works with regular IDs
|
|
216
|
+
Product.find(123)
|
|
217
|
+
|
|
218
|
+
# In controllers, no changes needed
|
|
219
|
+
def show
|
|
220
|
+
@product = Product.find(params[:id]) # Works with both
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### EncodedId::Rails::Persists
|
|
225
|
+
|
|
226
|
+
Stores encoded IDs in database for performance with automatic validations.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Generate migration
|
|
230
|
+
rails generate encoded_id:rails:add_columns User
|
|
231
|
+
|
|
232
|
+
# Creates migration adding:
|
|
233
|
+
# - normalized_encoded_id (string) - for lookups without separators/annotations
|
|
234
|
+
# - prefixed_encoded_id (string) - with annotation prefix
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class User < ApplicationRecord
|
|
239
|
+
include EncodedId::Rails::Model
|
|
240
|
+
include EncodedId::Rails::Persists
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Automatic Validations**:
|
|
245
|
+
- Uniqueness validation on `normalized_encoded_id`
|
|
246
|
+
- Uniqueness validation on `prefixed_encoded_id`
|
|
247
|
+
- Read-only enforcement after creation (prevents manual updates)
|
|
248
|
+
|
|
249
|
+
**Callbacks**:
|
|
250
|
+
- `after_create`: Automatically sets encoded IDs
|
|
251
|
+
- `before_save`: Updates encoded IDs if ID changed
|
|
252
|
+
- `after_commit`: Validates persisted values match computed values
|
|
253
|
+
|
|
254
|
+
**Instance Methods**:
|
|
255
|
+
|
|
256
|
+
##### set_normalized_encoded_id!
|
|
257
|
+
Manually update persisted encoded IDs (uses `update_columns` to bypass callbacks).
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
user.set_normalized_encoded_id!
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
##### update_normalized_encoded_id!
|
|
264
|
+
Update persisted encoded IDs in-memory (will be saved with record).
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
user.update_normalized_encoded_id!
|
|
268
|
+
user.save
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Database Lookups**:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# Fast lookups via direct DB query
|
|
275
|
+
User.where(normalized_encoded_id: "p5w9z27j").first
|
|
276
|
+
User.where(prefixed_encoded_id: "user_p5w9-z27j").first
|
|
277
|
+
|
|
278
|
+
# IMPORTANT: Add indexes for performance
|
|
279
|
+
# See migration section below
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Record Duplication**:
|
|
283
|
+
When using `dup`, persisted encoded ID columns are automatically set to `nil` for the new record:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
new_user = existing_user.dup
|
|
287
|
+
new_user.normalized_encoded_id # => nil (will be set on save)
|
|
288
|
+
new_user.prefixed_encoded_id # => nil
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
### Global Configuration
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# config/initializers/encoded_id.rb
|
|
297
|
+
EncodedId::Rails.configure do |config|
|
|
298
|
+
# Required for Hashids encoder
|
|
299
|
+
config.salt = "your-secret-salt" # Not required for Sqids
|
|
300
|
+
|
|
301
|
+
# Basic Options
|
|
302
|
+
config.id_length = 8 # Minimum length
|
|
303
|
+
config.character_group_size = 4 # Split every X chars
|
|
304
|
+
config.group_separator = "-" # Split character
|
|
305
|
+
config.alphabet = EncodedId::Alphabet.modified_crockford
|
|
306
|
+
|
|
307
|
+
# Annotation/Prefix Options
|
|
308
|
+
config.annotation_method_name = :annotation_for_encoded_id
|
|
309
|
+
config.annotated_id_separator = "_"
|
|
310
|
+
|
|
311
|
+
# Slug Options
|
|
312
|
+
config.slug_value_method_name = :name_for_encoded_id_slug
|
|
313
|
+
config.slugged_id_separator = "--"
|
|
314
|
+
|
|
315
|
+
# Encoder Selection
|
|
316
|
+
config.encoder = :sqids # Default: :sqids (or :hashids for backwards compatibility)
|
|
317
|
+
config.downcase_on_decode = false # Default: false (set to true for pre-v1 compatibility)
|
|
318
|
+
|
|
319
|
+
# Blocklist
|
|
320
|
+
config.blocklist = nil # EncodedId::Blocklist.minimal or custom
|
|
321
|
+
|
|
322
|
+
# Auto-include PathParam (makes all models use encoded IDs in URLs)
|
|
323
|
+
config.model_to_param_returns_encoded_id = false
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Note**: As of v1.0.0:
|
|
328
|
+
- Default encoder is `:sqids` (no salt required)
|
|
329
|
+
- `downcase_on_decode` defaults to `false` (case-sensitive)
|
|
330
|
+
- For backwards compatibility with pre-v1: set `encoder: :hashids` and `downcase_on_decode: true`
|
|
331
|
+
|
|
332
|
+
### Encoder-Specific Notes
|
|
333
|
+
|
|
334
|
+
**Sqids (default)**:
|
|
335
|
+
- No salt required
|
|
336
|
+
- Automatically avoids blocklisted words via iteration
|
|
337
|
+
- Faster decoding
|
|
338
|
+
|
|
339
|
+
**Hashids**:
|
|
340
|
+
- Requires salt (minimum 4 characters)
|
|
341
|
+
- Raises `EncodedId::BlocklistError` if blocklisted word appears
|
|
342
|
+
- Faster encoding, especially with blocklists
|
|
343
|
+
|
|
344
|
+
### Per-Model Configuration
|
|
345
|
+
|
|
346
|
+
#### Using `encoded_id_config` (Recommended)
|
|
347
|
+
|
|
348
|
+
The cleanest way to configure encoding options per model:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
class User < ApplicationRecord
|
|
352
|
+
include EncodedId::Rails::Model
|
|
353
|
+
|
|
354
|
+
# Configure encoder settings for this model
|
|
355
|
+
encoded_id_config encoder: :hashids, id_length: 12
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Supports all configuration options**:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
class Product < ApplicationRecord
|
|
363
|
+
include EncodedId::Rails::Model
|
|
364
|
+
|
|
365
|
+
encoded_id_config(
|
|
366
|
+
encoder: :sqids,
|
|
367
|
+
id_length: 12,
|
|
368
|
+
character_group_size: 3,
|
|
369
|
+
alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
|
|
370
|
+
blocklist: EncodedId::Blocklist.minimal,
|
|
371
|
+
downcase_on_decode: true
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Available Options**:
|
|
377
|
+
- `encoder` - `:sqids` or `:hashids`
|
|
378
|
+
- `id_length` - Minimum encoded ID length
|
|
379
|
+
- `character_group_size` - Character grouping (nil for no grouping)
|
|
380
|
+
- `alphabet` - Custom alphabet
|
|
381
|
+
- `blocklist` - Blocklist instance or array
|
|
382
|
+
- `downcase_on_decode` - Case-insensitive decoding
|
|
383
|
+
- `annotation_method_name` - Method to call for annotation
|
|
384
|
+
- `annotated_id_separator` - Separator for annotated IDs
|
|
385
|
+
- `slug_value_method_name` - Method to call for slug
|
|
386
|
+
- `slugged_id_separator` - Separator for slugged IDs
|
|
387
|
+
|
|
388
|
+
**Note**: For advanced blocklist control (modes like `:length_threshold`, `:always`, `:raise_if_likely`), use the base `EncodedId::ReversibleId` directly or configure via custom coder.
|
|
389
|
+
|
|
390
|
+
**Configuration Inheritance**: Child classes inherit their parent's configuration:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
class BaseModel < ApplicationRecord
|
|
394
|
+
self.abstract_class = true
|
|
395
|
+
include EncodedId::Rails::Model
|
|
396
|
+
|
|
397
|
+
# All child models inherit these settings
|
|
398
|
+
encoded_id_config encoder: :hashids, id_length: 10
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
class User < BaseModel
|
|
402
|
+
# Inherits encoder: :hashids, id_length: 10
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
class Product < BaseModel
|
|
406
|
+
# Override parent settings
|
|
407
|
+
encoded_id_config encoder: :sqids
|
|
408
|
+
# Now uses encoder: :sqids but still id_length: 10
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### Custom Salt Per Model
|
|
413
|
+
|
|
414
|
+
Override salt per model (Hashids only):
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
class User < ApplicationRecord
|
|
418
|
+
include EncodedId::Rails::Model
|
|
419
|
+
|
|
420
|
+
def self.encoded_id_salt
|
|
421
|
+
"user-specific-salt"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Contextual Encoding
|
|
427
|
+
|
|
428
|
+
Different configurations for different use cases:
|
|
429
|
+
|
|
430
|
+
```ruby
|
|
431
|
+
class User < ApplicationRecord
|
|
432
|
+
include EncodedId::Rails::Model
|
|
433
|
+
|
|
434
|
+
# Short ID for QR codes
|
|
435
|
+
def qr_encoded_id
|
|
436
|
+
self.class.encode_encoded_id(id,
|
|
437
|
+
id_length: 6,
|
|
438
|
+
character_group_size: nil
|
|
439
|
+
)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# API-friendly (no separators/annotations)
|
|
443
|
+
def api_encoded_id
|
|
444
|
+
self.class.encode_encoded_id(id,
|
|
445
|
+
character_group_size: nil,
|
|
446
|
+
annotation_method_name: nil
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Routes & Controllers
|
|
453
|
+
|
|
454
|
+
### Basic Setup
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
# routes.rb
|
|
458
|
+
Rails.application.routes.draw do
|
|
459
|
+
resources :users, param: :encoded_id
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# UsersController
|
|
463
|
+
class UsersController < ApplicationController
|
|
464
|
+
def show
|
|
465
|
+
@user = User.find_by_encoded_id!(params[:encoded_id])
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### With ActiveRecordFinders
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# routes.rb - standard :id param
|
|
474
|
+
resources :products
|
|
475
|
+
|
|
476
|
+
# ProductsController
|
|
477
|
+
class ProductsController < ApplicationController
|
|
478
|
+
def show
|
|
479
|
+
# Works with both regular and encoded IDs
|
|
480
|
+
@product = Product.find(params[:id])
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### With Slugged IDs
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
# routes.rb
|
|
489
|
+
resources :articles
|
|
490
|
+
|
|
491
|
+
# ArticlesController
|
|
492
|
+
class ArticlesController < ApplicationController
|
|
493
|
+
def show
|
|
494
|
+
# Handles slugged IDs automatically
|
|
495
|
+
@article = Article.find_by_encoded_id!(params[:id])
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Common Patterns
|
|
501
|
+
|
|
502
|
+
### Complete Integration Example
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
class Product < ApplicationRecord
|
|
506
|
+
include EncodedId::Rails::Model
|
|
507
|
+
include EncodedId::Rails::SluggedPathParam
|
|
508
|
+
include EncodedId::Rails::Persists
|
|
509
|
+
include EncodedId::Rails::ActiveRecordFinders
|
|
510
|
+
|
|
511
|
+
# Configure encoding options for this model
|
|
512
|
+
encoded_id_config(
|
|
513
|
+
blocklist: EncodedId::Blocklist.minimal,
|
|
514
|
+
id_length: 10
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def name_for_encoded_id_slug
|
|
518
|
+
name.parameterize
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Usage
|
|
523
|
+
product = Product.create(name: "Cool Gadget")
|
|
524
|
+
product.encoded_id # => "product_k6jR8Myo23"
|
|
525
|
+
product.slugged_encoded_id # => "cool-gadget--product_k6jR8Myo23"
|
|
526
|
+
|
|
527
|
+
# All these work
|
|
528
|
+
Product.find("product_k6jR8Myo23")
|
|
529
|
+
Product.find("cool-gadget--product_k6jR8Myo23")
|
|
530
|
+
Product.find_by_encoded_id("k6jR8Myo23")
|
|
531
|
+
Product.where_encoded_id("product_k6jR8Myo23").active
|
|
532
|
+
|
|
533
|
+
# URLs automatically use slugged IDs
|
|
534
|
+
product_path(product) # => "/products/cool-gadget--product_k6jR8Myo23"
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Migration for Existing Data
|
|
538
|
+
|
|
539
|
+
When adding `Persists` module to existing models:
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
# 1. Generate and run migration
|
|
543
|
+
rails generate encoded_id:rails:add_columns User
|
|
544
|
+
# Edit migration to add indexes (see below)
|
|
545
|
+
rails db:migrate
|
|
546
|
+
|
|
547
|
+
# 2. Backfill existing records
|
|
548
|
+
User.find_each(batch_size: 1000) do |user|
|
|
549
|
+
user.set_normalized_encoded_id!
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Or via background job
|
|
553
|
+
class BackfillEncodedIdsJob < ApplicationJob
|
|
554
|
+
def perform(model_class, start_id, end_id)
|
|
555
|
+
model_class.where(id: start_id..end_id).find_each do |record|
|
|
556
|
+
record.set_normalized_encoded_id!
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Enhanced Migration with Indexes** (recommended):
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
class AddEncodedIdColumnsToUsers < ActiveRecord::Migration[7.0]
|
|
566
|
+
def change
|
|
567
|
+
add_column :users, :normalized_encoded_id, :string
|
|
568
|
+
add_column :users, :prefixed_encoded_id, :string
|
|
569
|
+
|
|
570
|
+
# Add indexes for performance (critical for lookups)
|
|
571
|
+
add_index :users, :normalized_encoded_id, unique: true
|
|
572
|
+
add_index :users, :prefixed_encoded_id, unique: true
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Accessing Configuration
|
|
578
|
+
|
|
579
|
+
```ruby
|
|
580
|
+
# Access global configuration
|
|
581
|
+
EncodedId::Rails.configuration.salt
|
|
582
|
+
EncodedId::Rails.configuration.encoder # => :sqids
|
|
583
|
+
EncodedId::Rails.configuration.id_length # => 8
|
|
584
|
+
|
|
585
|
+
# Access model-specific configuration
|
|
586
|
+
User.encoded_id_salt
|
|
587
|
+
User.encoded_id_coder.encoder
|
|
588
|
+
User.encoded_id_options # => Hash of configured options
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## Performance Considerations
|
|
592
|
+
|
|
593
|
+
1. **Persistence**: Use `EncodedId::Rails::Persists` for high-traffic lookups
|
|
594
|
+
2. **Indexes**: ALWAYS add database indexes on `normalized_encoded_id` and `prefixed_encoded_id`
|
|
595
|
+
3. **Caching**: Encoded IDs are deterministic and memoized per record - cache them if needed
|
|
596
|
+
4. **Blocklists**: Large blocklists impact encoding performance, especially with Sqids (iterates to avoid words)
|
|
597
|
+
5. **Blocklist Modes**:
|
|
598
|
+
- Sqids: Iteratively regenerates to avoid blocklisted words (may impact performance)
|
|
599
|
+
- Hashids: Raises exception if blocklisted word detected (requires retry logic)
|
|
600
|
+
- For advanced control, use base `EncodedId` configuration directly
|
|
601
|
+
|
|
602
|
+
## Best Practices
|
|
603
|
+
|
|
604
|
+
1. **Consistent Configuration**: Don't change salt/encoder after going to production
|
|
605
|
+
2. **Model Naming**: Use clear annotation prefixes to identify model types
|
|
606
|
+
3. **Error Handling**: Always use `find_by_encoded_id!` in controllers for proper 404s
|
|
607
|
+
4. **URL Design**: Choose between encoded IDs vs slugged IDs based on UX needs
|
|
608
|
+
5. **Testing**: Test with both regular IDs and encoded IDs in your specs
|
|
609
|
+
6. **Indexes**: Always add database indexes when using `Persists` module
|
|
610
|
+
7. **Validations**: Rely on automatic validations from `Persists` - don't manually update columns
|
|
611
|
+
8. **Record Duplication**: `dup` automatically clears encoded IDs - persisted IDs set to nil for new records
|
|
612
|
+
|
|
613
|
+
## Debugging
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
# Check configuration
|
|
617
|
+
user = User.first
|
|
618
|
+
user.class.encoded_id_salt
|
|
619
|
+
user.class.encoded_id_coder.encoder
|
|
620
|
+
user.class.encoded_id_options
|
|
621
|
+
|
|
622
|
+
# Test encoding/decoding
|
|
623
|
+
encoded = User.encode_encoded_id(123)
|
|
624
|
+
decoded = User.decode_encoded_id(encoded)
|
|
625
|
+
|
|
626
|
+
# Inspect persisted values (if using Persists)
|
|
627
|
+
user.normalized_encoded_id
|
|
628
|
+
user.prefixed_encoded_id
|
|
629
|
+
|
|
630
|
+
# Clear and regenerate encoded IDs
|
|
631
|
+
user.clear_encoded_id_cache!
|
|
632
|
+
user.encoded_id # Regenerates
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Security Considerations
|
|
636
|
+
|
|
637
|
+
- Encoded IDs are obfuscated, NOT encrypted
|
|
638
|
+
- Don't rely on them for authentication or authorization
|
|
639
|
+
- They help prevent enumeration attacks but aren't cryptographically secure
|
|
640
|
+
- Always validate decoded IDs before database operations
|
|
641
|
+
- Use `find_by_encoded_id!` to ensure proper error handling
|
|
642
|
+
|
|
643
|
+
## Example Use Cases
|
|
644
|
+
|
|
645
|
+
1. **Public-Facing IDs**: Hide sequential database IDs from users
|
|
646
|
+
2. **SEO-Friendly URLs**: Combine slugs with encoded IDs (`cool-gadget--product_k6j8`)
|
|
647
|
+
3. **API Design**: Provide opaque identifiers that don't leak information
|
|
648
|
+
4. **Multi-Tenant Apps**: Use different salts per tenant for isolation
|
|
649
|
+
5. **Legacy Migration**: Gradually move from numeric to encoded IDs
|
|
650
|
+
6. **Referral Codes**: Encode user IDs into shareable links
|
|
651
|
+
7. **Soft Launches**: Hide actual user/item counts from competitors
|