lib/pupa/processor/connection_adapters/mongodb_adapter.rb
require 'mongo'
module Pupa
class Processor
class Connection
# A proxy class to save plain old Ruby objects to MongoDB.
class MongoDBAdapter
attr_reader :raw_connection
# @param [String] database_url the database URL
def initialize(database_url)
uri = URI.parse(database_url)
@raw_connection = Mongo::Client.new(["#{uri.host}:#{uri.port}"], database: uri.path[1..-1])
@raw_connection = @raw_connection.with(user: uri.user, password: uri.password) if uri.user && uri.password
end
# Finds a document matching the selection criteria.
#
# The selection criteria *must* set a `_type` key in order to determine
# the collection to query.
#
# @param [Hash] selector the selection criteria
# @return [Hash,nil] the matched document, or nil
# @raises [Pupa::Errors::TooManyMatches] if multiple documents are found
def find(selector)
collection_name = collection_name_from_class_name(selector[:_type].camelize)
if selector.except(:_type).empty?
raise Errors::EmptySelectorError, "selector is empty during find in collection #{collection_name}"
end
collection = raw_connection[collection_name]
query = collection.find(selector)
case query.count
when 0
nil
when 1
query.first
else
raise Errors::TooManyMatches, "selector matches multiple documents during find in collection #{collection_name}: #{JSON.dump(selector)}"
end
end
# Inserts or replaces a document in MongoDB.
#
# @param [Object] object an object
# @return [Array] whether the object was inserted and the object's database ID
# @raises [Pupa::Errors::TooManyMatches] if multiple documents would be updated
def save(object)
selector = object.fingerprint
collection_name = collection_name_from_class_name(object.class.to_s)
if selector.empty?
raise Errors::EmptySelectorError, "selector is empty during save in collection #{collection_name} for #{object._id}"
end
collection = raw_connection[collection_name]
query = collection.find(selector)
# Run query before callbacks to avoid e.g. timestamps in the selector.
case query.count
when 0
object.run_callbacks(:save) do
object.run_callbacks(:create) do
collection.insert_one(object.to_h(persist: true))
[true, object._id.to_s]
end
end
when 1
# Make the document available to the callbacks.
# @see https://github.com/jpmckinney/pupa-ruby/issues/17
object.document = query.first
object.run_callbacks(:save) do
query.update_one(object.to_h(persist: true).except(:_id))
[false, object.document['_id'].to_s]
end
else
raise Errors::TooManyMatches, "selector matches multiple documents during save in collection #{collection_name} for #{object._id}: #{JSON.dump(selector)}"
end
end
private
# Returns the name of the collection in which to save the object.
#
# @param [String] class_name the name of the object's class
# @return [String] the name of the collection in which to save the object
def collection_name_from_class_name(class_name)
class_name.demodulize.underscore.pluralize.to_sym
end
end
end
end
end