Rails; Fetch records within initializer - ruby-on-rails

I've been wondering it is common to fetch records within initializer?
Here this is an example for service object to fetch records and generated pdf receipt file.
Input is invoice uuid, and fetch the related records such as card detail, invoice items within initialier.
class Pdf::GenerateReceipt
include Service
attr_reader :invoice, :items, :card_detail
def initialize(invoice_uuid)
#invoice ||= find_invoice!(invoice_uuid) # caching
#items = invoice.invoice_items
#card_detail = card_detail
end
.....
def call
return ReceiptGenerator.new(
id: invoice.uuid, # required
outline: outline, # required
line_items: line_items, # required
customer_info: customer_info
)
rescue => e
false, e
end
.....
def card_detail
card_metadata = Account.find(user_detail[:id]).credit_cards.primary.last
card_detail = {}
card_detail[:number] = card_metadata.blurred_number
card_detail[:brand] = card_metadata.brand
card_detail
end
end
Pdf::GenerateReceipt.('28ed7bb1-4a3f-4180-89a3-51cb3e621491') # => then generate pdf
The problem is if the records not found, this generate an error.
I could rescue within the initializer, however that seems not common.
How could I work around this in more ruby way?

This is mostly opinion and anecdotal, but I prefer to deal with casting my values as far up the chain as possible. So i would find the invoice before this object and pass it in as an argument, same with the card_detail.
If you do that in this class, it will limit the responsibility to coordinating those two objects, which is way easier to test but also adds another layer that you have to reason about in the future.
So how i would handle, split this into 4 separate things
Invoice Finder thing
Card Finder thing
Pdf Generator that takes invoice and card as arguments
Finally, something to orchestrate the 3 actions above
Hope this helps.
Addition: Check out the book confident ruby by avdi grimm. It's really great for outlining handling this type of scenario.

Related

JSON API standard: how to associate data that isn't saved and has no id (yet)?

In my rails app I send data to the react front end in JSON API standard format via the jsonapi-serializer gem. This works well for data coming from our database, but when we have failing form data, which is sent in the form of rails nested resources, sometimes this data needs to be returned with errors before the records are actually saved.
When new model data has failing validations, I need those errors to come back to my front end and have the associations intact. I've been trying to run them through jsonapi-serializer as well, but the records that don't have ids, don't make it into the relationships key. The problem is that failing new records don't have an id, and they come back to the front end un-associated.
My only solution to this so far is to manually shim fake temporary ids in there. I'm not sure if I'm missing something obvious. That id is removed on the front end for re-submission. My solution isn't perfect and has limitations and issues.
Is there a built-in way to do this or am I stuck with my own implementation? Is there some kind of alternate key I could use for this association?
For what it's worth, this is my implementation so far, just to clarify what I'm doing. It's far from perfect and a lot of code for what seems like it should be a problem with a built-in solution. It's based on trying to iterate over the object tree being serialized and seeing if a "fake" id is needed. This still chokes on some things that can be passed via include: []
class BaseSerializer
module ShimIdConcerns
extend ActiveSupport::Concern
private
# Very Important! Controls relationship of json api assocs in front end
# The id that is returned to the front end needs to be smarter than simply
# the actual id or nil. The id is used to associate jsonapi objects to their
# main resource whether they have an id yet or not. Sometimes these objects
# are not persisted, but have errors and need to be associated properly in the UI.
# Works in conjuncton with attribute :is_persisted
def shim_id_on_failing_new_associations(resource, options)
shim_id_on_failing_new_associations_recursive resource, options[:include].to_a
end
# dot_assocs: expects assocs in 'packages.package_services.service' format
def shim_id_on_failing_new_associations_recursive(resource, dot_assocs)
assocs = simplify_assocs(dot_assocs)
assocs.each do |assoc_path|
next if assoc_path.blank?
segments = assoc_path.split('.')
method = segments.shift
next unless resource.respond_to?(method)
assoc = resource.send(method)
if assoc.is_a? ActiveRecord::Base
shim_id(resource)
shim_id_on_failing_new_associations_recursive assoc, segments.join('.')
elsif assoc.is_a?(ActiveRecord::Associations::CollectionProxy) || assoc.is_a?(Array)
assoc.each do |one_of_many|
shim_id_on_failing_new_associations_recursive one_of_many, segments.join('.')
end
end
end
end
# Gives a temp id on failing or new resources so they can be associated in react
# in jsonapi/react. Ensure this id is not re-submitted, however
def shim_id(resource)
resource.id = rand(1000000000000) if resource.id.nil? && resource.new_record?
end
# turns
# [ :reservation, :'reservation.client', :'reservation.client.credit_card' ]
# into
# [ "reservation.client.credit_card" ]
# to avoid duplicate processing
def simplify_assocs(dot_assocs)
ap dot_assocs
all = [dot_assocs].flatten.map(&:to_s)
simp = []
# yes quadratic, but will be little data
all.each do |needle|
matches = 0
all.each do |hay|
matches += 1 if hay.index(needle) === 0
end
simp << needle if matches === 1
end
simp
end
end
end

