In my rails application I have categories which are classified based on location. I want to cache these categories on serializer level by using jsonapi-serializer's caching method cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 3.hours. The issue here is that when I call it for the first time to retrieve categories in City A for example then later try to retrieve categories from City B it will still show City A's data. What I'm trying to do now is to pass city to serializer then add it to namespace using something like this namespace: "jsonapi-serializer/categories/#{:city}" in order to differentiate between them but I'm not able to find a way to do so yet.
Currently I have this block of code to retrieve categories based on location then serialize and render a json
category = Category.active_categories(headers['User-Location'])
unless category.empty?
generic_category = Category.all_category
categories = generic_category, category
render CategorySerializer.new(categories.flatten, { params: { user_location: headers['User-Location'] } })
end
While Category.active_categories and Category.all_category are caching the queries as below:
active_categories:
def self.active_categories(user_location)
Rails.cache.fetch("active_categories/#{user_location}", expires_in: 3.hours) do
Category.where(status: 'active')
.joins(:sellers).where(sellers: { status: 'active', is_validated: true })
.joins(sellers: :cities).where(cities: { name_en: user_location })
.joins(sellers: :products).where(products: { status: 'available' })
.where.not(products: { quantity: 0 }).with_attached_cover
.order(featured: :desc).uniq.to_a
end
end
all_category:
def self.all_category
Rails.cache.fetch('all_category', expires_in: 24.hours) do
Category.where(name_en: 'All').with_attached_cover.first
end
end
Found the solution already. Just override the caching method as the example below:
require 'active_support/cache'
class MySerializer
include JSONAPI::Serializer
cache_options(store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 3.hours)
def self.record_cache_options(options, fieldset, include_list, params)
return super(options, fieldset, include_list, params) if params[:user_location].blank?
opts = options.dup
opts[:namespace] += ':' + params[:user_location]
opts
end
end
Related
Description
I have just migrated our application from searchkick to meilisearch however meilisearch doesn't have a way I can search for single term across multiple indexes or models like searchkick does.
Basic example
I want to to be able to search my term on at least one model
example
Meilisearch.search(term, models: [...index_names])
Here is a workaround using parallel gem
# search.rb
module Queries
class Search < BaseQuery
include SearchHelper
type [Types::SearchResultsType], null: true
argument :query, String, required: true
argument :models, [String], required: true, default_value: ['AppUser']
def resolve(**args)
search(args)
end
end
end
# search_helper.rb
module SearchHelper
SEARCH_MODELS = %w[AppUser Market Organisation]
def search(args)
raise Errors::SearchError::BlankQueryError if args[:query].blank?
raise Errors::SearchError::UnpermittedQueryError if args[:query] == '*'
models = args[:models].map { |m| m.tr(' ', '').camelize }
if models.difference(SEARCH_MODELS).any?
raise Errors::SearchError::UnknownSearchModelError
end
Parallel.flat_map(models, in_threads: models.size) do |m|
m.constantize.search(args[:query])
end
end
end
# search_results_type
module Types
class SearchResultsType < Types::BaseUnion
description 'Models which may be searched on'
possible_types(
Types::AppUserType,
Types::MarketType,
Types::OrganisationType,
)
def self.resolve_type(object, context)
if object.is_a?(AppUser)
Types::AppUserType
elsif object.is_a?(Market)
Types::MarketType
else
Types::OrganisationType
end
end
end
end
Graphql query
{
search(query: "Gh", models: ["Market","Organisation"]) {
... on Market {
id
name
}
... on Organisation {
id
name
}
... on AppUser {
id
email
}
}
}
I have 2 tables, order and product_orders
they are related, in product_order it has the order_id
when I render the Json "render json: order", it comes out correct,
if I do "render json: p_order", it also outputs correct.
But I needed to generate the order json, with the items "quantity, unity_price and total from the Product_order table, how would I do that?
any suggestions or links for understanding/study?
def call_command
orders = Order.all
product_orders = ProductOrder.all
order = orders.map do |t|
build = { order_id: "#{t.id}",
created_at_id: "#{t.created_at}",
user_id: "#{t.user_id}",
desk_id: "#{t.desk_id}",
status: "#{t.status}",
subtotal: "#{t.subtotal}",
total: "#{t.total}"
}
end
p_order = product_orders.map do |p|
build = { #product_order_id: "#{p.id}",
# created_at_id: "#{p.created_at}",
order_id: "#{p.order_id}",
product_it: "#{p.product_id}",
quantity: "#{p.quantity}",
unity_price: "#{p.unit_price}",
total: "#{p.total}"
}
end
render json: order
end
Assuming that your Order and ProductOrder models are setup like this:
class Order < ActiveRecord::Base
has_many :product_orders
end
class ProductOrder < ActiveRecord::Base
belongs_to :order
end
You can use the includes in your render call, like the following:
#orders = Order.all
render json: #orders, include: ['product_orders']
You can also use the ActiveModel Serializers as #Benjamin suggested:
#orders.as_json(include: :product_orders)
I am working on implementing a search endpoint with ruby based on a json request sent from the client which should have the form GET /workspace/:id/searches? filter[query]=Old&filter[type]=ct:Tag,User,WokringArea&items=5
The controller looks like this
class SearchesController < ApiV3Controller
load_and_authorize_resource :workspace, class: "Company"
load_and_authorize_resource :user, through: :workspace
load_and_authorize_resource :working_area, through: :workspace
def index
keyword = filtered_params[:query].delete("\000")
keyword = '%' + keyword + '%'
if filtered_params[:type].include?('User')
#users = #workspace.users.where("LOWER(username) LIKE LOWER(?)", keyword)
end
if filtered_params[:type].include?('WorkingArea')
#working_areas = #workspace.working_areas.where("LOWER(name) LIKE LOWER(?)", keyword)
end
#resources = #working_areas
respond_json(#resources)
end
private
def filtered_params
params.require(:filter).permit(:query, :type)
end
def ability_klasses
[WorkspaceAbility, UserWorkspaceAbility, WorkingAreaAbility]
end
end
respond_json returns the resources with a json format and it looks like this
def respond_json(records, status = :ok)
if records.try(:errors).present?
render json: {
errors: records.errors.map do |pointer, error|
{
status: :unprocessable_entity,
source: { pointer: pointer },
title: error
}
end
}, status: :unprocessable_entity
return
elsif records.respond_to?(:to_ary)
#pagy, records = pagy(records)
end
options = {
include: params[:include],
permissions: permissions,
current_ability: current_ability,
meta: meta_infos
}
render json: ApplicationRecord.serialize_fast_apijson(records, options), status: status
end
Now the issue is the response is supposed to look like this:
{
data: [
{
id: 32112,
type: 'WorkingArea'
attributes: {}
},
{
id: 33321,
type: 'User',
attributes: {}
},
{
id: 33221,
type: 'Tag'
attributes: {}
}
How can I make my code support responding with resources that have different types?
You can define a model, not in your database, that is based on the results from the API. Then you include some of the ActiveModel modules for more features.
# app/models/workspace_result.rb
class WorkspaceResult
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Serialization
attr_accessor(
:id,
:type,
:attributes
)
def initialize(attributes={})
filtered_attributes = attributes.select { |k,v| self.class.attribute_method?(k.to_sym) }
super(filtered_attributes)
end
def self.from_json(json)
attrs = JSON.parse(json).deep_transform_keys { |k| k.to_s.underscore }
self.new(attrs)
end
end
Then in your API results you can do something like:
results = []
response.body["data"].each do |result|
results << WorkspaceArea.from_json(result)
end
You can also define instance methods on this model, etc.
I'm running a rails application that calls Simplecasts API to display my podcast episodes. I followed a tutorial to setup the API services using Faraday. My question is how to only display published episodes on my index page? Normally, I would add a .where(:status => "live") in my controller, IE #podcasts = Episodes.where(:status => "published") but this doesn't seem to work.
Simplecast's API for the podcast returns a collection that contains all the available episodes, each has a status node.
Any help would be appreciated as I'm new to working with external APIs in Rails
Sample API response
"collection": [
{
"updated_at": "2020-03-25T17:57:00.000000-04:00",
"type": "full",
"token": "lgjOmFwr",
"title": "Test",
"status": "draft",
Episode.rb
module Simplecast
class Episodes < Base
attr_accessor :count,
:slug,
:title,
:status
MAX_LIMIT = 10
def self.episodes(query = {})
response = Request.where('/podcasts/3fec0e0e-faaa-461f-850d-14d0b3787980/episodes', query.merge({ number: MAX_LIMIT }))
episodes = response.fetch('collection', []).map { |episode| Episode.new(episode) }
[ episodes, response[:errors] ]
end
def self.find(id)
response = Request.get("episodes/#{id}")
Episode.new(response)
end
def initialize(args = {})
super(args)
self.collection = parse_collection(args)
end
def parse_collection(args = {})
args.fetch("collection", []).map { |episode| Episode.new(episode) }
end
end
end
Controller
class PodcastsController < ApplicationController
layout "default"
def index
#podcasts, #errors = Simplecast::Episodes.episodes(query)
#podcast, #errors = Simplecast::Podcast.podcast(query)
render 'index'
end
# GET /posts/1
# GET /posts/1.json
def show
#podcast = Simplecast::Episodes.find(params[:id])
respond_to do |format|
format.html
format.js
end
end
private
def query
params.permit(:query, {}).to_h
end
end
Looks like collection is just an array of hashes so rails ActivrRelations methods aka .where are not supported. However It is an array so you can just filter this array:
published_episodes = collection.filter { |episode| episode[:status] == “ published” }
Also look through their API - may be the do support optional filtering params so you would get only published episodes in the first place.
BTW: second thought is to save external API request data in your own DB and then fetch require episodes with standard .where flow.
Having some issues putting these puzzle pieces together... I'm scraping a website to get an array of strings and I want the array to get sent back to my React client for use. Here's what I have
index.js
componentDidMount() {
const { restaurant } = this.state
axios.post('/api/scraper', { restaurant: restaurant })
.then((res) => {
console.log(res.data);
})
}
app/controllers/api/scraper_controller.rb
class Api::ScraperController < ApplicationController
respond_to :json
def create
#info = helpers.get_info(params[:restaurant])
respond_with #info
end
end
app/helpers/api/scraper_helper.rb
module Api::ScraperHelper
def get_info(restaurant)
puts restaurant
require 'openssl'
doc = Nokogiri::HTML(open('http://www.subway.com/en-us/menunutrition/menu/all', :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE))
#items = []
doc.css('.menu-cat-prod-title').each do |item|
#items.push(item.text)
end
end
end
The whole idea is to get the #items array sent back to my axios request on my React page
Your actual code will just return 0, because the result of applying each in a Nokogiri::XML::NodeSet in this case is 0, and is what you're leaving as the last executed "piece of code" within your method, so Ruby will return this.
If you add #items in the last line, then this will be returned, and you'll get ["Black Forest Ham", "Chicken & Bacon Ranch Melt", ...] that I guess is what you need:
#items = []
doc.css('.menu-cat-prod-title').each { |item| #items.push(item.text) }
#items
Note you could also do a map operation on doc.css('.menu-cat-prod-title'), which can then be assigned to any instance variable:
def get_info(restaurant)
...
doc.css('.menu-cat-prod-title').map(&:text)
end
I guess to return the data from create you could use something like render json: { items: #items }, as items contains an array of menues.