I have this operation:
require 'bcrypt'
class User::Create < Trailblazer::Operation
extend Contract::DSL
contract 'params', (Dry::Validation.Schema do
required(:email).filled
required(:password).filled
end)
step Contract::Validate(name: 'params'), before: 'operation.new'
step Model(User, :new)
success :generate_token
success :encrypt_password
success :assign_user_values
step Contract::Build(constant: User::Contract::Create)
step Contract::Validate()
step Contract::Persist()
def generate_token(options, *)
token = loop do
random_token = SecureRandom.uuid
break random_token unless User.exists?(token: random_token)
end
options['data.token'] = token
end
def encrypt_password(options, params:, **)
options['data.password'] = BCrypt::Password.create(params[:password])
end
def assign_user_values(options, params:, **)
options['model'].token = options['data.token']
options['model'].password = options['data.password']
options['model'].role = 'user'
end
end
When I run the operation, I get this output:
User::Create.(email: 'bidule@gmail.com', password: 'bidule')
User Exists (0.6ms) SELECT 1 AS one FROM "users" WHERE "users"."token" = $1 LIMIT $2 [["token", "64423af4-5741-46af-9756-15715a632d75"], ["LIMIT", 1]]
(0.6ms) SELECT COUNT(*) FROM "users" WHERE "users"."email" = $1 [["email", "bidule@gmail.com"]]
(0.4ms) SELECT COUNT(*) FROM "users" WHERE "users"."token" = $1 [["token", "64423af4-5741-46af-9756-15715a632d75"]]
(0.3ms) BEGIN
SQL (0.5ms) INSERT INTO "users" ("email", "password", "role", "token", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["email", "bidule@gmail.com"], ["password", "bidule"], ["role", "user"], ["token", "64423af4-5741-46af-9756-15715a632d75"], ["created_at", "2017-12-30 23:34:24.456817"], ["updated_at", "2017-12-30 23:34:24.456817"]]
(2.8ms) COMMIT
=> <Result:true <Skill {"contract.params.params"=>{:email=>"bidule@gmail.com", :password=>"bidule"}, "representer.params.class"=>false, "result.contract.params"=>#<Dry::Validation::Result output={:email=>"bidule@gmail.com", :password=>"bidule"} errors={}>, "model.class"=>User(id: integer, email: string, password: string, role: string, token: string, created_at: datetime, updated_at: datetime), "model.action"=>:new, "model"=>#<User id: 17, email: "bidule@gmail.com", password: "bidule", role: "user", token: "64423af4-5741-46af-9756-15715a632d75", created_at: "2017-12-30 23:34:24", updated_at: "2017-12-30 23:34:24">, "result.model"=><Result:true {} >, "data.token"=>"64423af4-5741-46af-9756-15715a632d75", "data.password"=>"$2a$10$gy19txopkeM5Vovx4/ohvOuZAP2CMwtBTcCwswJq9Ije7NVrKj8Ce", "contract.default"=>#<User::Contract::Create:0x0000000004e316e8 @fields={"email"=>"bidule@gmail.com", "password"=>"bidule", "role"=>"user", "token"=>"64423af4-5741-46af-9756-15715a632d75"}, @model=#<User id: 17, email: "bidule@gmail.com", password: "bidule", role: "user", token: "64423af4-5741-46af-9756-15715a632d75", created_at: "2017-12-30 23:34:24", updated_at: "2017-12-30 23:34:24">, @mapper=#<#<Class:0x0000000004e31558>:0x0000000004e30bf8 @model=#<User id: 17, email: "bidule@gmail.com", password: "bidule", role: "user", token: "64423af4-5741-46af-9756-15715a632d75", created_at: "2017-12-30 23:34:24", updated_at: "2017-12-30 23:34:24">>, @_changes={"email"=>true, "password"=>true}, @errors=#<Reform::Form::ActiveModel::Errors:0x0000000004e30338 @base=#<User::Contract::Create:0x0000000004e316e8 ...>, @messages={}, @details={}>>, "contract.default.params"=>{:email=>"bidule@gmail.com", :password=>"bidule"}, "representer.default.class"=>false, "result.contract.default"=>#<User::Contract::Create:0x0000000004e316e8 @fields={"email"=>"bidule@gmail.com", "password"=>"bidule", "role"=>"user", "token"=>"64423af4-5741-46af-9756-15715a632d75"}, @model=#<User id: 17, email: "bidule@gmail.com", password: "bidule", role: "user", token: "64423af4-5741-46af-9756-15715a632d75", created_at: "2017-12-30 23:34:24", updated_at: "2017-12-30 23:34:24">, @mapper=#<#<Class:0x0000000004e31558>:0x0000000004e30bf8 @model=#<User id: 17, email: "bidule@gmail.com", password: "bidule", role: "user", token: "64423af4-5741-46af-9756-15715a632d75", created_at: "2017-12-30 23:34:24", updated_at: "2017-12-30 23:34:24">>, @_changes={"email"=>true, "password"=>true}, @errors=#<Reform::Form::ActiveModel::Errors:0x0000000004e30338 @base=#<User::Contract::Create:0x0000000004e316e8 ...>, @messages={}, @details={}>>} {"params"=>{:email=>"bidule@gmail.com", :password=>"bidule"}} {"pipetree"=>[>contract.params.validate,>operation.new,>model.build,>generate_token,>encrypt_password,>assign_user_values,>contract.build,>contract.default.validate,>persist.save], "contract.params"=>#<#<Class:0x0000000005261790>:0x0000000005245798 @type_map={}, @config=#<#<Class:0x0000000004b1a518>:0x00000000052616a0 @config={:input_processor=>:noop, :hash_type=>:weak, :type_map=>{}, :path=>[], :predicates=>Dry::Logic::Predicates, :registry=>#<Dry::Validation::PredicateRegistry::Unbound:0x0000000005261588 @external=Dry::Logic::Predicates, @predicates={}>, :messages=>:yaml, :messages_file=>nil, :namespace=>nil, :rules=>[[:rule, [:email, [:and, [[:rule, [:email, [:predicate, [:key?, [[:name, :email], [:input, Undefined]]]]]], [:rule, [:email, [:key, [:email, [:predicate, [:filled?, [[:input, Undefined]]]]]]]]]]]], [:rule, [:password, [:and, [[:rule, [:password, [:predicate, [:key?, [[:name, :password], [:input, Undefined]]]]]], [:rule, [:password, [:key, [:password, [:predicate, [:filled?, [[:input, Undefined]]]]]]]]]]]]], :checks=>[], :options=>{}, :input=>nil, :input_rule=>nil, :dsl_extensions=>nil, :input_processor_map=>{:sanitizer=>#<Dry::Validation::InputProcessorCompiler::Sanitizer:0x0000000004afb618 @type_compiler=#<Dry::Types::Compiler:0x0000000004afb5f0 @registry=Dry::Types>>, :json=>#<Dry::Validation::InputProcessorCompiler::JSON:0x0000000004afb5c8 @type_compiler=#<Dry::Types::Compiler:0x0000000004afb5a0 @registry=Dry::Types>>, :form=>#<Dry::Validation::InputProcessorCompiler::Form:0x0000000004afb578 @type_compiler=#<Dry::Types::Compiler:0x0000000004afb550 @registry=Dry::Types>>}, :type_specs=>false}>, @predicates=#<Dry::Validation::PredicateRegistry::Bound:0x0000000005245720 @external=Dry::Logic::Predicates, @predicates={}, @schema=#<#<Class:0x0000000005261790>:0x0000000005245798 ...>>, @rule_compiler=#<Dry::Validation::SchemaCompiler:0x0000000005245630 @predicates=#<Dry::Validation::PredicateRegistry::Bound:0x0000000005245720 @external=Dry::Logic::Predicates, @predicates={}, @schema=#<#<Class:0x0000000005261790>:0x0000000005245798 ...>>, @options={:predicate_registry=>#<Dry::Validation::PredicateRegistry::Unbound:0x0000000005261588 @external=Dry::Logic::Predicates, @predicates={}>, :message_compiler=>#<Dry::Validation::MessageCompiler:0x00000000052459c8 @messages=#<Dry::Validation::Messages::YAML data={"en.errors.or"=>"or", "en.errors.array?"=>"must be an array", "en.errors.empty?"=>"must be empty", "en.errors.excludes?"=>"must not include %{value}", "en.errors.excluded_from?.arg.default"=>"must not be one of: %{list}", "en.errors.excluded_from?.arg.range"=>"must not be one of: %{list_left} - %{list_right}", "en.errors.exclusion?"=>"must not be one of: %{list}", "en.errors.eql?"=>"must be equal to %{left}", "en.errors.not_eql?"=>"must not be equal to %{left}", "en.errors.filled?"=>"must be filled", "en.errors.format?"=>"is in invalid format", "en.errors.number?"=>"must be a number", "en.errors.odd?"=>"must be odd", "en.errors.even?"=>"must be even", "en.errors.gt?"=>"must be greater than %{num}", "en.errors.gteq?"=>"must be greater than or equal to %{num}", "en.errors.hash?"=>"must be a hash", "en.errors.included_in?.arg.default"=>"must be one of: %{list}", "en.errors.included_in?.arg.range"=>"must be one of: %{list_left} - %{list_right}", "en.errors.inclusion?"=>"must be one of: %{list}", "en.errors.includes?"=>"must include %{value}", "en.errors.bool?"=>"must be boolean", "en.errors.true?"=>"must be true", "en.errors.false?"=>"must be false", "en.errors.int?"=>"must be an integer", "en.errors.float?"=>"must be a float", "en.errors.decimal?"=>"must be a decimal", "en.errors.date?"=>"must be a date", "en.errors.date_time?"=>"must be a date time", "en.errors.time?"=>"must be a time", "en.errors.key?"=>"is missing", "en.errors.attr?"=>"is missing", "en.errors.lt?"=>"must be less than %{num}", "en.errors.lteq?"=>"must be less than or equal to %{num}", "en.errors.max_size?"=>"size cannot be greater than %{num}", "en.errors.min_size?"=>"size cannot be less than %{num}", "en.errors.none?"=>"cannot be defined", "en.errors.str?"=>"must be a string", "en.errors.type?"=>"must be %{type}", "en.errors.size?.arg.default"=>"size must be %{size}", "en.errors.size?.arg.range"=>"size must be within %{size_left} - %{size_right}", "en.errors.size?.value.string.arg.default"=>"length must be %{size}", "en.errors.size?.value.string.arg.range"=>"length must be within %{size_left} - %{size_right}"}>, @options={}, @full=false, @hints=true, @locale=:en, @default_lookup_options={:locale=>:en}>, :input_processor=>#<Proc:0x000000000385d1a0@/app/vendor/bundle/ruby/2.3.0/gems/dry-validation-0.11.1/lib/dry/validation/schema/class_interface.rb:11 (lambda)>, :checks=>[]}, @schema=#<#<Class:0x0000000005261790>:0x0000000005245798 ...>>, @message_compiler=#<Dry::Validation::MessageCompiler:0x00000000052459c8 @messages=#<Dry::Validation::Messages::YAML data={"en.errors.or"=>"or", "en.errors.array?"=>"must be an array", "en.errors.empty?"=>"must be empty", "en.errors.excludes?"=>"must not include %{value}", "en.errors.excluded_from?.arg.default"=>"must not be one of: %{list}", "en.errors.excluded_from?.arg.range"=>"must not be one of: %{list_left} - %{list_right}", "en.errors.exclusion?"=>"must not be one of: %{list}", "en.errors.eql?"=>"must be equal to %{left}", "en.errors.not_eql?"=>"must not be equal to %{left}", "en.errors.filled?"=>"must be filled", "en.errors.format?"=>"is in invalid format", "en.errors.number?"=>"must be a number", "en.errors.odd?"=>"must be odd", "en.errors.even?"=>"must be even", "en.errors.gt?"=>"must be greater than %{num}", "en.errors.gteq?"=>"must be greater than or equal to %{num}", "en.errors.hash?"=>"must be a hash", "en.errors.included_in?.arg.default"=>"must be one of: %{list}", "en.errors.included_in?.arg.range"=>"must be one of: %{list_left} - %{list_right}", "en.errors.inclusion?"=>"must be one of: %{list}", "en.errors.includes?"=>"must include %{value}", "en.errors.bool?"=>"must be boolean", "en.errors.true?"=>"must be true", "en.errors.false?"=>"must be false", "en.errors.int?"=>"must be an integer", "en.errors.float?"=>"must be a float", "en.errors.decimal?"=>"must be a decimal", "en.errors.date?"=>"must be a date", "en.errors.date_time?"=>"must be a date time", "en.errors.time?"=>"must be a time", "en.errors.key?"=>"is missing", "en.errors.attr?"=>"is missing", "en.errors.lt?"=>"must be less than %{num}", "en.errors.lteq?"=>"must be less than or equal to %{num}", "en.errors.max_size?"=>"size cannot be greater than %{num}", "en.errors.min_size?"=>"size cannot be less than %{num}", "en.errors.none?"=>"cannot be defined", "en.errors.str?"=>"must be a string", "en.errors.type?"=>"must be %{type}", "en.errors.size?.arg.default"=>"size must be %{size}", "en.errors.size?.arg.range"=>"size must be within %{size_left} - %{size_right}", "en.errors.size?.value.string.arg.default"=>"length must be %{size}", "en.errors.size?.value.string.arg.range"=>"length must be within %{size_left} - %{size_right}"}>, @options={}, @full=false, @hints=true, @locale=:en, @default_lookup_options={:locale=>:en}>, @input_processor=#<Proc:0x000000000385d1a0@/app/vendor/bundle/ruby/2.3.0/gems/dry-validation-0.11.1/lib/dry/validation/schema/class_interface.rb:11 (lambda)>, @options={:predicate_registry=>#<Dry::Validation::PredicateRegistry::Unbound:0x0000000005261588 @external=Dry::Logic::Predicates, @predicates={}>, :message_compiler=>#<Dry::Validation::MessageCompiler:0x00000000052459c8 @messages=#<Dry::Validation::Messages::YAML data={"en.errors.or"=>"or", "en.errors.array?"=>"must be an array", "en.errors.empty?"=>"must be empty", "en.errors.excludes?"=>"must not include %{value}", "en.errors.excluded_from?.arg.default"=>"must not be one of: %{list}", "en.errors.excluded_from?.arg.range"=>"must not be one of: %{list_left} - %{list_right}", "en.errors.exclusion?"=>"must not be one of: %{list}", "en.errors.eql?"=>"must be equal to %{left}", "en.errors.not_eql?"=>"must not be equal to %{left}", "en.errors.filled?"=>"must be filled", "en.errors.format?"=>"is in invalid format", "en.errors.number?"=>"must be a number", "en.errors.odd?"=>"must be odd", "en.errors.even?"=>"must be even", "en.errors.gt?"=>"must be greater than %{num}", "en.errors.gteq?"=>"must be greater than or equal to %{num}", "en.errors.hash?"=>"must be a hash", "en.errors.included_in?.arg.default"=>"must be one of: %{list}", "en.errors.included_in?.arg.range"=>"must be one of: %{list_left} - %{list_right}", "en.errors.inclusion?"=>"must be one of: %{list}", "en.errors.includes?"=>"must include %{value}", "en.errors.bool?"=>"must be boolean", "en.errors.true?"=>"must be true", "en.errors.false?"=>"must be false", "en.errors.int?"=>"must be an integer", "en.errors.float?"=>"must be a float", "en.errors.decimal?"=>"must be a decimal", "en.errors.date?"=>"must be a date", "en.errors.date_time?"=>"must be a date time", "en.errors.time?"=>"must be a time", "en.errors.key?"=>"is missing", "en.errors.attr?"=>"is missing", "en.errors.lt?"=>"must be less than %{num}", "en.errors.lteq?"=>"must be less than or equal to %{num}", "en.errors.max_size?"=>"size cannot be greater than %{num}", "en.errors.min_size?"=>"size cannot be less than %{num}", "en.errors.none?"=>"cannot be defined", "en.errors.str?"=>"must be a string", "en.errors.type?"=>"must be %{type}", "en.errors.size?.arg.default"=>"size must be %{size}", "en.errors.size?.arg.range"=>"size must be within %{size_left} - %{size_right}", "en.errors.size?.value.string.arg.default"=>"length must be %{size}", "en.errors.size?.value.string.arg.range"=>"length must be within %{size_left} - %{size_right}"}>, @options={}, @full=false, @hints=true, @locale=:en, @default_lookup_options={:locale=>:en}>, :input_processor=>#<Proc:0x000000000385d1a0@/app/vendor/bundle/ruby/2.3.0/gems/dry-validation-0.11.1/lib/dry/validation/schema/class_interface.rb:11 (lambda)>, :checks=>[]}, @rules={:email=>#<Dry::Logic::Operations::And rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#key?> options={:args=>[:email], :id=>:email}>, #<Dry::Logic::Operations::Key rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#filled?> options={:args=>[]}>] options={:name=>:email, :evaluator=>#<Dry::Logic::Evaluator::Key path=[:email]>, :path=>:email, :id=>:email}>] options={:id=>:email}>, :password=>#<Dry::Logic::Operations::And rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#key?> options={:args=>[:password], :id=>:password}>, #<Dry::Logic::Operations::Key rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#filled?> options={:args=>[]}>] options={:name=>:password, :evaluator=>#<Dry::Logic::Evaluator::Key path=[:password]>, :path=>:password, :id=>:password}>] options={:id=>:password}>}, @checks={}, @executor=#<Dry::Validation::Executor:0x000000000524f8b0 @steps=[#<Dry::Validation::ProcessInput:0x000000000524f860 @processor=#<Proc:0x000000000385d1a0@/app/vendor/bundle/ruby/2.3.0/gems/dry-validation-0.11.1/lib/dry/validation/schema/class_interface.rb:11 (lambda)>>, #<Dry::Validation::ApplyRules:0x000000000524f838 @rules={:email=>#<Dry::Logic::Operations::And rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#key?> options={:args=>[:email], :id=>:email}>, #<Dry::Logic::Operations::Key rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#filled?> options={:args=>[]}>] options={:name=>:email, :evaluator=>#<Dry::Logic::Evaluator::Key path=[:email]>, :path=>:email, :id=>:email}>] options={:id=>:email}>, :password=>#<Dry::Logic::Operations::And rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#key?> options={:args=>[:password], :id=>:password}>, #<Dry::Logic::Operations::Key rules=[#<Dry::Logic::Rule::Predicate predicate=#<Method: Module(Dry::Logic::Predicates::Methods)#filled?> options={:args=>[]}>] options={:name=>:password, :evaluator=>#<Dry::Logic::Evaluator::Key path=[:password]>, :path=>:password, :id=>:password}>] options={:id=>:password}>}>], @final=#<Dry::Validation::BuildErrors:0x000000000524f8d8>>>}> >
The password of the model is not encrypted in the database, I believe this is because Contract::Build is fetching the password from the params and not the model.
Any thoughts?
You should encrypt the value on the contract before it syncs to the model:
def assign_user_values(options, params:, **)
options['model'].token = options['data.token']
options['contract.default'].password = options['data.password']
options['model'].role = 'user'
end
Since :token and :role are not in the contract, you can set these values directly on the model, but Contract::Persist() is going to sync the value from params, so change the value of the field in the contract before you sync to the model.
Edit: Note, you could also just do this in the method that encrypts the password; no need to pass that data around internally between steps.