Skip to content

Commit d79c457

Browse files
feat: use custom migration template with backfill for adding webauthn_id column
Replaces invoke of `active_record:migration` with a migration template that includes the `webauthn_id` column and backfill for existing records.
1 parent 9e0300d commit d79c457

3 files changed

Lines changed: 56 additions & 12 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
class AddWebauthnIdTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= Rails.version.to_f %>]
4+
def up
5+
add_column :<%= table_name %>, :webauthn_id, :string
6+
add_index :<%= table_name %>, :webauthn_id, unique: true
7+
8+
# WARNING: The code below backfills webauthn_id for all existing records
9+
# one row at a time. For larger tables, consider removing it and running
10+
# the backfill separately (e.g., in a background job or maintenance task).
11+
#
12+
# Worth noting: PostgreSQL and MySQL support single-query backfills:
13+
#
14+
# PostgreSQL:
15+
# UPDATE <%= table_name %> SET webauthn_id = encode(gen_random_bytes(64), 'base64') WHERE webauthn_id IS NULL
16+
#
17+
# MySQL:
18+
# UPDATE <%= table_name %> SET webauthn_id = TO_BASE64(RANDOM_BYTES(64)) WHERE webauthn_id IS NULL
19+
#
20+
execute("SELECT id FROM <%= table_name %> WHERE webauthn_id IS NULL").each do |row|
21+
webauthn_id = WebAuthn.generate_user_id
22+
execute("UPDATE <%= table_name %> SET webauthn_id = #{connection.quote(webauthn_id)} WHERE id = #{row['id']}")
23+
end
24+
end
25+
26+
def down
27+
remove_column :<%= table_name %>, :webauthn_id
28+
end
29+
end

lib/generators/devise/webauthn/webauthn_id/webauthn_id_generator.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
module Devise
77
module Webauthn
88
class WebauthnIdGenerator < Rails::Generators::Base
9+
include Rails::Generators::Migration
10+
911
hide!
1012
namespace "devise:webauthn:webauthn_id"
1113

14+
source_root File.expand_path("templates", __dir__)
15+
1216
desc "Add webauthn_id field to User model"
1317
class_option :resource_name, type: :string, default: "user", desc: "The resource name for Devise (default: user)"
1418

19+
def self.next_migration_number(dirname)
20+
ActiveRecord::Generators::Base.next_migration_number(dirname)
21+
end
22+
1523
def generate_migration
16-
invoke "active_record:migration", [
17-
"add_webauthn_id_to_#{user_table_name}",
18-
"webauthn_id:string:uniq"
19-
]
24+
migration_template "add_webauthn_id.rb.erb", "db/migrate/add_webauthn_id_to_#{table_name}.rb"
2025
end
2126

2227
def show_instructions
@@ -33,7 +38,7 @@ def show_instructions
3338

3439
private
3540

36-
def user_table_name
41+
def table_name
3742
options[:resource_name].pluralize.underscore
3843
end
3944
end

spec/generators/devise/webauthn/webauthn_id_generator_spec.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,35 @@
99

1010
before do
1111
prepare_destination
12-
allow(generator_instance).to receive(:invoke)
1312
invoke generator_instance
1413
end
1514

1615
context "when using default resource name" do
1716
let(:generator_instance) { generator }
1817

19-
it "invokes the active_record:migration generator with correct arguments" do
20-
expect(generator).to have_received(:invoke).with("active_record:migration",
21-
["add_webauthn_id_to_users", "webauthn_id:string:uniq"])
18+
it "creates a migration that adds webauthn_id to users" do
19+
assert_migration "db/migrate/add_webauthn_id_to_users.rb" do |migration|
20+
assert_match(/add_column :users, :webauthn_id, :string/, migration)
21+
assert_match(/add_index :users, :webauthn_id, unique: true/, migration)
22+
end
23+
end
24+
25+
it "creates a migration that backfills existing records" do
26+
assert_migration "db/migrate/add_webauthn_id_to_users.rb" do |migration|
27+
assert_match(/SELECT id FROM users WHERE webauthn_id IS NULL/, migration)
28+
assert_match(/WebAuthn\.generate_user_id/, migration)
29+
end
2230
end
2331
end
2432

2533
context "when using a custom resource name" do
2634
let(:generator_instance) { generator([destination_root], ["--resource_name=admin"]) }
2735

28-
it "invokes the active_record:migration generator with correct arguments" do
29-
expect(generator).to have_received(:invoke).with("active_record:migration",
30-
["add_webauthn_id_to_admins", "webauthn_id:string:uniq"])
36+
it "creates a migration that adds webauthn_id to admins" do
37+
assert_migration "db/migrate/add_webauthn_id_to_admins.rb" do |migration|
38+
assert_match(/add_column :admins, :webauthn_id, :string/, migration)
39+
assert_match(/add_index :admins, :webauthn_id, unique: true/, migration)
40+
end
3141
end
3242
end
3343
end

0 commit comments

Comments
 (0)