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>
189 lines
6.8 KiB
Ruby
189 lines
6.8 KiB
Ruby
module SimplyBased
|
|
module Model
|
|
module HasManyEmbedded
|
|
def has_many_embedded(name, options = {})
|
|
check_existing_properties(name, SimplyBased::Model::HasMany::Property)
|
|
properties << SimplyBased::Model::HasManyEmbedded::Property.new(self, name, options)
|
|
end
|
|
|
|
def define_has_many_embedded_getter(name, options)
|
|
# Make an alias to the property getter.
|
|
property_getter_method = "#{name}_property_getter"
|
|
alias_method property_getter_method, name
|
|
define_method(name) do |*args|
|
|
current = instance_variable_get("@#{name}")
|
|
if current && current.respond_to?(:parent_object_set?)
|
|
current.each { |o| o.parent_object = self }
|
|
return current
|
|
end
|
|
# Rebuild current, ensure parent object
|
|
current = Array.wrap(self.send(property_getter_method)).map do |h|
|
|
if h.is_a?(options[:class_name].constantize)
|
|
o = h
|
|
o.parent_object = self
|
|
else
|
|
o = options[:class_name].constantize.new
|
|
o._document = h
|
|
o.parent_object = self
|
|
end
|
|
o
|
|
end
|
|
def current.parent_object_set?
|
|
true
|
|
end
|
|
instance_variable_set("@#{name}", current)
|
|
return current
|
|
end
|
|
end
|
|
|
|
def define_has_many_embedded_setter(name, options)
|
|
define_method("#{name}=") do |values|
|
|
klass = self.class.get_class_from_name(name)
|
|
raise ArgumentError, "expected Array got #{values.class}" unless values.is_a?(Array)
|
|
instance_variable_set("@#{name}", []) unless instance_variable_get("@#{name}")
|
|
iid = 0
|
|
for value in values
|
|
if value.is_a?(Hash)
|
|
newval = klass.new
|
|
newval._document = value
|
|
newval.updated_at ||= Time.now
|
|
newval.created_at ||= Time.now
|
|
else
|
|
newval = value
|
|
end
|
|
newval.index = iid
|
|
instance_variable_get("@#{name}") << newval
|
|
iid += 1
|
|
end
|
|
current = instance_variable_get("@#{name}")
|
|
current.define_singleton_method(:parent_object_set?) { true }
|
|
save
|
|
end
|
|
end
|
|
|
|
def define_has_many_embedded_setter_add(name, options)
|
|
define_method("add_#{name.to_s.singularize}") do |value|
|
|
klass = self.class.get_class_from_name(name)
|
|
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
|
|
if value.is_a?(Hash)
|
|
newval = klass.new
|
|
newval._document = value
|
|
newval.updated_at ||= Time.now
|
|
newval.created_at ||= Time.now
|
|
else
|
|
newval = value
|
|
end
|
|
newval.index = (instance_variable_get("@#{name}") || []).size
|
|
instance_variable_get("@#{name}") << newval
|
|
save
|
|
end
|
|
end
|
|
|
|
def define_has_many_embedded_setter_remove(name, options)
|
|
define_method "remove_#{name.to_s.singularize}" do |value|
|
|
klass = self.class.get_class_from_name(name)
|
|
if value.is_a?(klass)
|
|
found = instance_variable_get("@#{name}").delete(value)
|
|
if found
|
|
self.send("reset_#{name}_index_values")
|
|
end
|
|
else
|
|
raise ArgumentError, "expected #{klass} got #{value.class}"
|
|
end
|
|
return save
|
|
end
|
|
end
|
|
|
|
def define_reset_index_values(name, options)
|
|
define_method "reset_#{name}_index_values" do
|
|
i = 0
|
|
for embedded_document in (instance_variable_get("@#{name}") || [])
|
|
embedded_document.index = i
|
|
i += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
def define_has_many_embedded_setter_remove_all(name, options)
|
|
define_method "remove_all_#{name}" do
|
|
all = send("#{name}", force_reload: true)
|
|
all.collect { |i| i }.each do |item|
|
|
send("remove_#{name.to_s.singularize}", item)
|
|
end
|
|
end
|
|
end
|
|
|
|
def define_has_many_embedded_count(name, options, through = nil)
|
|
method_name = name.to_s.singularize.underscore.gsub('/', '__') + '_count'
|
|
define_method(method_name) do |*args|
|
|
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
if local_options
|
|
local_options.assert_valid_keys(:force_reload, :with_deleted)
|
|
forced_reload = local_options[:force_reload]
|
|
with_deleted = local_options[:with_deleted]
|
|
else
|
|
forced_reload = false
|
|
with_deleted = false
|
|
end
|
|
|
|
if forced_reload || instance_variable_get("@#{method_name}").nil?
|
|
instance_variable_set("@#{method_name}", count_associated(through || options[:class_name], self.class, with_deleted: with_deleted, foreign_key: options[:foreign_key]))
|
|
end
|
|
instance_variable_get("@#{method_name}")
|
|
end
|
|
end
|
|
|
|
def set_parent_has_many_embedded_association_object(parent, child_collection)
|
|
child_collection.each do |child|
|
|
if child.respond_to?("#{parent.class.name.to_s.singularize.downcase}=")
|
|
child.send("#{parent.class.name.to_s.singularize.camelize.downcase}=", parent)
|
|
end
|
|
end
|
|
end
|
|
|
|
class Property < SimplyBased::Model::AssociationProperty
|
|
|
|
def build(object, json)
|
|
values = Array.wrap(json[name]).map do |v|
|
|
obj = if v.is_a?(Hash)
|
|
options[:class_name].constantize.new.tap { |o| o._document = v }
|
|
else
|
|
v
|
|
end
|
|
obj.parent_object = object
|
|
obj
|
|
end
|
|
values.define_singleton_method(:parent_object_set?) { true }
|
|
values.each_with_index { |v, i| v.index = i }
|
|
object.instance_variable_set("@#{name}", values)
|
|
end
|
|
|
|
def initialize(owner_clazz, name, options = {})
|
|
options = {
|
|
dependent: :nullify,
|
|
through: nil,
|
|
class_name: name.to_s.singularize.camelize,
|
|
foreign_key: nil
|
|
}.update(options)
|
|
@name, @options = name, options
|
|
|
|
options.assert_valid_keys(:dependent, :through, :class_name, :foreign_key)
|
|
|
|
owner_clazz.class_eval do
|
|
property name, type: Array
|
|
_define_cache_accessors(name, options)
|
|
define_has_many_embedded_getter(name, options)
|
|
define_has_many_embedded_setter(name, options)
|
|
define_has_many_embedded_setter_add(name, options)
|
|
define_has_many_embedded_setter_remove(name, options)
|
|
define_has_many_embedded_setter_remove_all(name, options)
|
|
define_reset_index_values(name, options)
|
|
define_has_many_embedded_count(name, options)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|