Files
simply_based/lib/simply_based/model/belongs_to.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

142 lines
6.0 KiB
Ruby

# Gracefully taken from CouchPotato after it has been removed for good.
module SimplyBased
module Model
module BelongsTo
def belongs_to(name, options = {})
check_existing_properties(name, SimplyBased::Model::BelongsTo::Property)
SimplyBased.assert_safe_view_token!(self.to_s, name, soft_delete_attribute)
association_property = if name.to_s.index('__')
# Already defined properly
name
elsif options[:class_name].present?
# Determine namespace and replace last argument with given name
name_hierarchy = options[:class_name].to_s.underscore.split(/\/|::/)
name_hierarchy[-1] = name
name_hierarchy.join('__')
elsif rindex = foreign_property.to_s.rindex('__')
# Make name based on current namespace
"#{foreign_property[0...rindex]}__#{name}"
else
# Just return the good old name
name
end
map_definition_without_deleted = <<-eos
function(doc) {
if (doc['ruby_class'] == '#{self.to_s}' && doc['#{name}_id'] != null) {
if (doc['#{soft_delete_attribute}'] && doc['#{soft_delete_attribute}'] != null){
// "soft" deleted
}else{
emit([doc.#{name}_id, doc.created_at], 1);
}
}
}
eos
reduce_definition = "_sum"
view "association_#{foreign_property}_belongs_to_#{association_property}",
:map_function => map_definition_without_deleted,
:reduce_function => reduce_definition,
:type => :custom,
:include_docs => true
map_definition_with_deleted = <<-eos
function(doc) {
if (doc['ruby_class'] == '#{self.to_s}' && doc['#{name}_id'] != null) {
emit([doc.#{name}_id, doc.created_at], 1);
}
}
eos
view "association_#{foreign_property}_belongs_to_#{association_property}_with_deleted",
:map_function => map_definition_with_deleted,
:reduce_function => reduce_definition,
:type => :custom,
:include_docs => true
properties << SimplyBased::Model::BelongsTo::Property.new(self, name, options)
end
class Property #:nodoc:
attr_accessor :name, :options
def initialize(owner_clazz, name, options = {})
@name = name
@options = {
:class_name => owner_clazz.find_association_class_name(name)
}.update(options)
@options.assert_valid_keys(:class_name)
owner_clazz.class_eval do
property :"#{name}_id"
alias_method :"#{name}_changed?", :"#{name}_id_changed?"
define_method name do |*args|
local_options = args.last.is_a?(Hash) ? args.last : {}
local_options.assert_valid_keys(:force_reload, :with_deleted)
forced_reload = local_options[:force_reload] || false
with_deleted = local_options[:with_deleted] || false
return instance_variable_get("@#{name}") unless instance_variable_get("@#{name}").nil? or forced_reload
if send("#{name}_id").present?
# Try to fetch the object. Does not have to be present. When relation dependency is ignore, the id remains.
# This will result in a id to a non existent object. Therefore the rescue for object
object = self.class.get_class_from_name(name).find(send("#{name}_id"), :with_deleted => with_deleted) rescue nil
instance_variable_set("@#{name}", object)
else
instance_variable_set("@#{name}", nil)
end
end
define_method "#{name}=" do |value|
klass = self.class.get_class_from_name(name)
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.nil? || value.is_a?(klass)
if value
# Has many object update
value_has_many_name = klass.properties.find{|p| p.is_a?(SimplyBased::Model::HasMany::Property) && p.options[:class_name] == self.class.name}.try(:name)
value.send(value_has_many_name) << self unless !value_has_many_name || value.send(value_has_many_name).include?(self)
# Has one object update
value_has_one_name = klass.properties.find{|p| p.is_a?(SimplyBased::Model::HasOne::Property) && p.options[:class_name] == self.class.name}.try(:name)
value.instance_variable_set("@#{value_has_one_name}", self) unless !value_has_one_name || value.send(value_has_one_name) == self
end
# Mark changed if appropriate
# send("#{name}_will_change!") if value != instance_variable_get("@#{name}") is not a persisted property, do not mark as changed
instance_variable_set("@#{name}", value)
if value.nil?
send("#{name}_id=", nil)
else
send("#{name}_id=", value.id)
end
end
define_method "#{name}_id=" do |new_foreign_id|
super(new_foreign_id)
value = instance_variable_get("@#{name}")
remove_instance_variable("@#{name}") if instance_variable_defined?("@#{name}") && new_foreign_id != value.try(:id)
end
end
end
def build(object, json)
object.send "#{name}_id=", json["#{name}_id"]
end
def serialize(json, object)
json["#{name}_id"] = object.send("#{name}_id") if object.send("#{name}_id")
end
alias :value :serialize
def association?
true
end
end
end
end
end