I've been doing RoR for years, but this is my first project with Mongo (also my first api-only project). I'm having a rough time with HABTM associations and I suspect it has to do with params, but I'm not sure what else to try.
Here's what I've got:
class Project
include Mongoid::Document
field :name, type: String
field :start_date, type: Date
field :target_date, type: Date
has_and_belongs_to_many :users
end
class User
include Mongoid::Document
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:jwt_authenticatable, jwt_revocation_strategy: JWTBlacklist
field :email, type: String
field :_id, type: String, default: ->{ email }
{ ... devise stuff ...}
has_and_belongs_to_many :projects
end
In my projects controller, I have this for parameters:
def project_params
params.permit(:id, :name, :start_date, :target_date, :description, user_ids: [])
end
And yes, I've also tried doing {user_ids: []}.
When I use Postman to make a URL PUT request to attempt to add users to a project, I get an "unpermitted parameter" error. But ... I'm permitting that parameter, right?
I'm kind of going nuts because I don't know if I have a Mongo problem, a Rails 5 problem, or an API problem. All other calls are working fine.
Started PUT "/rapi/projects/3" for 127.0.0.1 at 2017-02-15 22:55:10 -0500
Overwriting existing field _id in class JWTBlacklist.
Overwriting existing field _id in class User.
Processing by Api::V1::ProjectsController#update as JSON
Parameters: {"user_ids"=>"test#gmail.com", "id"=>"3"}
MONGODB | localhost:27017 | anthem.find | STARTED | {"find"=>"users", "filter"=>{"_id"=>"test#gmail.com"}}
MONGODB | localhost:27017 | anthem.find | SUCCEEDED | 0.000363703s
Overwriting existing field _id in class Project.
MONGODB | localhost:27017 | anthem.find | STARTED | {"find"=>"projects", "filter"=>{"_id"=>"3"}}
MONGODB | localhost:27017 | anthem.find | SUCCEEDED | 0.000244022s
Unpermitted parameters: user_ids, format
Overwriting existing field _id in class Release.
Completed 204 No Content in 20ms
I'd appreciate any ideas about what else I might try.
But ... I'm permitting that parameter, right?
Not quite. You're permitting user_ids as an array. But you send it as a scalar value. This is enough difference for strong params to not let the data through.
Make up your mind and do either one (permit array and send array) or the other (permit scalar and send scalar).
I think you should use devise_parameter_sanitizer.permit
devise_parameter_sanitizer.permit(:id, :name, :start_date, :target_date, :description, keys: [:username])
Related
When I do in rails console:
User.first
=> #<User _id: 6241f97de64d1eb2cc003d08,....
I get document, but when I try to find it by id like
User.find('6241f97de64d1eb2cc003d08') or User.find(BSON::ObjectId('6241f97de64d1eb2cc003d08')) or
User.find('6241f97de64d1eb2cc003d08'.to_bson) etc
I get:
message:
Document(s) not found for class User with id(s) 6241f97de64d1eb2cc003d08.
also
3.0.3 :049 > User.where(_id: BSON::ObjectId('6241f97de64d1eb2cc003d08')).find
=> nil
Why it does not work????
I use rails 7 and mongoid
Finally found the solution.
User model was defined like this:
class User
include Mongoid::Document
include Mongoid::Timestamps
field :_id, type: String
field :first_name, type: String
field :last_name, type: String
end
And changing it to this:
class User
include Mongoid::Document
include Mongoid::Timestamps
# field :_id, type: String
field :first_name, type: String
field :last_name, type: String
end
Commenting "field: _id" did the trick. I think string type was "covering" expected ObjectId as _id and find methods are converting strings into ObjectId so we ended up in search against string vs ObjectId.
I have a document with an embedded list of sub-docs. How do I update/change one particular document in the embedded list with Ecto?
defmodule MyApp.Thing do
use MyApp.Model
schema "things" do
embeds_many :users, User
end
end
defmodule MyApp.User do
use MyApp.Model
embedded_schema do
field :name, :string
field :email, :string
field :admin, :boolean, default: false
end
end
defmodule MyApp.Model do
defmacro __using__(_) do
quote do
use MyApp.Web, :model
#primary_key {:id, :binary_id, autogenerate: true}
#foreign_key_type :binary_id # For associations
end
end
end
My solution so far is to generate a list of all users except the one I want to update and make a new list of the one user's changeset and the other users and then put_embed this list on the thing. It works but it feels like there must be a more elegant solution to this.
user = Enum.find(thing.users, fn user -> user.id == user_id end)
other_users = Enum.filter(thing.users, fn user -> user.id != user_id end)
user_cs = User.changeset(user, %{email: email})
users = [user_cs | other_users]
thing
|> Ecto.Changeset.change
|> Ecto.Changeset.put_embed(:users, users)
|> Repo.update
EDIT: I just discovered a serious pitfall with this "solution". The untouched users get updated as well which can be a problem with concurring calls (race condition). So there has to be another solution.
I've just started working Elixir & Phoenix today, i am trying to add Ecto as a mapper, but i'm having some trouble using time.
This is my model.
schema "users" do
field :name, :string
field :email, :string
field :created_at, :datetime, default: Ecto.DateTime.local
field :updated_at, :datetime, default: Ecto.DateTime.local
end
I'm trying to set the created_at and updated_at per default, but when i try to compile this, i get the following error.
== Compilation error on file web/models/user.ex ==
** (ArgumentError) invalid default argument `%Ecto.DateTime{day: 13, hour: 19, min: 47, month: 2, sec: 12, year: 2015}` for `:datetime`
lib/ecto/schema.ex:687: Ecto.Schema.check_default!/2
lib/ecto/schema.ex:522: Ecto.Schema.__field__/4
web/models/board.ex:9: (module)
(stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
There is not much help to get in the documentation, what would be the correct way to do this?
Defaults fields names are :inserted_at and :updated_at but you can merge with your own field names, passing a keyword list
schema "users" do
field :name, :string
field :email, :string
timestamps([{:inserted_at,:created_at}])
end
:datetime is the native Postgres data type for, well a datetime; this data type maps to a two-elements Elixir tuple ({{yy, mm, dd}, {hh, mm, ss}}). An %Ecto.DateTime{} struct is not a two-elements tuple, hence the compilation error.
You may want to set the type of your fields to Ecto.DateTime, it should all work seamlessly.
Here is the relevant documentation about primitive types and non-primitive types.
PS you may also want to have a look at Ecto.Schema.timestamps/1, which is macro that expands to basically what you wrote manually (it adds the created_at and updated_at fields and it let's you choose what type they should be, defaulting to Ecto.DateTime):
schema "users" do
field :name, :string
field :email, :string
timestamps
end
You could also consider having the default not be in the schema, but in the migration: "created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP"
I am attempting to order the results of a query by the value of a specific embedded document, but even with what seems to be a valid set of options and using the $elemMatch operator, my results are coming back in natural order.
My model is composed of Cards, which embeds_many :card_attributes, which in turn reference a specific CardAttributeField and contain an Integer value. I would like to be able to order a collection of Cards by that value.
I am able to isolate a collection of Cards which have a CardAttribute referencing a specific CardAttributeField like this:
cards = Card.where(:card_attributes.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
If I knew the order in which the card_attributes were set, I could use MongoDB array notation, like this:
cards.order_by(['card_attributes.0.value', :asc])
This does deliver my expected results in test scenarios, but it won't work in the real world.
After much messing around, I found a syntax which I thought would allow me to match a field without using array notation:
cards.asc(:'card_attributes.value'.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
This produced a set of options on the resulting Mongoid::Criteria which looked like:
{:sort=>{"{#<Origin::Key:0x2b897548 #expanded=nil, #operator=\"$elemMatch\", #name=:\"card_attributes.value\", #strategy=:__override__, #block=nil>=>{:card_attribute_field_id=>\"54c6c6fe2617f55611000068\"}}"=>1}}
However, the results here came back in the same order regardless or whether I called asc() or desc().
Is there any way to do what I'm after? Am I taking the wrong approach, or do I have a mistake in my implementation? Thanks.
Simplified, my model is:
class Card
include Mongoid::Document
# various other fields
has_many :card_attribute_fields
embeds_many :card_attributes do
def for_attribute_field card_attribute_field
where(:card_attribute_field_id => card_attribute_field.id)
end
end
end
class CardAttributeField
include Mongoid::Document
belongs_to :card
field :name, type: String
field :default_value, type: String
field :description, type: String
end
class CardAttribute
include Mongoid::Document
embedded_in :card
field :card_attribute_field_id, type: Moped::BSON::ObjectId
field :value, type: Integer
end
I want to modify Devise to make it work with a users table with a UUID primary key with PostgreSQL.
Here is the migration:
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table :users, id: false do |t|
t.uuid :uuid, null: false
# ...
end
change_table :users do |t|
t.index :uuid, unique: true
# ...
end
end
def migrate(direction)
super
if direction == :up
# This is only necessary because the following does not work:
# t.uuid :uuid, primary: true, null: false
execute "ALTER TABLE users ADD PRIMARY KEY (uuid);"
end
end
end
Here is the User model:
class User < ActiveRecord::Base
primary_key = :uuid
devise :database_authenticatable, :recoverable, :registerable,
:rememberable, :trackable, :validatable
validates :uuid, presence: true
before_validation :ensure_uuid
def ensure_uuid; self.uuid ||= SecureRandom.uuid end
end
Here is the error:
PG::Error: ERROR: operator does not exist: uuid = integer
LINE 1: ...ECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER BY "users"."uuid" ASC LIMIT 1
Extracted source (around line #5):
1 .navbar-inner
2 .container
3 = a_nav_tag "App", root_path
4 - if user_signed_in?
5 %ul.nav.pull-right
6 %li.dropdown#user_menu
7 %a.dropdown-toggle(data-toggle="dropdown" href="#")
As you can see above, user_signed_in? is broken. I expect there are several changes needed to move from a 'normal' auto-incrementing ID to a UUID.
For now, I'm just posting the question. I'll take a swing at this later today. If you happen to know how to do this -- or know of a Devise fork, I'd appreciate it.
I've done this in Rails 4 simply by making the id column a uuid data type when creating the table, and no other configuration changes whatsoever. ie. do not create a column named 'uuid', just change the type of the 'id' column to uuid.
Just clear your browser's cookie for the web app (in my case, localhost). The error above is caused because the session was retaining the old user primary key, 1.
After that, things work in my testing. I hope this isn't just luck, it would be a good design if Devise was agnostic about the primary key. (In Devise's code, I saw no use of .id except in some tests.)
2020 answer:
when creating the users table, set the ID as uuid
def change
enable_extension 'pgcrypto' # needed if not already enabled
create_table :users, id: :uuid do |t|
t.string :email,
...