Rails partial need to be rendered twice with different content, but output is equal

I have to output 2 blocks on the same page, each one of them must contain 3 random posts from database.
I have simple model
class Post < ActiveRecord::Base
scope :random, -> { order('RANDOM()') }
end
I put the ActiveRecord code in application_helper.rb:
module ApplicationHelper
def random_posts(num = 3)
posts = Post.uncached do
Post.random.limit(num)
end
end
end
then in layout I use this call twice (Slim template engine used)
= render random_posts
which uses this partial app/views/posts/_post.html.slim
a.btn = post.title
Blocks are filled with 3 random posts from database, but they are the same in each blocks! Why is it so? Each block has to contain different posts.
I've created a repo here with simple demonstration
I got this to work by flipping uncached to cache. I was experimenting on the Post model, but you could probably drop this in your helper just as easily:
class Post < ActiveRecord::Base
def self.random_posts(n = 3)
cache do
random.limit(n)
end
end
end
For two calls of the method using uncached, the ActiveRecord log lines are Post Load ... and CACHE ..., but using cache, they are both Post Load.... I really wish I could explain why this works, but it's completely counterintuitive and makes no sense to me.
Forking your code, it seems that the collection Post.random is being cached in Rails in some way. If you add a debugger on the random_posts in ApplicationHelper:
Post.random.map(&:id)
Will have the same collection every time.
Taken from this blogpost, you could use this as an alternative:
In ApplicationHelper.rb:
def self.random_posts(num = 3)
ids = Post.pluck(:id).shuffle[0..4]
Post.where(id: ids)
end

ActiveRecord::Base Class Not Mutable?

I have a class I've extended from ActiveRecord::Base...
class Profile < ActiveRecord::Base
and I collect the records from it like so...
records = #profile.all
which works fine, but it doesn't seem that I can successfully Update the attributes. I don't want to save them back to the database, just modify them before I export them as JSON. My question is, why can't I update these? I'm doing the following (converting date formats before exporting):
records.collect! { |record|
unless record.term_start_date.nil?
record.term_start_date = Date.parse(record.term_start_date.to_s).strftime('%Y,%m,%d')
end
unless record.term_end_date.nil?
record.term_end_date = Date.parse(record.term_end_date.to_s).strftime('%Y,%m,%d')
end
record
}
At first I had just been doing this in a do each loop, but tried collect! to see if it would fix things, but no difference. What am I missing?
P.S. - I tried this in irb on one record and got the same results.
I suggest a different way to solve the problem, that keeps the logic encapsulated in the class itself.
Override the as_json instance method in your Profile class.
def as_json(options={})
attrs = super(options)
unless attrs['term_start_date'].nil?
attrs['term_start_date'] = Date.parse(attrs['term_start_date'].to_s).strftime('%Y,%m,%d')
end
unless attrs['term_end_date'].nil?
attrs['term_end_date'] = Date.parse(attrs['term_end_date'].to_s).strftime('%Y,%m,%d')
end
attrs
end
Now when you render the records to json, they'll automatically use this logic to generate the intermediate hash. You also don't run the risk of accidentally saving the formatted dates to the database.
You can also set up your own custom option name in the case that you don't want the formatting logic.
This blog post explains in more detail.
Try to add record.save! before record.
Actually, by using collect!, you just modifying records array, but to save modified record to database you should use save or save! (which raises exception if saving failed) on every record.

