Files
simply_based/lib/simply_based/model/find_by.rb
T
GregandClaude Opus 4.8 86299b8827 SimplyBased 0.1.0 — Couchbase-native ActiveModel ORM
A from-scratch fork of simply_couch's model layer that runs on Couchbase
natively: `include SimplyBased::Model` instead of `include SimplyCouch::Model`.

- SimplyBased::Store replaces the CouchDB layer: KV documents, N1QL/SQL++
  finders + lazily-created GSIs, CAS mapped onto _rev. No design documents or
  JavaScript map/reduce — the hand-written association views become N1QL
  (belongs_to/has_many/has_one scalar FK; habtm via ANY ... SATISFIES + array
  index, or KV multi-get on the storing side).
- Redis replacements on the same cluster: Counter (atomic increment/decrement),
  Cache (ActiveSupport::Cache::Store over KV+TTL, drop-in Rails.cache/ActiveJob),
  Lock (insert-if-absent + TTL + CAS-checked release).
- Runs the simply_couch test suite against live Couchbase: 378/432 applicable
  examples green (CouchDB-mechanic specs tagged :couchdb_only and excluded).
- Remaining: ancestry (tree) + embedded_in reverse-lookups (recursive/embedded
  N1QL) and a few pagination/lifecycle edges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:58:47 -05:00

212 lines
10 KiB
Ruby

module SimplyBased
module Model
module FindBy
include PaginationOptions
def _define_find_by(name, *args)
raise_when_not_found = name.to_s.end_with?('!')
name = name.to_s.chop.to_sym if raise_when_not_found
keys = name.to_s.sub(/^find_by_/, "").split("_and_").map(&:to_sym)
# replace asociation assignments with their property values if possible
keys.each.with_index do |key, i|
if properties.find{|p| p.name.to_sym == key.to_sym}.is_a?(SimplyBased::Model::BelongsTo::Property)
keys[i] = "#{keys[i]}_id"
end
end
view_name = name.to_s.sub(/^find_/, "").to_sym
view_keys = keys.length == 1 ? keys.first : keys
without_deleted_view_name = "#{view_name}_withoutdeleted"
without_deleted_view_keys = keys + [:deleted_at]
unless respond_to?(view_name)
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(view_name, key: view_keys)
end
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(without_deleted_view_name, key: without_deleted_view_keys)
end
if raise_when_not_found
(class << self; self end).instance_eval do
define_method(:"#{name}!") do |*key_args|
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
options.assert_valid_keys(:with_deleted)
with_deleted = options.delete(:with_deleted)
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size
key_args.map!{|a| a.is_a?(SimplyBased::Model) ? a.id : a}
if soft_deleting_enabled? && !with_deleted
key_args = key_args + [nil] # deleted_at
result = database.view(send(without_deleted_view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
else
result = database.view(send(view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
end
raise SimplyBased::RecordNotFound unless result
result
end
end
send(:"#{name}!", *args)
else
(class << self; self end).instance_eval do
define_method(name) do |*key_args|
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
options.assert_valid_keys(:with_deleted)
with_deleted = options.delete(:with_deleted)
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size
key_args.map!{|a| a.is_a?(SimplyBased::Model) ? a.id : a}
if soft_deleting_enabled? && !with_deleted
key_args = key_args + [nil] # deleted_at
database.view(send(without_deleted_view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
else
database.view(send(view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
end
end
end
send(name, *args)
end
end
def _define_find_all_by(name, *args)
raise_when_not_found = name.to_s.end_with?('!')
name = name.to_s.chop.to_sym if raise_when_not_found
keys = name.to_s.sub(/^find_all_by_/, "").split("_and_")
# replace asociation assignments with their property values if possible
keys.each.with_index do |key, i|
if properties.find{|p| p.name.to_sym == key.to_sym}.is_a?(SimplyBased::Model::BelongsTo::Property)
keys[i] = "#{keys[i]}_id"
end
end
view_name = name.to_s.sub(/^find_all_/, "").to_sym
count_name = name.to_s.sub(/^find_all_/, 'count_').to_sym
view_keys = keys.length == 1 ? keys.first : keys
without_deleted_view_name = "#{view_name}_withoutdeleted"
without_deleted_view_keys = keys + [:deleted_at]
unless respond_to?(view_name)
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(view_name, key: view_keys)
end
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(without_deleted_view_name, key: without_deleted_view_keys)
end
# Pre-define the matching count_by_* method so the generated finder's
# total_entries lookup (send(count_name, ...)) resolves to a real method
# instead of re-entering method_missing on first use.
# NOTE: respond_to? returns true via respond_to_missing? even before
# the method is defined, so we must check method_defined? instead.
_define_count_by(count_name, define_only: true) unless method_defined?(count_name) || private_method_defined?(count_name)
if raise_when_not_found
(class << self; self end).instance_eval do
define_method(:"#{name}!") do |*key_args|
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
with_pagination_options(options.update(total_entries: send(count_name, *key_args))) do |options|
options.assert_valid_keys(:with_deleted, :limit, :skip, :keys)
with_deleted = options.delete(:with_deleted)
key_args.map!{|a| a.is_a?(SimplyBased::Model) ? a.id : a}
options[:key] = key_args.first if key_args.size == 1
options[:key] = key_args if key_args.size > 1
options[:include_docs] = true
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size || options[:keys]
key_args.map!{|a| a.is_a?(SimplyBased::Model) ? a.id : a}
if soft_deleting_enabled? && !with_deleted
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
result = database.view(send(without_deleted_view_name, options))
else
result = database.view(send(view_name, options))
end
raise SimplyBased::RecordNotFound unless result && result.any?
result
end
end
end
send(:"#{name}!", *args)
else
(class << self; self end).instance_eval do
define_method(name) do |*key_args|
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
with_pagination_options(options.update(total_entries: send(count_name, *key_args))) do |options|
options.assert_valid_keys(:with_deleted, :limit, :skip, :keys)
with_deleted = options.delete(:with_deleted)
key_args.map!{|a| a.is_a?(SimplyBased::Model) ? a.id : a}
options[:key] = key_args.first if key_args.size == 1
options[:key] = key_args if key_args.size > 1
options[:include_docs] = true
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size || options[:keys]
if soft_deleting_enabled? && !with_deleted
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
database.view(send(without_deleted_view_name, options))
else
database.view(send(view_name, options))
end
end
end
end
send(name, *args)
end
end
def _define_count_by(name, *args, define_only: false)
keys = name.to_s.sub(/^count_by_/, "").split("_and_")
view_name = name.to_s.sub(/^count_/, "").to_sym
view_keys = keys.length == 1 ? keys.first : keys
without_deleted_view_name = "#{view_name}_withoutdeleted"
without_deleted_view_keys = keys + [:deleted_at]
unless respond_to?(view_name)
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(view_name, key: view_keys)
end
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
view(without_deleted_view_name, key: without_deleted_view_keys)
end
(class << self; self end).instance_eval do
define_method("#{name}") do |*key_args|
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
options.assert_valid_keys(:with_deleted, :keys)
with_deleted = options.delete(:with_deleted)
options[:key] = key_args.first if key_args.size == 1
options[:key] = key_args if key_args.size > 1
options[:reduce] = true
if soft_deleting_enabled? && !with_deleted
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
database.view(send(without_deleted_view_name, options))
else
database.view(send(view_name, options))
end
end
end
# define_only is used when another finder (find_all_by) just needs the
# method present; skip the immediate count query in that case.
send(name, *args) unless define_only
end
end
end
end