Representing Elixir/Ecto associations in GraphQL with absinthe - graphql

I've added a many to many association to a couple of my models and it seems to work fine in isolation--meaning w/o the GraphQL schema declarations. Here is the code for one of my models:
use Ecto.Schema
import Ecto.Changeset
alias Trader.Collect.Card
schema "users" do
field(:email, :string)
field(:first_name, :string)
field(:last_name, :string)
field(:password, :string)
field(:username, :string)
many_to_many(:cards, Card, join_through: "user_cards")
timestamps()
end
#doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:first_name, :last_name, :email, :username, :password])
|> validate_required([:first_name, :last_name, :email, :username, :password])
end
end
And here is the GraphQL type declaration:
defmodule TraderWeb.Schema.Types.User do
use Absinthe.Schema.Notation
#desc "User model representation"
object :user do
field(:id, non_null(:id))
field(:first_name, non_null(:string))
field(:last_name, non_null(:string))
field(:username, non_null(:string))
field(:email, non_null(:string))
field(:password, non_null(:string))
# field(:cards, list_of(:card), resolve: assoc(:cards))
end
end
This is the top level schema definition for the Absinthe/GraphQL part:
defmodule TraderWeb.Schema.Schema do
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
# Import Types individually here
import_types(TraderWeb.Schema.Types.{
User,
Card,
CardSet
})
# import queries here
import_types(TraderWeb.Schema.Queries.{
User,
Card,
CardSet
})
query do
import_fields(:user_queries)
import_fields(:card_queries)
import_fields(:card_set_queries)
end
end
Please note the cards field is commented out in the Type. Everything works fine in this case, however, if I uncomment that cards field, I get the following error:
== Compilation error in file lib/trader_web/schema/types/user.ex ==
** (CompileError) lib/trader_web/schema/types/user.ex:12: undefined function assoc/1
(elixir) src/elixir_locals.erl:108: :elixir_locals."-ensure_no_undefined_local/3-lc$^0/1-0-"/2
(elixir) src/elixir_locals.erl:108: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
(elixir) lib/kernel/parallel_compiler.ex:229: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
I've googled this issue pretty aggressively and can't find anything for this. It's unclear where that assoc function even lives--is it an ecto thing? or an absinthe-ecto thing? I also found sample code somewhere using dataloader but I couldn't get that working at all.
I appreciate any thoughts and ideas you all have!
thanks

You will either need the (deprecated) Absinthe.Ecto package, or work with the new Dataloader. There is a section in the documentation for Absinthe on Ecto best practices, which describes the new syntax for using dataloader https://hexdocs.pm/absinthe/ecto.html#dataloader. As that also requires additions to your context, it would be too much to add the complete setup here, but the docs are pretty good.

Related

struct__/1 is undefined, cannot expand struct error Phoenix 1.3

I'm trying to create a contact form in a phoenix 1.3 app. I used mix phx.gen.html to create the relevant files. However, I'm getting a compilation error when trying to start the server:
== Compilation error on file lib/iotc/web/controllers/email_controller.ex ==
** (CompileError) lib/iotc/web/controllers/email_controller.ex:7: Email.__struct__/1 is undefined, cannot expand struct Email
(stdlib) lists.erl:1354: :lists.mapfoldl/3
lib/iotc/web/controllers/email_controller.ex:6: (module)
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
Looking at some other posts with a similar issue, it could be related to the alias, but I have alias Itoc.Contact in the controller, and I don't think alias Iotc.Contact.Email would be right here.
email_controller.ex
defmodule Iotc.Web.EmailController do
use Iotc.Web, :controller
alias Iotc.Contact
def index(conn, _params) do
changeset = Email.changeset(%Email{})
emails = Contact.list_emails()
render(conn, "index.html", emails: emails, changeset: changeset)
end
...
email.ex
defmodule Iotc.Contact.Email do
use Ecto.Schema
import Ecto.Changeset
alias Iotc.Contact.Email
schema "contact_emails" do
field :email, :string
field :event, :string
field :message, :string
field :name, :string
timestamps()
end
#doc false
def changeset(%Email{} = email, attrs) do
email
|> cast(attrs, [:name, :email, :message, :event])
|> validate_required([:name, :email, :message, :event])
end
end
With respect to this
OK makes sense. I've updated it to the controller to : changeset =
Contact.Email.changeset(%Contact.Email{}) But I now get: warning:
function Iotc.Contact.Email.changeset/1 is undefined or private. Did
you mean one of: * changeset/2
You only have one function changeset/2 defined in the Email module.
But you're doing Contact.Email.changeset(%Contact.Email{}) passing only one argument. Do ``Contact.Email.changeset(%Contact.Email{})` and it should work.
The /2 part of the signature tells you the arity of the function, namely how many arguments takes.

