Can you have an association based on a JSONB column in Rails? - ruby-on-rails

I have a Rails app (rails v6.0.3, ruby 2.7.1) that is using the Noticed gem to send notifications. I have the following model configuration:
class Vendor < ApplicationRecord
has_noticed_notifications
end
The has_noticed_notifications is, as described in their README, a "Helper for associating and destroying Notification records where(params: {param_name.to_sym => self})"
So when I create a Notification like so...
VendorAddedNotification.with(
vendor: vendor,
data_source: "user",
).deliver(some_user) # => Notification inserted!
I expect to be able to find the Notifications that reference the vendor, using the Noticed method, like so:
vendor = Vendor.find ...
vendor.notifications_as_vendor # => Expected: [ Notification#123 ]
However, the input is always an empty array (Actual => [])
I looked at their source code and it looks like notifications_as_vendor is the following query:
Notification.where(params: { :vendor => self }) # where self = an instance of the Vendor model
However, that doesn't seem to work, and I'm not sure if it's supposed to or not. I tried running a simpler query to see if it worked ...
Notification.where(params: { :data_source => "user" })
But that did not work either. However, when I ran the same query with a different signature, it did:
Notification.where("params->>'data_source' = ?", "user")
So my question is-- is this Notified's mistake, or am I missing something in my configuration? I'm using PSQL for this, here is the relevant schema:
...
create_table "notifications", force: :cascade do |t|
t.string "recipient_type", null: false
t.bigint "recipient_id", null: false
t.string "type", null: false
t.jsonb "params"
t.datetime "read_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["read_at"], name: "index_notifications_on_read_at"
t.index ["recipient_type", "recipient_id"], name: "index_notifications_on_recipient_type_and_recipient_id"
end
...
And here are the related models:
class VendorAddedNotification < Noticed::Base
deliver_by :database
param :vendor
param :data_source
end
class Notification < ApplicationRecord
include Noticed::Model
belongs_to :recipient, polymorphic: true
end
Thank you in advance!

I've found why it's not working, it seems to be an issue with Notified.
In plain SQL I ran:
# PLAIN SQL
select "params" from "notifications" limit 1
Which returns the notification's params (returned notifcation's id=77)
# PLAIN SQL Result
"{""added_by"": {""_aj_globalid"": ""gid://stack-shine/WorkspaceMember/269""}, ""data_source"": ""user"", ""_aj_symbol_keys"": [""workspace_vendor"", ""data_source"", ""added_by""], ""workspace_vendor"": {""_aj_globalid"": ""gid://stack-shine/WorkspaceVendor/296""}}"
Now in Rails when I do
vendor = Notification.find(77).params[:vendor]
vendor.notifications_as_vendor.to_sql
The result is ...
"SELECT \"notifications\".* FROM \"notifications\" WHERE \"notifications\".\"params\" = '{\"vendor\":{\"_aj_globalid\":\"gid://stack-shine/Vendor/296\"},\"_aj_symbol_keys\":[\"vendor\"]}'"
... the extracted params from that query are:
'{\"vendor\":{\"_aj_globalid\":\"gid://stack-shine/Vendor/296\"},\"_aj_symbol_keys\":[\"vendor\"]}'
So ... In the database, the serialized params are A, but Rails is search for B:
# A: `params` In the database
"{""added_by"": {""_aj_globalid"": ""gid://stack-shine/WorkspaceMember/269""}, ""data_source"": ""user"", ""_aj_symbol_keys"": [""vendor"", ""data_source"", ""added_by""], ""vendor"": {""_aj_globalid"": ""gid://stack-shine/Vendor/296""}}"
# B: `params` Searched with by Rails
"{\"vendor\":{\"_aj_globalid\":\"gid://stack-shine/Vendor/296\"},\"_aj_symbol_keys\":[\"vendor\"]}"
Clearly this query could not work because the params in the database are not the params being search by Rails.
The notification, in the database, has extra parameters on top of "vendor" ("data_source" and "added_by") that are not being search up by the Vendor. Is this why it returns nothing?
For now, I'll simply the look up the notifications myself by storing the vendor_id in params and doing something like Notification.where("params >> vendor_id = ?", 123)

Related

Date related columns missing from find_or_initialize response

