published: 25th of August 2021
By default, new Lucky projects use the Int64 type for primary keys. In this post I will cover the process of changing the Lucky database primary keys type from an Int64 to a UUID .
The following software versions are used in this post.
This excellent post by Stephen Dolan outlines most of the process and without his guide, I would not known how to do this. There are a couple of extra steps that I needed to also complete which I assume was due to an earlier version of Lucky used in his post.
This post assumes you are working with a newly initialized app, it does not cover migrating existing data. At the end of this post I am completely resetting the database.
Create a migration file to enable the pgcrypto extension.
touch db/migrations/00000000000001_enable_pgcrypto.cr
Add the following contents to the db/migrations/00000000000001_enable_pgcrypto.cr file.
class EnablePgcrypto::V00000000000001 < Avram::Migrator::Migration::V1
def migrate
execute "CREATE EXTENSION IF NOT EXISTS pgcrypto"
end
def rollback
execute "DROP EXTENSION pgcrypto"
end
end
Rename the user migration file so the migration happens after the pgcrypto migration step.
mv db/migrations/00000000000001_create_users.cr db/migrations/00000000000002_create_users.cr
Update the db/migrations/00000000000002_create_users.cr file to use the UUID type and also rename the class.
# db/migrations/00000000000002_create_users.cr
# class CreateUsers::V00000000000001 < Avram::Migrator::Migration::V1
class CreateUsers::V00000000000002 < Avram::Migrator::Migration::V1
def migrate
enable_extension "citext"
create table_for(User) do
# primary_key id : Int64
primary_key id : UUID
add_timestamps
add email : String, unique: true, case_sensitive: false
add encrypted_password : String
end
end
def rollback
drop table_for(User)
disable_extension "citext"
end
end
Update the src/models/base_model.cr file to use the UUID type for the primary_key field on all tables. Other default parameters such as timestamps can also be set here.
# src/models/base_model.cr
abstract class BaseModel < Avram::Model
# Add this macro
macro default_columns
primary_key id : UUID
timestamps
end
def self.database : Avram::Database.class
AppDatabase
end
end
Update the src/pages/password_resets/new_page.cr file to use the UUID type for the user_id field.
# src/pages/password_resets/new_page.cr
class PasswordResets::NewPage < AuthLayout
needs operation : ResetPassword
# needs user_id : Int64
needs user_id : UUID
def content
h1 "Reset your password"
render_password_reset_form(@operation)
end
private def render_password_reset_form(op)
form_for PasswordResets::Create.with(@user_id) do
mount Shared::Field, attribute: op.password, label_text: "Password", &.password_input(autofocus: "true")
mount Shared::Field, attribute: op.password_confirmation, label_text: "Confirm Password", &.password_input
submit "Update Password", flow_id: "update-password-button"
end
end
end
Update the src/actions/password_resets/create.cr file to use the UUID type for the user_id field.
# src/actions/password_resets/create.cr
class PasswordResets::Create < BrowserAction
include Auth::PasswordResets::Base
include Auth::PasswordResets::TokenFromSession
post "/password_resets/:user_id" do
ResetPassword.update(user, params) do |operation, user|
if operation.saved?
session.delete(:password_reset_token)
sign_in user
flash.success = "Your password has been reset"
redirect to: Home::Index
else
# html NewPage, operation: operation, user_id: user_id.to_i64
html NewPage, operation: operation, user_id: UUID.new(user_id)
end
end
end
end
Update the src/actions/password_resets/edit.cr file to use the UUID type for the user_id field.
# src/actions/password_resets/edit.cr
class PasswordResets::Edit < BrowserAction
include Auth::PasswordResets::Base
include Auth::PasswordResets::TokenFromSession
get "/password_resets/:user_id/edit" do
# html NewPage, operation: ResetPassword.new, user_id: user_id.to_i64
html NewPage, operation: ResetPassword.new, user_id: UUID.new(user_id)
end
end
Update the src/models/user_token.cr file to return a UUID from the user_id param. Also update the return type to UUID .
# src/models/user_token.cr
# Generates and decodes JSON Web Tokens for Authenticating users.
class UserToken
Habitat.create { setting stubbed_token : String? }
ALGORITHM = JWT::Algorithm::HS256
def self.generate(user : User) : String
payload = {"user_id" => user.id}
settings.stubbed_token || create_token(payload)
end
def self.create_token(payload)
JWT.encode(payload, Lucky::Server.settings.secret_key_base, ALGORITHM)
end
# def self.decode_user_id(token : String) : Int64?
def self.decode_user_id(token : String) : UUID?
payload, _header = JWT.decode(token, Lucky::Server.settings.secret_key_base, ALGORITHM)
# payload["user_id"].to_s.to_i64
UUID.new(payload["user_id"].to_s)
rescue e : JWT::Error
Lucky::Log.dexter.error { {jwt_decode_error: e.message} }
nil
end
# Used in tests to return a fake token to test against.
def self.stub_token(token : String)
temp_config(stubbed_token: token) do
yield
end
end
end
Once that is completed, setup the DB and run the migrations with the lucky db.reset cli command.
In this post we setup UUIDs for the primary key field on a postgres database for a Lucky webapp.
https://stephencodes.com/blog/switching-lucky-to-uuids/
https://luckyframework.org/guides/database/migrations#primary-keys