Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ user = User.first
user = User.find_by(email: "alice@example.com")
user = User.find_by!(email: "alice@example.com") # raises if not found

# find or create
user = User.find_or_create_by(email: "alice@example.com") # finds or creates
user = User.find_or_create_by!(email: "alice@example.com") # raises on validation error

# find or create with a block (block only runs if creating)
user = User.find_or_create_by(email: "alice@example.com") do |u|
u.first_name = "Alice"
u.last_name = "Smith"
end

# update
user.update(first_name: "Alicia")
user.first_name = "Alicia"
Expand Down
14 changes: 14 additions & 0 deletions lib/airctiverecord/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ def relation_class
end

def relation_class_name = "#{name}::Relation"

def create(attributes = {}, &block)
record = new(attributes)
yield(record) if block_given?
record.save
record
end

def create!(attributes = {}, &block)
record = new(attributes)
yield(record) if block_given?
record.save!
record
end
end

def initialize(attributes = {}, **kwargs)
Expand Down
8 changes: 8 additions & 0 deletions lib/airctiverecord/scoping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ def find_by(conditions) = all.find_by(conditions)

def find_by!(conditions) = all.find_by!(conditions)

def find_or_create_by(conditions, &block)
find_by(conditions) || create(conditions, &block)
end

def find_or_create_by!(conditions, &block)
find_by(conditions) || create!(conditions, &block)
end

def first(limit = nil) = all.first(limit)

def last(limit = nil) = all.last(limit)
Expand Down
2 changes: 1 addition & 1 deletion lib/airctiverecord/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module AirctiveRecord
VERSION = "0.2.1"
VERSION = "0.3.0"
end
239 changes: 239 additions & 0 deletions spec/find_or_create_by_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "find_or_create_by" do
let(:model_class) do
build_test_model("TestUser") do
self.base_key = "appTest123"
self.table_name = "Users"

field :email, "Email Address"
field :name, "Name"
field :age, "Age"

validates :email, presence: true
validates :age, numericality: { greater_than: 0 }, allow_nil: true

def self.records(**params)
@last_params = params
@stub_records || []
end

class << self
attr_reader :last_params
attr_accessor :stub_records
end
end
end

before do
model_class.stub_records = []
end

describe "#find_or_create_by" do
context "when record exists" do
it "returns the existing record" do
existing_record = model_class.new(email: "test@example.com", name: "Alice")
existing_record.instance_variable_set(:@id, "rec123")
existing_record.instance_variable_set(:@new_record, false)

model_class.stub_records = [existing_record]

result = model_class.find_or_create_by(email: "test@example.com")

expect(result).to eq(existing_record)
expect(result.id).to eq("rec123")
end
end

context "when record does not exist" do
it "creates a new record" do
# Mock save to return true and set id
allow_any_instance_of(model_class).to receive(:save) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by(email: "new@example.com", name: "Bob")

expect(result).to be_a(model_class)
expect(result.email).to eq("new@example.com")
expect(result.name).to eq("Bob")
expect(result.id).to be_present
end

it "returns the created record even if invalid" do
result = model_class.find_or_create_by(email: nil, name: "Invalid")

expect(result).to be_a(model_class)
expect(result.valid?).to be false
expect(result.new_record?).to be true
end
end

context "with field mappings" do
it "uses field mappings in both find and create" do
# Mock save to return true and set id
allow_any_instance_of(model_class).to receive(:save) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by(email: "mapped@example.com", name: "Charlie")

# Check that the find_by was called with conditions
expect(model_class.last_params[:filter]).to include("{Email Address} = 'mapped@example.com'")

# Check record was created with mapped fields
expect(result.email).to eq("mapped@example.com")
expect(result.name).to eq("Charlie")
end
end

context "with a block" do
it "yields to the block when creating a new record" do
# Mock save to return true and set id
allow_any_instance_of(model_class).to receive(:save) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by(email: "block@example.com") do |user|
user.name = "Block User"
user.age = 25
end

expect(result.email).to eq("block@example.com")
expect(result.name).to eq("Block User")
expect(result.age).to eq(25)
end

it "does not yield when record exists" do
existing_record = model_class.new(email: "exists@example.com", name: "Existing")
existing_record.instance_variable_set(:@id, "rec789")
existing_record.instance_variable_set(:@new_record, false)

model_class.stub_records = [existing_record]

block_called = false
result = model_class.find_or_create_by(email: "exists@example.com") do |user|
block_called = true
user.name = "Should Not Change"
end

expect(block_called).to be false
expect(result.name).to eq("Existing")
end
end
end

describe "#find_or_create_by!" do
context "when record exists" do
it "returns the existing record" do
existing_record = model_class.new(email: "test@example.com", name: "Alice")
existing_record.instance_variable_set(:@id, "rec456")
existing_record.instance_variable_set(:@new_record, false)

model_class.stub_records = [existing_record]

result = model_class.find_or_create_by!(email: "test@example.com")

expect(result).to eq(existing_record)
expect(result.id).to eq("rec456")
end
end

context "when record does not exist" do
it "creates a new record" do
# Mock save! to return true and set id
allow_any_instance_of(model_class).to receive(:save!) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by!(email: "new@example.com", name: "Bob")

expect(result).to be_a(model_class)
expect(result.email).to eq("new@example.com")
expect(result.name).to eq("Bob")
expect(result.id).to be_present
end

it "raises RecordInvalid if validation fails" do
expect do
model_class.find_or_create_by!(email: nil, name: "Invalid")
end.to raise_error(AirctiveRecord::RecordInvalid, /can't be blank/)
end
end

context "with field mappings" do
it "uses field mappings in both find and create" do
# Mock save! to return true and set id
allow_any_instance_of(model_class).to receive(:save!) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by!(email: "mapped2@example.com", name: "Diana")

# Check that the find_by was called with conditions
expect(model_class.last_params[:filter]).to include("{Email Address} = 'mapped2@example.com'")

# Check record was created with mapped fields
expect(result.email).to eq("mapped2@example.com")
expect(result.name).to eq("Diana")
end
end

context "with a block" do
it "yields to the block when creating a new record" do
# Mock save! to return true and set id
allow_any_instance_of(model_class).to receive(:save!) do |record|
record.instance_variable_set(:@id, "rec#{rand(100000)}")
record.instance_variable_set(:@new_record, false)
true
end

result = model_class.find_or_create_by!(email: "block@example.com") do |user|
user.name = "Block User"
user.age = 30
end

expect(result.email).to eq("block@example.com")
expect(result.name).to eq("Block User")
expect(result.age).to eq(30)
end

it "does not yield when record exists" do
existing_record = model_class.new(email: "exists@example.com", name: "Existing")
existing_record.instance_variable_set(:@id, "rec999")
existing_record.instance_variable_set(:@new_record, false)

model_class.stub_records = [existing_record]

block_called = false
result = model_class.find_or_create_by!(email: "exists@example.com") do |user|
block_called = true
user.name = "Should Not Change"
end

expect(block_called).to be false
expect(result.name).to eq("Existing")
end

it "raises RecordInvalid with block attributes if validation fails" do
expect do
model_class.find_or_create_by!(email: "invalid@example.com") do |user|
user.email = nil # Make it invalid
end
end.to raise_error(AirctiveRecord::RecordInvalid, /can't be blank/)
end
end
end
end