I have a Rails 5 app and want to assign some Date (not Datetime) columns of an Advert model.
The model schema is as follows, including 2 Date columns - 'start' and 'end'.
create_table "adverts", force: :cascade do |t|
t.integer "user_id"
t.integer "product_id"
t.date "start"
t.date "end"
t.boolean "expired", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The Advert controller also permits update of these columns,
def advert_params
params.require(:advert).permit(:user_id, :product_id, :start, :end, :expired)
end
I then have a method in the controller that is intended to update an advert.
The route is
post 'update_advert', :on => :collection
The advert record is pulled / created using find_or_initialize.
def update_advert
#advert = Advert.find_or_initialize_by(user_id: advert_params[:user_id], product_id: advert_params[:product_id])
logger.info "checkpoint1"
logger.info advert_params.inspect
logger.info "checkpoint2"
...
end
This gives the following log output,
I, [2020-05-19T12:32:46.947732 #13110] INFO -- : [e741ba19] checkpoint1
I, [2020-05-19T12:32:46.948986 #13110] INFO -- : [e741ba19] <ActionController::Parameters {"user_id"=>1, "product_id"=>1000, "expired"=>false} permitted: true>
I, [2020-05-19T12:32:46.949151 #13110] INFO -- : [e741ba19] checkpoint2
Note the start and end columns are missing, which therefore prevents me from going on to update them.
If I 'show' the record via the standard Rails web interface, I see the fields as expected.
If I pull the record via the JSON api, I see the expected fields.
{"id":4,"user_id":1,"product_id":1000,"start":"2020-05-18","end":"2020-06-18","expired":false}
My question is - why are the start and end columns not returned as part of the find_or_initialize response?
Thanks for reading.

Storing List item details in a database?

So, this may be more of a "Software Engineering" question. But im thinking of a good way at how to store details for a Widget in active record.
Pretend Widget A has a show page, and in that show page we have some accordian style "FAQS" or something to that effect. Within the accordian is a list, with bullet points highlighting different things of how Widget A works, or how to use Widget A.
Since obviously we wouldn't want to make a separate page for each widget, these items would need to be stored somewhere. But we also wouldn't want to make...10, 20 or 30 separate fields in the database for each one of these. So whats the solutions for this?
My first thought is some sort of hash or array, but does rails allow this? Especially if they are long strings per item. Is there a better way?
Or is the proper way to do this is just claim this as a model (like.."faq_item") or something, and then have a reference ID for the Widget it needs to go to? (that way the "faq_item" model/schema would only need a few fields, and can just assigned the reference ID to the Widget it would belong to.
If each widget has only a few "FAQ items" (or "details", as I'll refer to them) and each detail is nothing more than a text string, you could store a widget's details in a serialized array as such:
# models/widget.rb
class Widget < ApplicationRecord
# serialize the `details` attribute as JSON into
# the `details` column on the widgets table
serialize :details, JSON
end
# db/schema.rb
# ...
create_table "widgets", force: :cascade do |t|
t.string "name"
t.text "details"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
# rails console
wid = Widget.create!(
:name =>
'Wideband, Voltage-Feedback Operational Amplifier With Disable',
:details => [
'Flexible supply range: 5-V to 12-V Single Supply, +/- 2.5-V to 5-V Dual Supply',
'Unity-Gain Stable: 500 MHz (G = 1)',
'High Output Current: 190 mA',
'High Slew Rate: 1800 V/us',
'Wideband 5-V Operation: 220 MHz (G = 2)'
])
# => #<Widget ...>
wid.details.first
# => "Flexible supply range: 5-V to 12-V Single Supply, +/- 2.5-V to 5-V Dual Supply"
You can look at the Rails 5 serialization API for more information on serialize.
If, however, you need to store more information for each detail (for instance, created_at/updated_at fields) or each widget has more than a few details, then it may be prudent to create a new table for widget details as you suggested:
# models/widget.rb
class Widget < ApplicationRecord
has_many :details, :dependent => :destroy
end
# models/widget/detail.rb
class Widget::Detail < ApplicationRecord
belongs_to :widget
end
# db/schema.rb
# ...
create_table "widget_details", force: :cascade do |t|
t.integer "widget_id"
t.text "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
wid = Widget.create!(
:name =>
'CMOS, 125 MHz Complete DDS Synthesizer',
:details => [
Widget::Detail.create!(:content => '125 MHz Clock Rate'),
Widget::Detail.create!(:content => 'On-Chip High Performance DAC'),
Widget::Detail.create!(:content => '32-Bit Frequency Tuning Word')
])
# => #<Widget ...>
wid.details.first
# => #<Widget::Detail ... content: "125 MHz Clock Rate" ...>
If you are using Postgres you could use a JSONB type field in your database. With a JSONB data type you will be able to have unstructured data while being able to query the field with Postgres and ActiveRecord without the need for a new table.
Like this:
rails g migration add_fields_to_widgets details:jsonb
rails db:migrate
Test your widget creation inside the rails console.
Widget.create(name: "Widget Foo", details: { "how to use": "Instructions on how to use", "height": "12cm", "width": "100cm" })
If you'd want to find all the widgets with 12cm height, you would just have to make a query like this:
Widget.where("details->>'height' = ?", "12cm")
which would return your original Widget Foo object, and then you would be able to manipulate it with pure JavaScript on your front-end.

Trying to save JSON data to database

I'm not sure what I'm missing, but the following code below isn't saving to my database. I'm using rails 5.1.4 and ruby 2.4.1. I have no controller or views and using Mysql if that's of any help.
Model
class Agent < ApplicationRecord
json = JSON.parse('{"Agents":[{"firstName":"John","lastName":"Smith","id":"57fa5f47-8851-11e7-b391-02cbcf8dd991"},{"firstName":"Alice","lastName":"Thompson","id":"77eccb07-101d-11e7-83be-02e5025d7d75"}]}')
json['Agents'].each do |data|
Agent.create(
id: data['id'],
first_name: data['firstName'],
last_name: data['lastName']
)
end
end
Schema
create_table "agents", id: :string, limit: 36, force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "first_name"
t.string "last_name"
end
As Gokul M pointed out, firstName and lastName need to be snake-cased to match your schema. However, your code still will not work, because :id is a protected attribute in ActiveRecord models. Meaning that it can’t be mass-assigned. You can however assign it normally. i.e model.id=foo
So you could try something like
json['Agents'].each do |data|
agent = Agent.new
agent.id = data['id']
agent.first_name = data['firstName']
agent.last_name = data['lastName']
agent.save
end
Though, I haven’t tested this code.

ThinkingSphinx: dynamic indices on the SQL-backed indices?

I am trying to use ThinkingSphinx (with SQL-backed indices) in my Rails 5 project.
I need some dynamic run-time indices to search over.
I have a Message model:
class Message < ApplicationRecord
belongs_to :sender, class_name: 'User', :inverse_of => :messages
belongs_to :recipient, class_name: 'User', :inverse_of => :messages
end
and its indexer:
ThinkingSphinx::Index.define :message, :with => :active_record, :delta => true do
indexes text
indexes sender.email, :as => :sender_email, :sortable => true
indexes recipient.email, :as => :recipient_email, :sortable => true
indexes [sender.email, recipient.email], :as => :messager_email, :sortable => true
has sender_id, created_at, updated_at
has recipient_id
end
schema.rb:
create_table "messages", force: :cascade do |t|
t.integer "sender_id"
t.integer "recipient_id"
t.text "text"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "read", default: false
t.boolean "spam", default: false
t.boolean "delta", default: true, null: false
t.index ["recipient_id"], name: "index_messages_on_recipient_id", using: :btree
t.index ["sender_id"], name: "index_messages_on_sender_id", using: :btree
end
The problem is about so-called "dialogs". They don't exist in the database - they are determined at run-time. A dialog - that's a set of messages between 2 users, where each user may be either a sender or a receiver.
The task is to search through my dialogs and to find the dialog (dialog's messages) by the piece of the correspondent email. So complicated!
Here's my effort:
conditions = {messager_email: search_email}
with_current_user_dialogs =
"*, IF(sender_id = #{current_user.id} OR recipient_id = #{current_user.id}, 1, 0) AS current_user_dialogs"
messages = Message.search search_email, conditions: conditions,
select: with_current_user_dialogs,
with: {'current_user_dialogs' => 1}
This is almost fine - but still not. This query correctly searches only within my dialog (within the messages I sent or received) and only within :sender and :recipient fields simultaneously (which is not best).
Say my email is "client1#example.com". Other emails are like "client2#example.com", "client3#example.com", "manager1#example.com".
The trouble is that when I search for "client1" - I get all the messages where I was either a sender or a receiver. But I should get nothing in response - I need to search only across my correspondents emails - not mine.
Even worse stuff happens also while querying "client" - I get back the correct correspondents with "client2#example.com", "client3#example.com" - but the result is spoiled with wrong "client1#example.com".
I need a way to choose at run-time - which index subset to search within.
I mean this condition is not enough for me:
indexes [sender.email, recipient.email], :as => :messager_email, :sortable => true
It searches (for "client") within all the sender.email and all the recipient.email at once.
But I need to dynamically choose like: "search only within sender.email values conforming to if sender.id != current_user.id" OR "search only within recipient.email conforming to if recipient.id != current_user.id" (because I can be as a sender as a receiver).
That's what I call a "dynamic index".
How to do that? Such "dynamic index" surely would depend on the current current_user value - so it will be different for the different users - even on the same total messages set.
It is clear that I can't apply whatever post-search cut-offs (what to cut off?) - I need to somehow limitate the search itself.
I tried to search over some scope - but got the error that "searching is impossible over scopes" - something like that.
Maybe I should use the real-time indexing instead of the SQL-backed indexing?
Sorry for the complexity of my question.
Would the following work?
other = User.find_by :email => search_email
with_current_user_dialogs = "*, IF((sender_id = #{current_user.id} AND recipient_id = #{other.id}) OR (recipient_id = #{current_user.id} AND sender_id = #{other.id}), 1, 0) AS current_user_dialogs"
Or do you need partial matches on the searched email address?
[EDIT]
Okay, from the discussion in the comments below, it's clear that the field data is critical. While you can construct a search query that uses both fields and attributes, you can't have logic in the query that combines the two. That is, you can't say: "Search field 'x' when attribute 'i' is 1, otherwise search field 'y'."
The only way I can possibly see this working is if you're using fields for both parts of the logic. Perhaps something like the following:
current_user_email = "\"" + current_user.email + "\""
Message.search(
"(#sender_email #{current_user_email} #recipient_email #{search_email}) | (#sender_email #{search_email} #recipient_email #{current_user_email})"
)

Is there a way to pass property.method to .where() instead of property in Rails?

My Schedule model looks like this:
create_table "schedules", force: :cascade do |t|
t.integer "week_day"
t.time "opening_time"
t.time "closing_time"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "taco_place_id"
end
add_index "schedules", ["taco_place_id"], name: "index_schedules_on_taco_place_id"
As you can see, there are opening_time and closing_time properties and I have a realtionship Schedule belongs_to :taco_place and TacoPlace has_many :schedules, dependent: :destroy.
What I am trying to do from the Schedule model is to get the actual schedule for a TacoPlace for today (if it exists).
I have already implemented a scope for having today's schedules for a TacoPlace (depending on the week_day property) that looks like this:
scope :today_for_taco_place, ->(taco_place){where(taco_place_id: taco_place.id, week_day: Time.now.wday)}
and I'm using it in this method:
def self.actual_for_taco_place(taco_place)
today = self.today_for_taco_place(taco_place)
today.where("opening_time <= :now and closing_time >= :now", now: Time.now.utc).first
end
I have tested it and it "works". The thing is that if I run "Schedule.first.opening_time" on the console I get "2000-01-01 06:00:00 UTC". As you can see, it does not only include the time, but also the day (even if it was seeded as "opening_time: "15:00".to_time, closing_time: "24:00".to_time").
Finally, here is the question:
Is there a way that I can run something like this: (I know this won't work yet)
def self.actual_for_taco_place(taco_place)
today = self.today_for_taco_place(taco_place)
today.where("#{opening_time.strftime("%H%M")} <= :now and #{closing_time.strftime("%H%M") >= :now", now: Time.now.utc.strftime("%H%M")).first
end
So that the .where() method doesn't look for the property (opening_time or closing_time), but rather perform the strftime() method so I can compare the time only? Or should I save the opening_time and closing_time as integers (i.e. "1200") or manually convert them in a method?
Sorry if my question was long or hard to understand. Thank you in advance for your advise.
Opening_time and closing_time are now integers. I figured out that I don't gain anything from it being a "time" instead of an "integer" since it is only representing an hour.

Resources