Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Allow querying in REST API and GraphQL #995

Merged
merged 1 commit into from
Jul 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/controllers/api/resource_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ class Api::ResourceController < Api::BaseController
before_action :validate_payload, only: [:create, :update]

def index
render json: ApiResourceSerializer.new(@api_namespace.api_resources.order(updated_at: :desc)).serializable_hash
response = @api_namespace.api_resources

response = response.jsonb_search(:properties, search_params.to_hash) if params[:properties]

render json: ApiResourceSerializer.new(response.order(updated_at: :desc)).serializable_hash
end

def query
Expand Down Expand Up @@ -83,4 +87,8 @@ def load_api_resource
def resource_params
params.permit(data: {})
end

def search_params
params.require(:properties).permit!
end
end
7 changes: 6 additions & 1 deletion app/graphql/types/api_namespace_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ class ApiNamespaceType < Types::BaseObject
argument :order_direction, String, required: false
argument :order_dimension, String, required: false
argument :offset, Integer, required: false
argument :properties, GraphQL::Types::JSON, required: false

def resolve(parent, frozen_parameters, context)
parameters = { **frozen_parameters }
parameters[:order_dimension] ||= 'created_at'
parameters[:order_direction] ||= 'desc'
parameters[:limit] ||= 50
parameters[:offset] ||= 0
parent.object.api_resources.order("#{parameters[:order_dimension].underscore} #{parameters[:order_direction]}").limit(parameters[:limit]).offset(parameters[:offset])

api_resources = parent.object.api_resources
api_resources = api_resources.jsonb_search(:properties, parameters[:properties]) if parameters[:properties]

api_resources.order("#{parameters[:order_dimension].underscore} #{parameters[:order_direction]}").limit(parameters[:limit]).offset(parameters[:offset])
end
end
end
Expand Down
1 change: 1 addition & 0 deletions app/models/api_resource.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class ApiResource < ApplicationRecord
include JsonbFieldsParsable
include JsonbSearch::Searchable

after_initialize :inherit_properties_from_parent

Expand Down
98 changes: 98 additions & 0 deletions app/models/concerns/jsonb_search/query_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
module JsonbSearch
module QueryBuilder
QUERY_OPTION = {
EXACT: 'EXACT',
PARTIAL: 'PARTIAL'
}.freeze

class << self
def build_jsonb_query(column_name, query_params)
parsed_params = parse_params(query_params.deep_symbolize_keys)
build(parsed_params, column_name)
end

private

def parse_params(query_params)
queries = []

query_params.each do |key, value|
if value.is_a?(Hash) && value.key?(:value)
# { name: { value: 'violet', query }} || { name: { value: 'violet', option: 'EXACT' }}
queries << { option: value[:option] || QUERY_OPTION[:EXACT], key: key, value: value[:value] }
elsif value.is_a?(Hash)
# { foo: { bar: 'baz', wat: 'up' }}
value.each do |k, v|
if v.is_a?(Hash) && v.key?(:value)
queries << { key: key, value: [{ option: v[:option] || QUERY_OPTION[:EXACT], key: k, value: v[:value] }] }
else
queries << { key: key, value: [{ option: QUERY_OPTION[:EXACT], key: k, value: v }]}
end
end
else
# { name: 'violet' } || { tags: ['violet', 'rails'] }
queries << { option: QUERY_OPTION[:EXACT], key: key, value: value }
end
end

queries
end

def build(queries, column_name)
queries_array = queries.map do |object|
generate_query(object, column_name)
end
queries_array.join(' AND ')
end

def generate_query(param, query_string)
key = param[:key]
term = param[:value]
option = param[:option]
case term.class.to_s

when 'Hash'
return hash_query(key, term, option, query_string)
when 'Array'
if option
return array_query(key, term, option, query_string)
else
term.each do |obj|
# "column -> 'property' ->> 'nested property' = 'term'"
query_string = generate_query(obj, "#{query_string} -> '#{key}'")
end

return query_string
end
else
return string_query(key, term, option, query_string)
end
end

# "column ->> 'property' = 'term'"
def string_query(key, term, option, query)
if option == QUERY_OPTION[:PARTIAL]
term = "%#{term}%"
operator = 'LIKE'
end

"lower(#{query} ->> '#{key}') #{operator || '='} lower('#{term}')"
end

# "column -> 'property' @> '{/"search/": /"term/"}'"
def hash_query(key, term, option, query)
operator = option == QUERY_OPTION[:PARTIAL] ? '@>' : '='
"#{query} -> '#{key}' #{operator} '#{term.to_json}'"
end

# "column -> 'property' ? '['term']'"
def array_query(key, term, option, query)
if option == QUERY_OPTION[:PARTIAL]
"#{query} -> '#{key}' @> '#{term.to_json}'"
else
"#{query} -> '#{key}' @> '#{term.to_json}' AND #{query} -> '#{key}' <@ '#{term.to_json}'"
end
end
end
end
end
14 changes: 14 additions & 0 deletions app/models/concerns/jsonb_search/searchable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Search Jsonb fields
# Simple query format => { name: 'violet' }
# Extended format => { name: { value: 'violet', option: 'PARTIAL' } }, option is optional, default is EXACT

module JsonbSearch
module Searchable
extend ActiveSupport::Concern
include JsonbSearch::QueryBuilder

included do
scope :jsonb_search, ->(column_name, query) { where(JsonbSearch::QueryBuilder.build_jsonb_query(column_name, query)) }
end
end
end
Loading