Created
February 9, 2018 17:29
-
-
Save Startouf/91d3b5eab004f04dfbb0d068b311ff3e to your computer and use it in GitHub Desktop.
Mongoid adapter for jsonapi_suite
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) | |
scope.to_a | |
end | |
def filter(scope, attribute, value) | |
scope.where(attribute => value) | |
end | |
# @Override using Mongoid's #asc and #desc | |
def order(scope, attribute, direction) | |
scope.public_send(direction, attribute) | |
end | |
# @Override | |
def paginate(scope, current_page, per_page) | |
scope.page(current_page).per(per_page) | |
end | |
# @Override | |
def count(scope, _attr) | |
scope.count | |
end | |
# @Override | |
# No transaction mechanism in Mongoid :'(' | |
def transaction(_model_class) | |
yield | |
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 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 | |
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) } | |
child_scope.in(primary_key => children_ids.uniq.compact) | |
end | |
assign do |parents, children| | |
foreign_key ||= infer_foreign_key(parents, association_name) | |
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 | |
# 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
Do you recall if this is working okay?