How do I use Absinthe Dataloader for many to many relationships - graphql

In my phoenix application, I have a many to many relationship between an Artist and a Cause schema implemented using a join table artists_causes. In my Artist schema, I have many_to_many :causes, Cause, join_through: "artists_causes" and in the Cause schema I have many_to_many :artists, Artist, join_through: "artists_causes"
I am using absinthe for graphql and in my CauseTypes module, I have implemented a the cause object as below
defmodule MyAppWeb.Schema.CauseTypes do
#moduledoc """
All types for causes
"""
use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 3]
object :cause do
field :id, :id
field :artists, list_of(:artist), resolve: dataloader(Artists)
end
def dataloader do
alias MyApp.{Artists, Causes}
loader = Dataloader.new
|> Dataloader.add_source(Causes, Causes.datasource())
|> Dataloader.add_source(Artists, Artists.datasource())
end
def context(ctx) do
Map.put(ctx, :loader, dataloader())
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
end
From my understanding, with Absinthe Dataloader, the dataloader/1 is what I need to have the list of Artists loaded. However, I am not able to query for the artists from within a cause getting the error artists: #Ecto.Association.NotLoaded<association :artists is not loaded> when I run the query below in graphiql
query{
causes{
id
artists {
id
}
}
}
Is there any little piece that I am missing on working with many to many relationships?
==========
Update
I updated my list_causes function as below
def list_causes do
Repo.all(MyApp.Causes.Cause)
end
to
def list_causes do
Repo.all(from c in Cause,
left_join: ac in "artists_causes", on: c.id == ac.cause_id,
left_join: a in Artist, on: a.id == ac.artist_id,
preload: [:artists]
)
end
and , I am now getting the error FunctionClauseError at POST /graphiql\n\nException:\n\n ** (FunctionClauseError) no function clause matching in anonymous fn/3 in Absinthe.Resolution.Helpers.dataloader/1 which maybe pointing towards with the Absinthe.Resolution.Helpers.dataloader/1 method. I have the helpers imported Is there something else I could be missing?

I think you must preload relation with artists manualy from Ecto, before passing it to Absinthe.
For example, fetch causes like:
from(c in Cause,
preload: [:artists],
select: c
)
|> Repo.all()
ADDITIONAL
My way of resolving Absinthe query.
In query object I pass resolver module function reference.
resolve(&App.Resolver.get_all_causes/2)
And with resolver function I return dataset
def get_all_causes(_params, _info) do
{:ok,
from(c in Cause,
preload: [:artists],
select: c
)
|> Repo.all()}
end

Related

Ruby Sequel STI model_map or key_map issue with DB queries

I have a module with a 'parent' class, Trait, and its 'children' inherited classes, Text and Image, using the Ruby Sequel gem with Postgresql within a module, Tank.
module Tank; end
module Tank
class Trait < Sequel::Model
plugin :single_table_inheritance, :type
end
end
module Tank
class Image < Trait; end
end
module Tank
class Text < Trait; end
end
While within a console, I attempt to call #destroy on a trait. This results in an error because Sequel attempts to find a Trait with #type Tank::Text as shown below.
> Trait[439555].destroy
Sequel::NoExistingObject: Attempt to delete object did not result in
a single row modification (Rows Deleted: 0, SQL: DELETE FROM "traits"
WHERE (("traits"."type" IN ('Tank::Text')) AND ("id" = 439555)))
I have tried using some of the advanced options; #model_map, #key_map, and #key_chooser as seen in the Sequel documentation: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/SingleTableInheritance.html.
Admittedly I am not so familiar with lambdas and procs, but this was my attempt:
module HoldingTank
class Trait < Sequel::Model
plugin :single_table_inheritance, :type,
model_map: { 'Text'=>:Text, 'Image'=>:Image }
end
end
and I tried adding the following when that did not work:
key_chooser: lambda {|i| i.model.sti_key_map[i.model.to_s.split('::').last].first}
with both tries having similar results to the following, where now it is searching for rows where 1 = 0 (obviously something went wrong):
> Trait[439555].destroy
Sequel::NoExistingObject: Attempt to delete object did not result in
a single row modification (Rows Deleted: 0, SQL: DELETE FROM "traits"
WHERE ((1 = 0) AND ("id" = 439555)))
For what it's worth, the Trait class already existed as entirely Text entries before I added STI. I updated all the current Trait instances to have #type = 'Text' which I have confirmed in the console. I also get these results and I don't know if they are to be expected or not as I am new to Sequel and STI:
> Text.count
=> 0
> Trait.where(type: 'Text').count
=> 931589
Thanks in advance for any assistance.

How to check if value exists in params.permit?

I can't seem to figure out how to accomplish what I am trying to do here on my create method.
What I have right now works if there are no values, the item is deleted. However, if 1 or more param values exist, it passes and is saved. Not what I needed. I need an all or nothing scenario. I want to save only if all the permitted keys have their value. params.permit(:name, :description, :copyright)
Before an entry is saved using organizations.save!, I need to make sure none of the params that are permitted are nil or empty.
I search all over and can't seem to narrow down on an answer to my exact issue.
Here is my code:
class OrganizationsController < ApplicationController
def index
query_params = params.permit(:id, :name,)
if query_params.blank?
organizations = Organization.all
else
organizations = Organization.where(query_params)
end
render json: organizations, root: "organizations"
end
def create
organizations = Organization.new(organization_params)
if organization_params.present?
organizations.delete
else
organizations.save!
render json: organizations
end
end
private
def organization_params
params.permit(:name, :description, :copyright)
end
end
You should add validations to your model.
From your question i understand that you want to save details only if you get values in all the field, if not you don't want to save, right?. If yes, then adding validations to your model will give you what you wanted.
Add the following to your organization model
validates_presence_of :name
validates_presence_of :description
validates_presence_of :copyright
by doing so, the user won't be allowed to save the details unless and until all three fields have some value in it.
There is no need to use delete as the incomplete information will not be saved.
for more and advanced info click here
To check none of the values of organization_params hash is empty, you can do something like this:
organization_params.values.all? { |x| !x.empty? }
or, this:
organization_params.all? { |k,v| !v.empty? }
You can also check if any param value is empty:
organization_params.any? { |k,v| v.empty? }
So, your create method can be re-written as:
def create
organizations = Organization.new(organization_params)
if organization_params.any? { |k,v| v.empty? }
# at least one param is empty, so delete the record
organizations.delete
else
# all the params values are present, so save the record
organizations.save!
render json: organizations
end
end

Changing ID to something more friendly

When creating a record the URL generated to view that record ends with its id
/record/21
I would like to be able to change that to something easier to read, such as my name and reference attributes from the model. I have looked at friendly_id but has trouble implementing a custom method to generate the URL
class Animal < ActiveRecord::Base
extend FriendlyId
friendly_id :name_and_ref
def name_and_ref
"#{name}-#{reference}"
end
end
I ended up getting an error
PG::UndefinedColumn: ERROR: column animals.name_and_ref does not exist LINE 1: SELECT "animals".* FROM "animals" WHERE "animals"."name_an... ^ : SELECT "animals".* FROM "animals" WHERE "animals"."name_and_ref" = 'Clawd-A123456' ORDER BY "animals"."id" ASC LIMIT 1
def show
#animal = Animal.friendly.find(params[:id])
end
I then come across the to_param method which Rails has available, in my model I have
def to_param
"#{self.id}-#{self.name}"
end
which will generate a URL for me of
/19-clawd
This works, but when I do the following it throws an error
def to_param
"#{self.name}-#{self.reference}"
end
My question though is how can I generate the URL to be name and reference without it throwing
Couldn't find Animal with 'id'=Clawd-A123456
If you would like to use your own "friendly id" then you'll need to adjust the find statement in your controller to something like
id = params[:id].split(/-/, 2).first
#animal = Animal.find(id)
Similarly, for the name/reference combination
name, reference = params[:id].split(/-/, 2)
#animal = Animal.find_by(name: name, reference: reference)
The second choice is a little more difficult because you'll have to do some work in the model to guarantee that the name/reference pair is unique.
The easiest way, is to go with friendly_id and simply add the missing database column. Keep in mind that you will need to ensure this new column is unique for every record. It basically acts as primary key.

ActiveRecord conditions across multiple tables.

I'm trying to retrieve cars from my database where each car has a manufacturer, and can have multiple styles.
For example, a ford fiesta is a coupe, sedan and hatch.
I've got my relationships set-up in my models, but now I want to create a query to return the results. The query construction will depend on what parameters are supplied.
This is what I've got so far
conditions = {}
conditions[:manufacturer_id] = params[:manufacturer_id] unless params[:manufacturer_id].blank? # this works!
conditions[:style_id] = "style_id IN (?)", params[:style_ids] unless params[:style_ids].blank? #this breaks it :(
cars = Car.find(:all, :conditions=> conditions)
return render :json => cars
The error getting returned is
PG::Error: ERROR: column cars.style_ids does not exit of course this is because the style_id is in a join table called cars_styles. Is there a way to tell ActiveRecord which table to look for within the condition?
The key thing here is that I want to only have one controller method which takes the params in existence and then creates the right query. So if I don't have a manufacturer_id, it will only query the styles, or if vice versa. Of course, I'll be adding other params later too.
I ended up doing this with scoped queries like this
scope :from_manufacturer, lambda{|manu|{:joins => :manufacturer, :conditions => "manufacturers.id = #{manu}" }}
scope :from_style, lambda{|style|{:joins => :style, :conditions => "styles.id = #{style}"}}
def self.get_cars(params)
scope = self
[:manufacturer,:style].each do |s|
scope = scope.send("from_#{s}", params[s]) if params[s].present?
end
scope
end
Works great!

List dynamic attributes in a Mongoid Model

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

Resources