encoded_id-rails 1.0.0.rc6 → 1.0.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.
@@ -11,9 +11,20 @@
11
11
  - **Slugged IDs**: Human-readable slugs combined with encoded IDs (e.g., `john-doe--user_p5w9-z27j`)
12
12
  - **Annotated IDs**: Model type prefixes for clarity (e.g., `user_p5w9-z27j`)
13
13
  - **Finder Methods**: Find records using encoded IDs with familiar ActiveRecord syntax
14
- - **Database Persistence**: Optional storage of encoded IDs for performance
15
- - **Per-Model Configuration**: Different encoding strategies per model
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
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!` |
17
28
 
18
29
  ## Installation & Setup
19
30
 
@@ -24,11 +35,18 @@ gem 'encoded_id-rails'
24
35
  # Install
25
36
  bundle install
26
37
 
27
- # Generate configuration
38
+ # Generate configuration (prompts for encoder choice: sqids or hashids)
28
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
29
45
  ```
30
46
 
31
- This creates `config/initializers/encoded_id.rb` with configuration options.
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)
32
50
 
33
51
  ## Core Modules
34
52
 
@@ -58,7 +76,7 @@ Returns encoded ID with human-readable slug.
58
76
  ```ruby
59
77
  class User < ApplicationRecord
60
78
  include EncodedId::Rails::Model
61
-
79
+
62
80
  def name_for_encoded_id_slug
63
81
  full_name.parameterize
64
82
  end
@@ -68,7 +86,7 @@ user.slugged_encoded_id # => "john-doe--user_p5w9-z27j"
68
86
  ```
69
87
 
70
88
  ##### annotation_for_encoded_id
71
- Override to customize the annotation prefix.
89
+ Override to customize the annotation prefix (defaults to `model_name.underscore`).
72
90
 
73
91
  ```ruby
74
92
  def annotation_for_encoded_id
@@ -76,6 +94,13 @@ def annotation_for_encoded_id
76
94
  end
77
95
  ```
78
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
+
79
104
  #### Class Methods
80
105
 
81
106
  ##### find_by_encoded_id(encoded_id)
@@ -90,8 +115,13 @@ User.find_by_encoded_id("john-doe--user_p5w9-z27j") # Slugged
90
115
  ##### find_by_encoded_id!(encoded_id)
91
116
  Same as above but raises `ActiveRecord::RecordNotFound` if not found.
92
117
 
118
+ ```ruby
119
+ User.find_by_encoded_id!("user_p5w9-z27j")
120
+ # Raises ActiveRecord::RecordNotFound if not found
121
+ ```
122
+
93
123
  ##### find_all_by_encoded_id(encoded_id)
94
- Find multiple records when encoded ID contains multiple IDs.
124
+ Find multiple records when encoded ID contains multiple IDs (returns nil if none found).
95
125
 
96
126
  ```ruby
97
127
  # If encoded ID represents [78, 45]
@@ -99,18 +129,37 @@ User.find_all_by_encoded_id("z2j7-0dmw")
99
129
  # => [#<User id: 78>, #<User id: 45>]
100
130
  ```
101
131
 
102
- ##### where_encoded_id(encoded_id)
103
- Returns ActiveRecord relation for chaining.
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.
104
144
 
105
145
  ```ruby
106
146
  User.where_encoded_id("user_p5w9-z27j").where(active: true)
147
+ User.where_encoded_id("id1", "id2", "id3")
107
148
  ```
108
149
 
109
- ##### encode_encoded_id(id)
110
- Encode a specific ID using model's configuration.
150
+ ##### encode_encoded_id(id, **options)
151
+ Encode a specific ID using model's configuration (optionally override options).
111
152
 
112
153
  ```ruby
113
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]
114
163
  ```
115
164
 
116
165
  ### EncodedId::Rails::PathParam
@@ -137,7 +186,7 @@ Uses slugged encoded IDs in URLs.
137
186
  class User < ApplicationRecord
138
187
  include EncodedId::Rails::Model
139
188
  include EncodedId::Rails::SluggedPathParam
140
-
189
+
141
190
  def name_for_encoded_id_slug
142
191
  full_name.parameterize
143
192
  end
@@ -150,6 +199,8 @@ user.to_param # => "john-doe--user_p5w9-z27j"
150
199
 
151
200
  Overrides standard ActiveRecord finders to handle encoded IDs transparently.
152
201
 
202
+ **IMPORTANT**: Only use with integer primary keys. Do NOT use with string-based primary keys (UUIDs).
203
+
153
204
  ```ruby
154
205
  class Product < ApplicationRecord
155
206
  include EncodedId::Rails::Model
@@ -170,19 +221,17 @@ def show
170
221
  end
171
222
  ```
