Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jfrux/5c56b0c90cb7bb7efe6778ade0629d5a to your computer and use it in GitHub Desktop.
Save jfrux/5c56b0c90cb7bb7efe6778ade0629d5a to your computer and use it in GitHub Desktop.
Mongoid adapter for jsonapi_suite
# lib/jsonapi/adapters/transactionless_mongoid_adapter.rb
module Jsonapi
module Adapters
# Mongoid transactionless adapter
# See https://github.com/jsonapi-suite/jsonapi_compliable/blob/master/lib/jsonapi_compliable/adapters/abstract.rb
#
# @author [Cyril]
#
class TransactionlessMongoidAdapter < JsonapiCompliable::Adapters::Abstract
def sideloading_module
Jsonapi::Adapters::MongoidSideloading
end
# @override
# If we keep the default behavior, this returns a criteria
# and will mess up sideloading scopes
# jsonapi_suite resolve is meant to evaluate and not lazy-evaluate
def resolve(scope)
if scope && !scope.is_a?(Boolean) && !scope.is_a?(Array)
scope.to_a
else
scope
end
end
def filter(scope, attribute, value)
scope.where(attribute => value)
end
# @Override using Mongoid's #asc and #desc
def order(scope, attribute, direction)
puts scope
if scope && !scope.is_a?(Boolean) && !scope.is_a?(Array)
scope.public_send(direction, attribute)
else
scope
end
end
# @Override
def paginate(scope, current_page, per_page)
if scope && !scope.is_a?(Boolean) && !scope.is_a?(Array)
scope.page(current_page).per(per_page)
else
scope
end
end
# @Override
def count(scope, _attr)
if scope && !scope.is_a?(Boolean) && !scope.is_a?(Array)
scope.count
else
scope
end
end
# @Override
# No transaction mechanism in Mongoid :'('
def transaction(_model_class)
yield
end
# @Override
def destroy(model_class, id)
instance = model_class.find(id)
instance.destroy
instance
end
# @Override
def update(model_class, update_params)
instance = model_class.find(update_params.delete(:id))
instance.update_attributes(update_params)
instance
end
# @Override
def create(model_class, create_params)
puts "[mongoid_adapter] create:", create_params
instance = model_class.new(create_params)
instance.save
instance
end
# @Override
def associate(parent, child, association_name, association_type)
case association_type
when :has_many
parent.send(association_name).push(child)
when :belongs_to
child.send(:"#{association_name}=", parent)
when :habtm
# No such thing as child <-> parent in HABTM anyway
#
# For some reason `child.send(association_name)`
# is a Mongoid::Relations::Targets::Enumerable
# and seems to behave like a belongs_to/has
# child.send(association_name) << parent
when :embeds_one, :embeds_many
# Nested models are already associated :-)
else
raise "Define how to associate parent and child for #{association_type}!"
end
end
end
end
end
# lib/jsonapi/adapters/mongoid_sideloading.rb
module Jsonapi
module Adapters
# Mongoid Sideloading capabilities. Tightly coupled with Mongoid's semi-private internals
#
# @author [Cyril]
#
module MongoidSideloading
class UnresolvableSideloadingCriteria < RuntimeError; end
# Useful error message for when a default foreign key cannot be inferred
#
# @author [Cyril]
#
class UnresolvableForeignKey < RuntimeError
def initialize(association_name, resource_class)
@association_name = association_name
@resource_class = resource_class
end
def message
'Could not infer a default foreign key for the association ' +
"#{@association_name} on the resource '#{@resource_class}'. " \
'Try setting a :foreign_key param in the sideloading definition'
end
end
# Note : this module needs to know the internals of Mongoid especially related to laoding relations
# Unfortunately there's not a lot of documentation about this, best is to have a look directly at Mongoid's code
# Especially, have a look at
# - Mongoid Accessors : https://github.com/mongodb/mongoid/blob/master/lib/mongoid/association/accessors.rb
# in a nutshell, Mongoid uses instance variables @_{association_name} to memoize associations
# The goal is : Query by yourself, and make #needs_no_database_query? returns true
# It would seem using #set_relation does the thing !
# (equivalent of ActiveRecord's #loaded! method)
# Adapter to retrieve association metadata
# Used to infer various properties like foreign/local keys already defined in models
#
# @param resolved_scope [Array<?> or <?>] resolved scope containing models
# @param association_name [Symbol] Name of association
#
# @return [Hash] Metadata information
def association_metadata(resolved_scope, association_name)
@metadata ||= begin
if resolved_scope.is_a?(Array)
resolved_scope.first.class
else
resolved_scope.klass
end.relations[association_name.to_s]
end
end
def association_class
resource.config[:model]
end
def raise_if_no_model!(association_name)
raise RuntimeError, "Declare a 'model' in the resource #{resource.class.name} for sideloading !\n"\
"or a :foreign_key in the resource association for #{association_name}"
end
# Attempt to Infer a default child scope for a relation
# Note : may return bad results in case of polymorphic data types that would have different foreign ID, etc.
# (either use manual allow_sideload or specify :scope in that case)
#
# @param resolved_scope [Array<?>] the resolved parents
# @param association_name [Symbol]
#
# @return [Mongoid::Criteria<?>]
def infer_default_child_scope(resolved_scope, association_name)
inferred_scope = association_metadata(resolved_scope, association_name) \
&.klass&.all
raise UnresolvableSideloadingCriteria if inferred_scope.nil?
inferred_scope
end
# Attempt to infer the foreign key for a has_x relation
# @param association_name [Symbol]
# @raise [UnresolvableForeignKey]
#
# @return [Symbol] Foreign key attribute
def infer_foreign_key(resolved_scope, association_name)
foreign_key = association_metadata(resolved_scope, association_name)&.foreign_key
raise UnresolvableForeignKey.new(association_name, resource.class) if foreign_key.nil?
foreign_key
end
# @Override implementation for Mongoid, should be pretty similar to AR
def has_many(association_name, scope: nil, resource:, foreign_key: nil, primary_key: :id, &blk)
child_scope = scope
allow_sideload association_name, type: :has_many, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
scope do |parents|
foreign_key ||= infer_foreign_key(parents, association_name)
child_scope ||= infer_default_child_scope(parents, association_name)
parent_ids = parents.map { |p| p.send(primary_key) }.uniq.compact
child_scope.in(foreign_key => parent_ids)
end
assign do |parents, children|
foreign_key ||= infer_foreign_key(parents, association_name)
parents.each do |parent|
# parent.relations(association_name).loaded!
parent_identifier = parent.send(primary_key)
relevant_children = children.select { |child| child.send(foreign_key) == parent_identifier }
parent.set_relation(association_name, relevant_children)
end
end
instance_eval(&blk) if blk
end
end
# @Override implementation for Mongoid, should be pretty similar to AR
# Compared to has_many we just use `detect` instead of `select` when assignin children
def has_one(association_name, scope: nil, resource:, foreign_key: nil, primary_key: :id, &blk)
child_scope = scope
allow_sideload association_name, type: :has_one, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
scope do |parents|
foreign_key ||= infer_foreign_key(parents, association_name)
child_scope ||= infer_default_child_scope(parents, association_name)
parent_ids = parents.map { |p| p.send(primary_key) }.uniq.compact
child_scope.in?(foreign_key => parent_ids)
end
assign do |parents, children|
foreign_key ||= infer_foreign_key(parents, association_name)
parents.each do |parent|
# parent.relations(association_name).loaded!
parent_identifier = parent.send(primary_key)
relevant_children = children.detect { |child| child.send(foreign_key) == parent_identifier }
parent.set_relation(association_name, relevant_children)
end
end
instance_eval(&blk) if blk
end
end
# Mongoid embeds_many sideload still needs to filter embedded records according to the scope !!
# (Currently only works with no filtering )
#
# @param association_name [Symbol]
# @param scope: nil [Mongoid::Criteria]
# @param resource: nil [JsonapiCompliable::Resource]
#
# @return [void]
def embeds_many(association_name, scope: nil, resource:, foreign_key: nil, primary_key: :id, &blk)
# TODO : https://github.com/Startouf/MyJobGlasses/issues/1794
child_scope = scope
allow_sideload association_name, type: :embeds_many, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
scope do |parents|
child_scope ||= infer_default_child_scope(parents, association_name)
# No need to do anything more ! Embedded data are already loaded => no need to locate them
end
assign do |parents, _children|
parents.each do |parent|
# parent.embedded_relation returns a Mongoid::Criteria that can be merged with a root one
# ie professional.ratings.merge(Rating.desc(:rating)) WORKS !!
relevant_embedded_records = parent.send(association_name).merge(child_scope)
parent.set_relation(association_name, relevant_embedded_records)
end
end
instance_eval(&blk) if blk
end
end
# @Override implementation for Mongoid, should be pretty similar to AR
# @example
# conversation has_many :messages, message belongs_to :conversation
# => parent = message, child = conversation
# => foreign_key :conversation_id
# =>
# parent = conversation
# child = message
def belongs_to(association_name, scope: nil, resource:, foreign_key: nil, primary_key: :id, &blk)
# Fetch initial scope
child_scope = scope
# puts "child_scope: #{child_scope.to_json}"
allow_sideload association_name, type: :belongs_to, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
scope do |parents| # parents = conversations_criteria
foreign_key ||= infer_foreign_key(parents, association_name)
child_scope ||= infer_default_child_scope(parents, association_name)
children_ids = parents.map { |parent| parent.send(foreign_key) }
if child_scope
begin
child_scope.in(primary_key => children_ids.uniq.compact)
rescue
puts "child_scope.in is not defined on #{children_ids.uniq.compact}"
end
end
end
assign do |parents, children|
foreign_key ||= infer_foreign_key(parents, association_name)
parents.each do |parent|
parent_identifier = parent.send(foreign_key)
begin
relevant_child = children.find { |c| c.send(primary_key) == parent_identifier }
rescue
puts "children.find is not defined on #{children}"
ensure
relevant_child = nil
end
parent.set_relation(association_name, relevant_child)
end
end
end
end
# HABTM in Mongoid stores the keys locally in an array
# Memory : model.association_ids #=> [BSON::ObjectID('cafebabe'), BSON::ObjectID('badf00d'), ...]
# Serial : model.association_ids #=> ['cafebabe', 'badf00d', ... }
#
# @param association_name [Symbol] Association name
# @param scope: nil [type] [description]
# @param resource: [type] [description]
#
# @param foreign_key: [type] Accessor to the array of foreign keys
# @param primary_key: :id [Symbol] Primary key of the model to be found
# @param &blk [type] [description]
#
# @return [type] [description]
def has_and_belongs_to_many(association_name, scope: nil, resource:, foreign_key: nil, foreign_keys_key: nil, primary_key: :id, &blk)
child_scope = scope
foreign_keys_key ||= "#{association_name.to_s.singularize}_ids"
foreign_key = :id # compatibility reasons
allow_sideload association_name, type: :habtm, resource: resource, foreign_key: foreign_key, primary_key: primary_key do
scope do |parents|
# TODO : merge selectors with ORs (???)
child_scope ||= infer_default_child_scope(parents, association_name)
sideload_ids = parents.flat_map { |p| p.send(foreign_keys_key) }.uniq.compact
child_scope.in(id: sideload_ids)
end
assign do |parents, foreign_objects|
parents.each do |local_object|
# Assign foreign objects if their ID is in the ID list of the parent
local_keys = local_object.send("#{association_name.to_s.singularize}_ids")
relevant_foreign_objects = foreign_objects.select do |foreign_object|
local_keys.include?(foreign_object.send(foreign_key))
end
local_object.set_relation(association_name, relevant_foreign_objects)
end
end
end
instance_eval(&blk) if blk
end
# @Override implementation for Mongoid, should be pretty similar to AR
def polymorphic_belongs_to(association_name, group_by:, groups:, &blk)
allow_sideload association_name, type: :polymorphic_belongs_to, polymorphic: true do
group_by group_by
groups.each_pair do |type, config|
primary_key = config[:primary_key] || :id
foreign_key = config[:foreign_key] || :"#{association_name}_id"
allow_sideload type, primary_key: primary_key, foreign_key: foreign_key, type: :belongs_to, resource: config[:resource] do
scope do |parents|
parent_ids = parents.map { |parent| parent.send(foreign_key) }
parent_ids.compact!
parent_ids.uniq!
config[:scope].call.in(primary_key => parent_ids)
end
assign do |parents, children|
parents.each do |parent|
parent_identifier = parent.send(foreign_key)
relevant_child = children.find { |c| c.send(primary_key) == parent_identifier }
parent.set_relation(association_name, relevant_child)
end
end
end
end
instance_eval(&blk) if blk
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment