require "spec_helper" require "support/have_persisted_matcher" require "support/object_store_setup" require "support/seed_data_setup" require "terrestrial" RSpec.describe "Graph persistence" do include_context "object store setup" include_context "seed data setup" subject(:user_store) { object_store.fetch(:users) } let(:user) { user_store.where(id: "users/1").first } context "without associations" do let(:modified_email) { "hasel+modified@gmail.com" } it "saves the root object" do user.email = modified_email user_store.save(user) expect(datastore).to have_persisted( :users, hash_including( id: "users/1", email: modified_email, ) ) end it "doesn't send associated objects to the database as columns" do user.email = modified_email user_store.save(user) expect(datastore).not_to have_persisted( :users, hash_including( posts: anything, ) ) end # TODO move to a dirty tracking spec? context "when mutating entity fields in place" do it "saves the object" do user.email << "MUTATED" user_store.save(user) expect(datastore).to have_persisted( :users, hash_including( id: "users/1", email: /MUTATED$/, ) ) end end end context "modify shallow has many associated object" do let(:post) { user.posts.first } let(:modified_post_body) { "modified ur body" } it "saves the associated object" do post.body = modified_post_body user_store.save(user) expect(datastore).to have_persisted( :posts, hash_including( id: post.id, subject: post.subject, author_id: user.id, body: modified_post_body, ) ) end end context "modify deeply nested has many associated object" do let(:comment) { user.posts.first.comments.first } let(:modified_comment_body) { "body moving, body moving" } it "saves the associated object" do comment.body = modified_comment_body user_store.save(user) expect(datastore).to have_persisted( :comments, hash_including( { id: "comments/1", post_id: "posts/1", commenter_id: "users/1", body: modified_comment_body, } ) ) end end context "add a node to a has many association" do let(:new_post_attrs) { { id: "posts/neu", subject: "I am new", body: "new body", comments: [], categories: [], created_at: Time.now, } } let(:new_post) { Post.new(new_post_attrs) } it "adds the object to the graph" do user.posts.push(new_post) expect(user.posts).to include(new_post) end it "persists the object" do user.posts.push(new_post) user_store.save(user) expect(datastore).to have_persisted( :posts, hash_including( id: "posts/neu", author_id: user.id, subject: "I am new", ) ) end context "when the collection is not loaded until the new object is persisted" do it "is consistent with the datastore" do user.posts.push(new_post) user_store.save(user) expect(user.posts.to_a.map(&:id)).to eq( ["posts/1", "posts/2", "posts/neu"] ) end end end context "delete an object from a has many association" do let(:post) { user.posts.first } it "delete the object from the graph" do user.posts.delete(post) expect(user.posts.map(&:id)).not_to include(post.id) end it "delete the object from the datastore on save" do user.posts.delete(post) user_store.save(user) expect(datastore).not_to have_persisted( :posts, hash_including( id: post.id, ) ) end end context "modify a many to many relationship" do let(:post) { user.posts.first } context "delete a node" do it "mutates the graph" do category = post.categories.first post.categories.delete(category) expect(post.categories.map(&:id)).not_to include(category.id) end it "deletes the 'join table' record" do category = post.categories.first post.categories.delete(category) user_store.save(user) expect(datastore).not_to have_persisted( :categories_to_posts, { post_id: post.id, category_id: category.id, } ) end it "does not delete the object" do category = post.categories.first post.categories.delete(category) user_store.save(user) expect(datastore).to have_persisted( :categories, hash_including( id: category.id, ) ) end end context "add a node" do let(:post_with_one_category) { user.posts.to_a.last } let(:new_category) { user.posts.first.categories.to_a.first } it "mutates the graph" do post_with_one_category.categories.push(new_category) expect(post_with_one_category.categories.map(&:id)) .to match_array(["categories/1", "categories/2"]) end it "persists the change" do post_with_one_category.categories.push(new_category) user_store.save(user) expect(datastore).to have_persisted( :categories_to_posts, { post_id: post_with_one_category.id, category_id: new_category.id, } ) end end context "modify a node" do let(:category) { user.posts.first.categories.first } let(:modified_category_name) { "modified category" } it "mutates the graph" do category.name = modified_category_name expect(post.categories.first.name) .to eq(modified_category_name) end it "persists the change" do category.name = modified_category_name user_store.save(user) expect(datastore).to have_persisted( :categories, { id: category.id, name: modified_category_name, } ) end end context "node loaded as root has undefined one to many association" do let(:post_store) { object_store[:posts] } let(:post) { post_store.where(id: "posts/1").first } it "persists the changes to the root node" do post.body = "modified body" post_store.save(post) expect(datastore).to have_persisted( :posts, hash_including( id: "posts/1", body: "modified body", ) ) end it "does not overwrite unused foreign key" do post.body = "modified body" post_store.save(post) expect(datastore).to have_persisted( :posts, hash_including( id: "posts/1", author_id: "users/1", ) ) end end end context "when a many to one association is nil" do context "when the object does not have a reference to its parent" do it "populates that association with a nil" do post = Post.new( id: "posts/orphan", subject: "Nils gonna getcha", body: "", created_at: Time.parse("2015-09-05T15:00:00+01:00"), categories: [], comments: [], ) object_store[:posts].save(post) expect(datastore).to have_persisted( :posts, hash_including( id: "posts/orphan", author_id: nil, ) ) end end context "when an existing partent object reference is set to nil" do it "populates that association with a nil" do comment = user .posts .flat_map(&:comments) .detect { |c| c.id == "comments/1" } comment.commenter = nil user_store.save(user) expect(datastore).to have_persisted( :comments, hash_including( id: "comments/1", commenter_id: nil, ) ) end end end context "when a save operation fails (some object is not persistable)" do let(:unpersistable_object) { ->() { } } it "rolls back the transaction" do pre_change = datastore[:users].to_a.map(&:to_a).sort begin user.first_name = "this will be rolled back" user.posts.first.subject = unpersistable_object user_store.save(user) rescue Terrestrial::Error end post_change = datastore[:users].to_a.map(&:to_a).sort expect(pre_change).to eq(post_change) end end end