172
223
 
173
- **Warning**: Do NOT use with string-based primary keys (UUIDs).
174
-
175
224
  ### EncodedId::Rails::Persists
176
225
 
177
- Stores encoded IDs in database for performance.
226
+ Stores encoded IDs in database for performance with automatic validations.
178
227
 
179
228
  ```bash
180
229
  # Generate migration
181
230
  rails generate encoded_id:rails:add_columns User
182
231
 
183
- # Adds columns:
184
- # - normalized_encoded_id (string)
185
- # - prefixed_encoded_id (string)
232
+ # Creates migration adding:
233
+ # - normalized_encoded_id (string) - for lookups without separators/annotations
234
+ # - prefixed_encoded_id (string) - with annotation prefix
186
235
  ```
187
236
 
188
237
  ```ruby
@@ -190,12 +239,53 @@ class User < ApplicationRecord
190
239
  include EncodedId::Rails::Model
191
240
  include EncodedId::Rails::Persists
192
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**:
193
272
 
273
+ ```ruby
194
274
  # Fast lookups via direct DB query
195
275
  User.where(normalized_encoded_id: "p5w9z27j").first
276
+ User.where(prefixed_encoded_id: "user_p5w9-z27j").first
196
277
 
197
- # Add index for performance
198
- add_index :users, :normalized_encoded_id, unique: true
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
199
289
  ```
200
290
 
201
291
  ## Configuration
@@ -205,56 +295,130 @@ add_index :users, :normalized_encoded_id, unique: true
205
295
  ```ruby
206
296
  # config/initializers/encoded_id.rb
207
297
  EncodedId::Rails.configure do |config|
208
- # Required
209
- config.salt = "your-secret-salt"
210
-
211
- # Optional
298
+ # Required for Hashids encoder
299
+ config.salt = "your-secret-salt" # Not required for Sqids
300
+
301
+ # Basic Options
212
302
  config.id_length = 8 # Minimum length
213
303
  config.character_group_size = 4 # Split every X chars
214
304
  config.group_separator = "-" # Split character
215
305
  config.alphabet = EncodedId::Alphabet.modified_crockford
306
+
307
+ # Annotation/Prefix Options
216
308
  config.annotation_method_name = :annotation_for_encoded_id
217
309
  config.annotated_id_separator = "_"
310
+
311
+ # Slug Options
218
312
  config.slug_value_method_name = :name_for_encoded_id_slug
219
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)
220
323
  config.model_to_param_returns_encoded_id = false
221
- config.encoder = :hashids # or :sqids
222
- config.blocklist = nil
223
- config.hex_digit_encoding_group_size = 4
224
324
  end
225
325
  ```
226
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
+
227
344
  ### Per-Model Configuration
228
345
 
229
- Override salt per model:
346
+ #### Using `encoded_id_config` (Recommended)
347
+
348
+ The cleanest way to configure encoding options per model:
230
349
 
231
350
  ```ruby
232
351
  class User < ApplicationRecord
233
352
  include EncodedId::Rails::Model
234
-
235
- def self.encoded_id_salt
236
- "user-specific-salt"
237
- end
353
+
354
+ # Configure encoder settings for this model
355
+ encoded_id_config encoder: :hashids, id_length: 12
238
356
  end
239
357
  ```
240
358
 
241
- ### Advanced Model Configuration
242
-
243
- Full control via `encoded_id_coder`:
359
+ **Supports all configuration options**:
244
360
 
245
361
  ```ruby
246
362
  class Product < ApplicationRecord
247
363
  include EncodedId::Rails::Model
248
-
249
- def self.encoded_id_coder(options = {})
250
- super(options.merge(
251
- encoder: :sqids,
252
- id_length: 12,
253
- character_group_size: 3,
254
- group_separator: ".",
255
- alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
256
- blocklist: ["BAD", "FAKE"]
257
- ))
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"
258
422
  end
259
423
  end
260
424
  ```
@@ -266,18 +430,18 @@ Different configurations for different use cases:
266
430
  ```ruby
267
431
  class User < ApplicationRecord
268
432
  include EncodedId::Rails::Model
269
-
433
+
270
434
  # Short ID for QR codes
271
435
  def qr_encoded_id
