diff --git a/README.md b/README.md index 5d1baea7..7ad45f69 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,12 @@ post = Post.find_by!(slug: "foo") # raises when no records found. other_post = Post.find_by(slug: "foo", type: "bar") # Also works for multiple arguments. ``` +#### Create +```crystal +Post.create(name: "Granite Rocks!", body: "Check this out.") # Set attributes and call save +Post.create!(name: "Granite Rocks!", body: "Check this out.") # Set attributes and call save!. Will throw an exception when the save failed +``` + #### Insert ```crystal @@ -342,6 +348,11 @@ post = Post.new post.name = "Granite Rocks!" post.body = "Check this out." post.save + +post = Post.new +post.name = "Granite Rocks!" +post.body = "Check this out." +post.save! # raises when save failed ``` #### Update @@ -350,6 +361,12 @@ post.save post = Post.find 1 post.name = "Granite Really Rocks!" post.save + +post = Post.find 1 +post.update(name: "Granite Really Rocks!") # Assigns attributes and calls save + +post = Post.find 1 +post.update!(name: "Granite Really Rocks!") # Assigns attributes and calls save!. Will throw an exception when the save failed ``` #### Delete @@ -358,6 +375,9 @@ post.save post = Post.find 1 post.destroy puts "deleted" unless post + +post = Post.find 1 +post.destroy! # raises when delete failed ``` ### Queries @@ -423,9 +443,9 @@ class CustomView < Granite:Base field commentbody : String query <<-SQL - SELECT articles.articlebody, comments.commentbody - FROM articles - JOIN comments + SELECT articles.articlebody, comments.commentbody + FROM articles + JOIN comments ON comments.articleid = articles.id SQL end diff --git a/spec/granite/exceptions/record_invalid_spec.cr b/spec/granite/exceptions/record_invalid_spec.cr new file mode 100644 index 00000000..9b6b5344 --- /dev/null +++ b/spec/granite/exceptions/record_invalid_spec.cr @@ -0,0 +1,27 @@ +require "../../spec_helper" + +{% for adapter in GraniteExample::ADAPTERS %} +module {{adapter.capitalize.id}} + describe Granite::RecordNotSaved do + it "should have a message" do + parent = Parent.new + parent.save + + Granite::RecordNotSaved + .new(Parent.name, parent) + .message + .should eq("Could not process {{adapter.capitalize.id}}::Parent") + end + + it "should have a model" do + parent = Parent.new + parent.save + + Granite::RecordNotSaved + .new(Parent.name, parent) + .model + .should eq(parent) + end + end +end +{% end %} diff --git a/spec/granite/exceptions/record_not_destroyed_spec.cr b/spec/granite/exceptions/record_not_destroyed_spec.cr new file mode 100644 index 00000000..f0441466 --- /dev/null +++ b/spec/granite/exceptions/record_not_destroyed_spec.cr @@ -0,0 +1,27 @@ +require "../../spec_helper" + +{% for adapter in GraniteExample::ADAPTERS %} +module {{adapter.capitalize.id}} + describe Granite::RecordNotDestroyed do + it "should have a message" do + parent = Parent.new + parent.save + + Granite::RecordNotDestroyed + .new(Parent.name, parent) + .message + .should eq("Could not destroy {{adapter.capitalize.id}}::Parent") + end + + it "should have a model" do + parent = Parent.new + parent.save + + Granite::RecordNotDestroyed + .new(Parent.name, parent) + .model + .should eq(parent) + end + end +end +{% end %} diff --git a/spec/granite/transactions/create_spec.cr b/spec/granite/transactions/create_spec.cr index a12697e1..66f90a83 100644 --- a/spec/granite/transactions/create_spec.cr +++ b/spec/granite/transactions/create_spec.cr @@ -5,13 +5,13 @@ module {{adapter.capitalize.id}} describe "{{ adapter.id }} .create" do it "creates a new object" do parent = Parent.create(name: "Test Parent") - parent.id.should_not be_nil + parent.persisted?.should be_true parent.name.should eq("Test Parent") end it "does not create an invalid object" do parent = Parent.create(name: "") - parent.id.should be_nil + parent.persisted?.should be_false end it "takes JSON::Any" do @@ -51,7 +51,7 @@ module {{adapter.capitalize.id}} describe "with a custom primary key" do it "creates a new object" do school = School.create(name: "Test School") - school.custom_id.should_not be_nil + school.persisted?.should be_true school.name.should eq("Test School") end end @@ -59,7 +59,7 @@ module {{adapter.capitalize.id}} describe "with a modulized model" do it "creates a new object" do county = Nation::County.create(name: "Test School") - county.id.should_not be_nil + county.persisted?.should be_true county.name.should eq("Test School") end end @@ -72,5 +72,19 @@ module {{adapter.capitalize.id}} end end end + + describe "{{ adapter.id }} .create!" do + it "creates a new object" do + parent = Parent.create!(name: "Test Parent") + parent.persisted?.should be_true + parent.name.should eq("Test Parent") + end + + it "does not save but raise an exception" do + expect_raises(Granite::RecordNotSaved, "{{adapter.capitalize.id}}::Parent") do + parent = Parent.create!(name: "") + end + end + end end {% end %} diff --git a/spec/granite/transactions/destroy_spec.cr b/spec/granite/transactions/destroy_spec.cr index 7b21f035..988afa21 100644 --- a/spec/granite/transactions/destroy_spec.cr +++ b/spec/granite/transactions/destroy_spec.cr @@ -55,5 +55,35 @@ module {{adapter.capitalize.id}} end end end + + describe "{{ adapter.id }} #destroy!" do + it "destroys an object" do + parent = Parent.new + parent.name = "Test Parent" + parent.save! + + id = parent.id + parent.destroy + found = Parent.find id + found.should be_nil + end + + it "does not destroy but raise an exception" do + callback_with_abort = CallbackWithAbort.new + callback_with_abort.name = "DestroyRaisesException" + callback_with_abort.abort_at = "temp" + callback_with_abort.do_abort = false + callback_with_abort.save! + callback_with_abort.abort_at = "before_destroy" + callback_with_abort.do_abort = true + + + expect_raises(Granite::RecordNotDestroyed, "{{adapter.capitalize.id}}::CallbackWithAbort") do + callback_with_abort.destroy! + end + + CallbackWithAbort.find_by(name: callback_with_abort.name).should_not be_nil + end + end end {% end %} diff --git a/spec/granite/transactions/save_spec.cr b/spec/granite/transactions/save_spec.cr index 80157fad..0e3ad248 100644 --- a/spec/granite/transactions/save_spec.cr +++ b/spec/granite/transactions/save_spec.cr @@ -7,14 +7,14 @@ module {{adapter.capitalize.id}} parent = Parent.new parent.name = "Test Parent" parent.save - parent.id.should_not be_nil + parent.persisted?.should be_true end it "does not create an invalid object" do parent = Parent.new parent.name = "" parent.save - parent.id.should be_nil + parent.persisted?.should be_false end it "updates an existing object" do @@ -96,7 +96,7 @@ module {{adapter.capitalize.id}} county = Nation::County.new county.name = "Test School" county.save - county.id.should_not be_nil + county.persisted?.should be_true end it "updates an existing object" do @@ -132,5 +132,22 @@ module {{adapter.capitalize.id}} end end end + + describe "{{ adapter.id }} #save!" do + it "creates a new object" do + parent = Parent.new + parent.name = "Test Parent" + parent.save! + parent.persisted?.should be_true + end + + it "does not create but raise an exception" do + parent = Parent.new + + expect_raises(Granite::RecordNotSaved, "{{adapter.capitalize.id}}::Parent") do + parent.save! + end + end + end end {% end %} diff --git a/spec/granite/transactions/update_spec.cr b/spec/granite/transactions/update_spec.cr new file mode 100644 index 00000000..8b407cfe --- /dev/null +++ b/spec/granite/transactions/update_spec.cr @@ -0,0 +1,50 @@ +require "../../spec_helper" + +{% for adapter in GraniteExample::ADAPTERS %} +module {{adapter.capitalize.id}} + describe "{{ adapter.id }} #update" do + it "updates an object" do + parent = Parent.new(name: "New Parent") + parent.save! + + parent.update(name: "Other parent").should be_true + parent.name.should eq "Other parent" + + Parent.find!(parent.id).name.should eq "Other parent" + end + + it "does not update an invalid object" do + parent = Parent.new(name: "New Parent") + parent.save! + + parent.update(name: "").should be_false + parent.name.should eq "" + + Parent.find!(parent.id).name.should eq "New Parent" + end + end + + describe "{{ adapter.id }} #update!" do + it "updates an object" do + parent = Parent.new(name: "New Parent") + parent.save! + + parent.update!(name: "Other parent") + parent.name.should eq "Other parent" + + Parent.find!(parent.id).name.should eq "Other parent" + end + + it "does not update but raises an exception" do + parent = Parent.new(name: "New Parent") + parent.save! + + expect_raises(Granite::RecordNotSaved, "{{adapter.capitalize.id}}::Parent") do + parent.update!(name: "") + end + + Parent.find!(parent.id).name.should eq "New Parent" + end + end +end +{% end %} diff --git a/spec/spec_models.cr b/spec/spec_models.cr index b973040e..aa615fa5 100644 --- a/spec/spec_models.cr +++ b/spec/spec_models.cr @@ -128,6 +128,7 @@ require "uuid" table_name callbacks_with_abort primary abort_at : String, auto: false field do_abort : Bool + field name : String property history : IO::Memory = IO::Memory.new diff --git a/src/granite/exceptions.cr b/src/granite/exceptions.cr new file mode 100644 index 00000000..94640d07 --- /dev/null +++ b/src/granite/exceptions.cr @@ -0,0 +1,21 @@ +module Granite + class RecordNotSaved < ::Exception + getter model : Granite::Base + + def initialize(class_name : String, model : Granite::Base) + super("Could not process #{class_name}") + + @model = model + end + end + + class RecordNotDestroyed < ::Exception + getter model : Granite::Base + + def initialize(class_name : String, model : Granite::Base) + super("Could not destroy #{class_name}") + + @model = model + end + end +end diff --git a/src/granite/transactions.cr b/src/granite/transactions.cr index 804f6c12..025672df 100644 --- a/src/granite/transactions.cr +++ b/src/granite/transactions.cr @@ -1,3 +1,5 @@ +require "./exceptions" + module Granite::Transactions module ClassMethods def create(**args) @@ -11,6 +13,20 @@ module Granite::Transactions instance end + def create!(**args) + create!(args.to_h) + end + + def create!(args : Hash(Symbol | String, DB::Any) | JSON::Any) + instance = create(args) + + if instance.errors.any? + raise Granite::RecordNotSaved.new(self.name, instance) + end + + instance + end + def from_json(args : JSON::Any) if args.as_a? args.as_a.map { |a| model = new; model.set_attributes(a); model } @@ -151,6 +167,31 @@ module Granite::Transactions true end + + def save! + save || raise Granite::RecordNotSaved.new(self.class.name, self) + end + + def update(**args) + update(args.to_h) + end + + def update(args : Hash(Symbol | String, DB::Any) | JSON::Any) + set_attributes(args) + + save + end + + def update!(**args) + update!(args.to_h) + end + + def update!(args : Hash(Symbol | String, DB::Any) | JSON::Any) + set_attributes(args) + + save! + end + # Destroy will remove this from the database. def destroy begin @@ -166,6 +207,10 @@ module Granite::Transactions end true end + + def destroy! + destroy || raise Granite::RecordNotDestroyed.new(self.class.name, self) + end end # Returns true if this object hasn't been saved yet.