How do I obfuscate the ids of my records in rails?

I'm trying to figure out how to obfuscate the ids of my records in rails.
For example: a typical path might look like http://domain/records/1, so it's pretty easy for people to deduce how much traffic the site is getting if they just create a new record.
One solution that I've used is to hash the id with a salt, but since I'm not sure whether that function is bijective, I end up storing it in another column in my database and double check for uniqueness.
Another option I was thinking about was generating a random hash and storing that as another column. If it isn't unique ... just generate another one.
What's the best way of doing this?
You could use the built-in OpenSSL library to encrypt and decrypt your identifiers, that way you would only need to overwrite to_param on your models. You'll also need to use Base64 to convert the encrypted data into plain text. I would stick this in a module so it can be reused:
require 'openssl'
require 'base64'
module Obfuscate
def self.included(base)
base.extend self
end
def cipher
OpenSSL::Cipher::Cipher.new('aes-256-cbc')
end
def cipher_key
'blah!'
end
def decrypt(value)
c = cipher.decrypt
c.key = Digest::SHA256.digest(cipher_key)
c.update(Base64.decode64(value.to_s)) + c.final
end
def encrypt(value)
c = cipher.encrypt
c.key = Digest::SHA256.digest(cipher_key)
Base64.encode64(c.update(value.to_s) + c.final)
end
end
So now your models would need to look something like this:
class MyModel < ActiveRecord::Base
include Obfuscate
def to_param
encrypt id
end
end
Then in your controller when you need to find a record by the encrypted id, you would use something like this:
MyModel.find MyModel.decrypt(params[:id])
If you're looking to encrypt/decrypt ids without storing them in the database, this is probably the easiest way to go.
Instead of numeric ids, use some kind of friendly url or human readable slug. There are lots of tools to choose from in this department. Not only are they more friendly to your users, but well chosen slugs can give a nice advantage with search engines.
Here's a gem that keeps it numeric, requires no database migrations, and no routing changes: https://github.com/namick/obfuscate_id
I've found that this gem doesn't work in concert with some other gems, notably paper_trail. This is because of the way it replaces the find method, and paper_trail causes find to be called with the actual record id.
So I've been using the gem's "scatter_swap" functionality, but not the rest of it. Here's the model:
require 'obfuscate_id/scatter_swap'
class Page < ActiveRecord::Base
# This is a random number that, if changed, will invalidate all existing URLs. Don't change it!
##obfuscate_spin = # random number here, which is essentially the encryption key
##
# Generate URL parameter to be used in the URL as the "id"
def to_param
# Use the obfuscate_id gem's class to "spin" the id into something obfuscated
spun_id = ScatterSwap.hash(self.id, ##obfuscate_spin)
# Throw any additional attributes in here that are to be included in the URL.
"#{spun_id} #{name}".parameterize
end
def self.find_by_slug!(slug)
spun_id = slug[/^[0-9]+/]
begin
find_by_id! ScatterSwap.reverse_hash(spun_id, ##obfuscate_spin)
rescue ActiveRecord::RecordNotFound => e
raise ActiveRecord::RecordNotFound, "Couldn't find matching Page."
end
end
end
And in the controller:
class PagesController < InheritedResources::Base
# Find the page using its URL slug
before_filter :find_page, except: [:index, :create, :new]
def find_page
#page = Page.find_by_slug! params[:id]
# If the URL doesn't match exactly, and this is a GET.
# We'll redirect to the new, correct URL, but if this is a non-GET, let's let them finish their request instead.
if params[:id] != #page.to_param && request.get?
redirect_to url_for({ id: #page.to_param }), status: 301
end
end
end
As an alternative to the redirection that takes place there, you could simply include a canonical URL in the page. The redirection has the bug of ignoring any query parameters in the URL. This was not a problem for my project, as I didn't have any. But a canonical URL would be better.
It's pretty easy to generate unique random identifiers for your records either using a randomized string generator or a simple call to Digest::SHA1.hexdigest which produces reasonably random and cryptographically unique results.
For instance, you can create a secondary column called ident or unique_id that stores your public identifiers. You can then over-write to_param to use this instead:
class MyModel < ActiveRecord::Base
before_create :assign_ident
def self.from_param(ident)
find_by_ident(ident)
end
def to_param
self.ident
end
protected
def assign_ident
self.ident = Digest::SHA1.hexdigest(SecureRandom.random_number(1<<256).to_s)
end
end
Theoretically there is a chance of collision on SHA1 but the odds are so astronomically low you're more liable to have a software crash because of a memory error or hardware malfunction. You can test this to see if it suits your needs by generating a few billion identities to see if they ever collide, which they shouldn't. A 256-bit random number should provide a sufficient amount of data for the SHA1 algorithm to chew on.
After reading through #siannopollo's post, I created a Gem based on the idea of his post (but with some improvements): https://github.com/pencil/encrypted_id
Just because it hasn't been mentioned here: You could simply use UUIDs (wikipedia article)
There are multiple ways of using UUID as primary keys in Rails, depending on your Rails version and database engine. It's easy to find.
Just as a possibility, in case you depend too much on your existing integer primary key, you can also just add a UUID to your table and make your model use it automatically when it comes to generating URLs by overwriting Model#to_param more details in the docs

I want to map my database lookup tables to a hash, good idea?

I am developing a Rails web application and am confused about how to utilize the lookup table values in my models. Here is an example model from my app:
table name: donations
id
amount
note
user_id
appeal_id
donation_status_id
donation_type_id
is_anonymous
created_at
updated_at
The fields *donation_status_id* and *donation_type_id* refer to lookup tables. So in my code I have several random places where I make calls like this:
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus.find_by_name("completed").id
#do something
end
To my inexperienced eyes, a one-off query to the DonationStatus table seems incredibly wasteful here, but I don't see any other good way to do it. The first idea I thought of was to read all my lookup tables into a hash at application startup and then just query against that when I need to.
But is there a better way to do what I am trying to do? Should I not worry about queries like this?
Thanks!
Since you have two models, you should use ActiveRecord Model Associations when building the models.
class Donation
has_one :donation_status
end
class DonationStatus
belongs_to :donation
end
Then when you do
my_donation = Donation.find(params[:id])
if my_donation.donation_status.status_name == 'complete'
#do something
end
For more information, you may want to read up how rails is doing the model associations http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html Don't worry about performance, rails has taken care of that for you if you follow how the way it should be done
How about putting it in a constant? For example, something like this:
class DonationStatus < ActiveRecord::Base
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id
PENDING_DONATION_ID = DonationStatus.find_by_name("pending").id
# ...
end
class DonationsController < ApplicationController
def some_action
my_donation = Donation.find(params[:id])
if my_donation.donation_status_id == DonationStatus::COMPLETED_DONATION_ID
#do something
end
end
This way, DonationStatus.find_by_name("pending").id gets executed exactly one. I'm assuming, of course, that this table won't change often.
BTW, I learned this trick in Dan Chak's book, Enterprise Rails.
EDIT: I forgot to mention: in practice, I declare constants like this:
COMPLETED_DONATION_ID = DonationStatus.find_by_name("completed").id rescue "Can't find 'completed' in donation_statuses table"
What you could do is add this method to Donation:
# Donation.rb
def completed?
self.donation_status.name == 'completed' ? true : false
end
And then just do my_donation.completed?. If this is called a second time, Rails will look to cache instead of going to the DB.
You could add memcached if you want, or use Rails' caching further, and do:
def completed?
return Rails.cache.fetch("status_#{self.donation_status_id}_complete") do
self.donation_status.name == 'completed' ? true : false
end
end
What that will do is make a hash key called (for example) "status_1_complete" and if it's not defined the first time, will evaluate the block and set the value. Otherwise, it will just return the value. That way, if you had 1,000,000,000 donations and each of them had donation_status 1, it would go directly to the cache. memcached is quite fast and popular.

Resources