Thin controller / Thick model in Phoenix/Ecto

I'm trying to figure out where to place common functions that I would normally (in Rails/ActiveRecord) put in a model class. Specifically, I have User and Company with a many-to-many relationship between them, but a user has a default_company, which just has a boolean flag on the user_companies join table.
ActiveRecord
class User < ActiveRecord::Base
belongs_to :user_companies
has_many :companies, through: :user_companies
def default_company
# Filter through companies to find the one that I want
end
end
(Note, there's probably an even easier way to do it, but this is the basic idea.)
Ecto
I could do something similar in Ecto, like so:
defmodule MyApp.User do
use MyApp.Web, :model
alias MyApp.{Company, CompaniesUser}
schema "users" do
has_many :companies_users, CompaniesUser, on_delete: :delete_all
many_to_many :companies, Company, join_through: "companies_users"
end
def default_company(%User{} = user) do
from(company in Company,
join: cu in CompaniesUser,
where: cu.company_id == company.id
and cu.user_id == ^user.id
and cu.default_company == true
) |> first() |> Repo.one()
end
end
However, based on my limited experience, this seems incorrect. All the examples I have seen keep the Ecto model very limited, just a bunch of changeset methods and some validation code, but strictly nothing business related. There is talk of keeping your business logic separate from your database logic. I get that and respect it, but most of the examples show putting raw Ecto queries inside a controller or otherwise sprinkling Ecto queries all over your app, and that seems wrong too.
Phoenix 1.3
From what I've read about the upcoming 1.3, it looks like the expectation is that this will be handled with Contexts, or specifically, modules that will allow you to logically group your Ecto schema models along with associated modules that define (manually: you define it) an API to access your persistence layer. So, using my above example, it would be something like:
defmodule MyApp.Account do
alias MyApp.Account.User
alias MyApp.Corporate.{Company, CompaniesUser}
def default_company(%User{} = user) do
from(company in Company,
join: cu in CompaniesUser,
where: cu.company_id == company.id
and cu.user_id == ^user.id
and cu.default_company == true
) |> first() |> Repo.one()
end
end
defmodule MyApp.Account.User do
use MyApp.Web, :model
alias MyApp.Corporate.{Company, CompaniesUser}
schema "users" do
has_many :companies_users, CompaniesUser, on_delete: :delete_all
many_to_many :companies, Company, join_through: "companies_users"
end
end
It has 2 modules, one (MyApp.Account.User) is my raw Ecto schema. The other (MyApp.Account) is the API/entry point for all the other logic in my app, like the controllers.
I guess I like the theory, but I'm worried about trying to figure out what models should go where, like in this example: Does Company belong in the Account context, or do I make a new Corporate context?
(Sorry for asking/answering my own question, but in researching the question I found the info for Phoenix 1.3 and thought I might as well just post for anyone who is interested.)

Validating Ecto "many-to-many" relationships

I'm trying to determine the right way to validate a many-to-many relationship in Ecto 2. I have a Conversation model that needs to have many members, and Users can be part of many conversations, so I've established the models like so:
# User Model
defmodule MyApp.User do
...
schema "users" do
....
many_to_many :conversations, Conversation, join_through: "conversations_users"
...
end
...
end
# Conversation Model
defmodule MyApp.Conversation do
...
schema "conversations" do
has_many :messages, Message
many_to_many :members, User, join_through: "conversations_users"
timestamps()
end
def changeset(struct, _params) do
struct
|> validate_member_count
end
defp validate_member_count(changeset) do
members = Repo.all(assoc(changeset, :members))
valid? = length(members) == 2
if valid? do
add_error(changeset, :members, "foo")
else
changeset
end
end
end
However, I just can't get this to work. I've written a simple test to verify that the validations run correctly, but I keep getting the following error:
# Test
test "fails to validate a conversation with less than two members" do
changeset = Conversation.changeset(%Conversation{}, %{})
{message, []} = changeset.errors[:members]
assert message === "must have at least two members"
end
** (FunctionClauseError) no function clause matching in Ecto.Changeset.add_error/4
I'm having a hard time understanding what I'm doing wrong. It seems like it can't find the function, but I've checked the documentation and it seems like Ecto.Changeset.add_error/4 is definitely right, and the arguments to it seem correct as well.
My best guess is that I need to do something in the validation before calling my custom validator, but I just don't know what I should do.
There are 2 mistakes:
You're passing a MyApp.Conversation to validate_member_count, not an Ecto.Changeset. You can convert an Ecto Schema defining Struct into an Ecto.Changeset using Ecto.Changeset.change/1:
def changeset(struct, _params) do
struct
|> change
|> validate_member_count
end
Ecto.assoc/2 accepts an Ecto Schema Struct, not an Ecto.Changeset. You can access the underlying struct from an Ecto.Changeset using .data:
members = Repo.all(assoc(changeset.data, :members))
Final code:
def changeset(struct, _params) do
struct
|> change
|> validate_member_count
end
defp validate_member_count(changeset) do
members = Repo.all(assoc(changeset.data, :members))
valid? = length(members) == 2
if valid? do
add_error(changeset, :members, "foo")
else
changeset
end
end

Mongoid association & null object pattern?

How would you implement the null object pattern on a Mongoid relation?
Class Owner
include Mongoid::Document
embeds_one :preference
end
Most owners won't have a preference, and thus I want them to have a NullPreference instead, as described in Ben Orenstein's excellent talk.
What I would like is something like this:
class NullPreference
def name
'no name'
end
end
owner = Owner.new
preference = owner.preference
preference.name
=> 'no name'
I found a related question regarding the same thing in ActiveRecord, no answers though.
Edit: I'm using Mongoid 2.6 otherwise I could've used autobuild: true and get a real Preference and use the defaults instead.
An obvious way is to build a layer of abstraction over that field.
class Owner
include Mongoid::Document
embeds_one :preference_field # internal field, don't use directly
def preference
preference_field || NullPreference.new
end
def preference= pref
self.preference_field = pref
end
end
Maybe there are simpler ways.

Rails -- self vs. #

I am following Michael Hartl's RoR tutorial, and it is covering the basics of password encryption. This is the User model as it currently stands:
class User < ActiveRecord::Base
attr_accessor :password
attr_accessible :name, :email,: password, :password_confirmation
email_regex = /^[A-Za-z0-9._+-]+#[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+[A-Za-z]$/
#tests for valid email addresses.
validates :name, :presence => true,
:length => {:maximum => 50}
validates :email, :presence => true,
:format => {:with => email_regex},
:uniqueness => {:case_sensitive => false}
validates :password, :presence => true,
:length => {:maximum => 20, :minimum => 6},
:confirmation => true
before_save :encrypt_password
private
def encrypt_password
self.encrypted_password = encrypt(password)
end
def encrypt(string)
string
end
end
I posted a previous question about before_save not working, and it turns out that what I had accidentally done is written my encrypt_password as:
def encrypt_password
#encrypted_password = encrypt(password)
end
I understand that if self.encrypted_password sets the encrypted_password attribute, but why does #encrypted_password not do that as well? In the response to the previous post about before_save not working someone said that the instance variable was "forgotten" after the method ended with the way I had originally coded it -- why was this the case? Can someone please explain how self and # work differently in the context of the code above?
NOTE: I already took a look at the posts here and here, but they both say that "self" is calling the attribute = method, and I don't even understand how that method could exist here since I never created it or declared the encrypted_password w/ attr_accessor. So I am still confused, and this is not a re-posting of those questions.
The accessors for encrypted_password have been automatically added by Rails for you because a field by that name exists in the users table.
Any field you add to a table will be automatically made available via self.field_name.
Here is where Michael Hartl's tutorial creates the encrypted_password field in the users table.
Also look at the user_spec.rb (Listing 7.3) in the linked page, where the author is testing for the presence of the encrypted_password field.
UPDATED:
As #mu points out, the # is used for Ruby instance variables (aka "iv"). But encrypted_password is an "attribute" defined by Rails, and is not an instance variable.
If you run User.find(1).instance_variables, you will see that there is an iv called #attributes, which is of type Hash.
Inside that iv is where the encrypted_password is stored. Rails has defined accessor methods for encrypted_password, which gets/sets the data for that
attribute in the #attributes Hash.
Note that you could also get/set the data via #attributes["encrypted_password"] called from within the User class (but the accessor methods are convenient way to do just that).
If you let me, I'd like to rephrase the answer.
I explained in this post, that as soon as you create a (rails-) Model with the same (singular) name as one of the (plural) tablenames of your database, the "magic" of rails will create setters and getters in order to modify your table's records.
This is because your model inherits all methods from the ActiveRecord::Base Class, which defines basic CRUD accessors (Create, Read, Update, Delete).
The key point related to your question, is that you don't know how rails implements the instance variable related to your database table column, And you shouldn't. :) All you have to know is that at that point, you have setters and getters available to CRUD (create, read, update, delete) your database column "encrypted_password".
In your example, maybe rails uses an instance variable called #encrypted_password, maybe rails uses an hash-instance-variable called #attributes["encrypted_password"], or maybe rails uses an instance variable called #you_will_never_guess_encrypted_password.
-
And that's a good point you don't know about the internal rails behavior with instance variables. In 2019 Rails further development may lead the framework to use #complicated-hash-instance-variable to store the encrypted_password value.
In fact the best approach is to let rails manage its "private" "affair" ;) with instance variables, and just use the getter and setter methods it provides to you.
So your application will still work with encrypted_password in the next century (I hope so ^^).
So if you use #encrypted_password it may work with some "imaginary" version of rails and it won't work anymore with other rails versions. Actually with a current version of rails it doesn't work.
-
The second key point is that when you want to use the getter "encrypted_password" Rails created for your encrypted_password database table column, you prefix it with "self" in order to tells Ruby : "ok I want to use the encrypted_password method of my User instance variable."
In Ruby, a method is called by passing its name to a receiver.
You write it like this :
my_receiver.my_method
In your case we pass the method encrypted_password to the User instance variable. But we don't know how this instance variable will be named, so we use the word self to tell Ruby : "I'm talking about any instance variable of the User class that calls the encrypted_password method".
For instance we could have named our instance variable "toto" :
toto = User.new
so toto.encrypted_password would display the encrypted password, and self in this very case in our code would reference toto.
However, thanks to Ruby, if you don't give any receiver when calling a method, Ruby will assume you pass it to self.
Reference : Pragmatic Programmer's guide
So in your example, you even don't need to put "self." as prefix.
You could have it written like this :
class User < ActiveRecord::Base
def encrypt_password
encrypted_password = encrypt(password)
end
end
I hope this helps to clarify this interesting subject.
TL;DR -
Always write self.widget_count = 123 if you intend to save widget_count back to the database.
(But please do read the long answers, as the reason why is valuable to know.)

Resources