272
- self.class.encode_encoded_id(id,
273
- id_length: 6,
436
+ self.class.encode_encoded_id(id,
437
+ id_length: 6,
274
438
  character_group_size: nil
275
439
  )
276
440
  end
277
-
441
+
278
442
  # API-friendly (no separators/annotations)
279
443
  def api_encoded_id
280
- self.class.encode_encoded_id(id,
444
+ self.class.encode_encoded_id(id,
281
445
  character_group_size: nil,
282
446
  annotation_method_name: nil
283
447
  )
@@ -318,6 +482,21 @@ class ProductsController < ApplicationController
318
482
  end
319
483
  ```
320
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
+
321
500
  ## Common Patterns
322
501
 
323
502
  ### Complete Integration Example
@@ -328,18 +507,16 @@ class Product < ApplicationRecord
328
507
  include EncodedId::Rails::SluggedPathParam
329
508
  include EncodedId::Rails::Persists
330
509
  include EncodedId::Rails::ActiveRecordFinders
331
-
510
+
511
+ # Configure encoding options for this model
512
+ encoded_id_config(
513
+ blocklist: EncodedId::Blocklist.minimal,
514
+ id_length: 10
515
+ )
516
+
332
517
  def name_for_encoded_id_slug
333
518
  name.parameterize
334
519
  end
335
-
336
- def self.encoded_id_coder(options = {})
337
- super(options.merge(
338
- encoder: :sqids,
339
- blocklist: ["offensive", "words"],
340
- id_length: 10
341
- ))
342
- end
343
520
  end
344
521
 
345
522
  # Usage
@@ -359,8 +536,15 @@ product_path(product) # => "/products/cool-gadget--product_k6jR8Myo23"
359
536
 
360
537
  ### Migration for Existing Data
361
538
 
539
+ When adding `Persists` module to existing models:
540
+
362
541
  ```ruby
363
- # For persisted encoded IDs
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
364
548
  User.find_each(batch_size: 1000) do |user|
365
549
  user.set_normalized_encoded_id!
366
550
  end
@@ -375,12 +559,45 @@ class BackfillEncodedIdsJob < ApplicationJob
375
559
  end
376
560
  ```
377
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
+
378
591
  ## Performance Considerations
379
592
 
380
593
  1. **Persistence**: Use `EncodedId::Rails::Persists` for high-traffic lookups
381
- 2. **Indexes**: Add database indexes on `normalized_encoded_id`
382
- 3. **Caching**: Encoded IDs are deterministic - cache them if needed
383
- 4. **Blocklists**: Large blocklists impact performance, especially with Sqids
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
384
601
 
385
602
  ## Best Practices
386
603
 
@@ -389,32 +606,30 @@ end
389
606
  3. **Error Handling**: Always use `find_by_encoded_id!` in controllers for proper 404s
390
607
  4. **URL Design**: Choose between encoded IDs vs slugged IDs based on UX needs
391
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
392
612
 
393
- ## Troubleshooting
394
-
395
- ### Load Order Issues
396
-
397
- ```ruby
398
- # In initializer if seeing load errors
399
- require 'encoded_id'
400
- require 'encoded_id/rails'
401
-
402
- # Or in ApplicationRecord
403
- require 'encoded_id/rails/model'
404
- require 'encoded_id/rails/path_param'
405
- ```
406
-
407
- ### Debugging
613
+ ## Debugging
408
614
 
409
615
  ```ruby
410
616
  # Check configuration
411
617
  user = User.first
412
618
  user.class.encoded_id_salt
413
619
  user.class.encoded_id_coder.encoder
620
+ user.class.encoded_id_options
414
621
 
415
622
  # Test encoding/decoding
416
623
  encoded = User.encode_encoded_id(123)
417
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
418
633
  ```
419
634
 
420
635
  ## Security Considerations
@@ -423,11 +638,14 @@ decoded = User.decode_encoded_id(encoded)
423
638
  - Don't rely on them for authentication or authorization
424
639
  - They help prevent enumeration attacks but aren't cryptographically secure
425
640
  - Always validate decoded IDs before database operations
641
+ - Use `find_by_encoded_id!` to ensure proper error handling
426
642
 
427
643
  ## Example Use Cases
428
644
 
429
645
  1. **Public-Facing IDs**: Hide sequential database IDs from users
430
- 2. **SEO-Friendly URLs**: Combine slugs with encoded IDs for best of both worlds
646
+ 2. **SEO-Friendly URLs**: Combine slugs with encoded IDs (`cool-gadget--product_k6j8`)
431
647
  3. **API Design**: Provide opaque identifiers that don't leak information
432
648
  4. **Multi-Tenant Apps**: Use different salts per tenant for isolation
433
- 5. **Legacy Migration**: Gradually move from numeric to encoded IDs
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