cartodb-4.42/app/queries/carto/visualization_query_builder.rb
2024-04-06 05:25:13 +00:00

316 lines
8.1 KiB
Ruby

require 'active_record'
require_relative '../../models/carto/shared_entity'
require_dependency 'carto/bounding_box_utils'
require_dependency 'carto/uuidhelper'
# TODO: consider moving some of this to model scopes if convenient
class Carto::VisualizationQueryBuilder
include Carto::UUIDHelper
def self.user_public_tables(user)
user_public(user).with_type(Carto::Visualization::TYPE_CANONICAL)
end
def self.user_public_visualizations(user)
user_public_privacy_visualizations(user).with_published
end
def self.user_public_privacy_visualizations(user)
user_public(user).with_types(Carto::Visualization::MAP_TYPES)
end
def self.user_public_privacy_datasets(user)
user_public(user).with_types(Carto::Visualization::TYPE_CANONICAL)
end
def self.user_link_privacy_visualizations(user)
new.with_user_id(user.id)
.with_types(Carto::Visualization::MAP_TYPES)
.with_privacy(Carto::Visualization::PRIVACY_LINK)
end
def self.user_password_privacy_visualizations(user)
new.with_user_id(user.id)
.with_types(Carto::Visualization::MAP_TYPES)
.with_privacy(Carto::Visualization::PRIVACY_PROTECTED)
end
def self.user_private_privacy_visualizations(user)
new.with_user_id(user.id)
.with_types(Carto::Visualization::MAP_TYPES)
.with_privacy(Carto::Visualization::PRIVACY_PRIVATE)
end
def self.user_all_visualizations(user)
new.with_user_id(user ? user.id : nil).with_types(Carto::Visualization::MAP_TYPES)
end
def self.user_public(user)
new.with_user_id(user ? user.id : nil).with_privacy(Carto::Visualization::PRIVACY_PUBLIC)
end
def initialize
@include_associations = []
@eager_load_associations = []
@filtering_params = {}
end
def with_id_or_name(id_or_name)
raise 'VisualizationQueryBuilder: id or name supplied is nil' if id_or_name.nil?
if uuid?(id_or_name)
with_id(id_or_name)
else
with_name(id_or_name)
end
end
def with_id(id)
@filtering_params[:id] = id
self
end
def with_excluded_ids(ids)
@filtering_params[:excluded_ids] = ids
self
end
def without_synced_external_sources
@filtering_params[:exclude_synced_external_sources] = true
self
end
def without_imported_remote_visualizations
@filtering_params[:exclude_imported_remote_visualizations] = true
self
end
def without_raster
@filtering_params[:excluded_kinds] ||= []
@filtering_params[:excluded_kinds] << Carto::Visualization::KIND_RASTER
self
end
def with_name(name)
@filtering_params[:name] = name
self
end
def with_user_id(user_id)
@filtering_params[:user_id] = user_id
self
end
def with_user_id_not(user_id)
@filtering_params[:user_id_not] = user_id
self
end
def with_privacy(privacy)
@filtering_params[:privacy] = privacy
self
end
def with_liked_by_user_id(user_id)
@filtering_params[:liked_by_user_id] = user_id
self
end
def with_shared_with_user_id(user_id)
@filtering_params[:shared_with_user_id] = user_id
self
end
def with_owned_by_or_shared_with_user_id(user_id)
@filtering_params[:owned_by_or_shared_with_user_id] = user_id
self
end
def with_prefetch_user(force_join = false)
if force_join
with_eager_load_of(:user)
else
with_include_of(:user)
end
end
def with_prefetch_table
nested_association = { map: :user_table }
with_eager_load_of(nested_association)
end
def with_prefetch_dependent_visualizations
inner_visualization = { visualization: { map: { layers: :layers_user_tables }, permission: :owner } }
nested_association = { map: { user_table: { layers: { maps: inner_visualization } } } }
with_eager_load_of(nested_association)
end
def with_prefetch_permission
nested_association = { permission: :owner }
with_eager_load_of(nested_association)
end
def with_prefetch_external_source
with_eager_load_of(:external_source)
end
def with_prefetch_synchronization
with_eager_load_of(:synchronization)
self
end
def with_type(type)
@filtering_params[:type] = type
self
end
alias with_types with_type
def with_locked(locked)
@filtering_params[:locked] = locked
self
end
def with_current_user_id(user_id)
@current_user_id = user_id
end
def with_order(order, direction = 'asc')
@order = order.to_s
@direction = direction.to_s
self
end
def with_partial_match(tainted_search_pattern)
@filtering_params[:tainted_search_pattern] = tainted_search_pattern
self
end
def with_tags(tags)
@filtering_params[:tags] = tags
self
end
def with_bounding_box(bounding_box)
@filtering_params[:bounding_box] = bounding_box
self
end
def with_display_name
@filtering_params[:only_with_display_name] = true
self
end
def with_organization_id(organization_id)
@filtering_params[:organization_id] = organization_id
self
end
# Published: see `Carto::Visualization#published?`
def with_published
@filtering_params[:only_published] = true
self
end
def with_version(version)
@filtering_params[:version] = version
self
end
def build
offdatabase_order? ? build_regular : build_subquery
end
def count
filtered_query.count
end
def build_paged(page = 1, per_page = 20)
offdatabase_order? ? build_regular(page, per_page) : build_subquery(page, per_page)
end
def filtered_query
query = Carto::Visualization.all
Carto::VisualizationQueryFilterer.new(query).filter(@filtering_params)
end
private
def build_regular(page = nil, per_page = nil)
query = filtered_query
query = with_associations(query)
query = order_query(query)
query = query.offset((page.to_i - 1) * per_page.to_i).limit(per_page.to_i) if page && per_page
query
end
def build_subquery(page = nil, per_page = nil)
subquery = with_ordering_associations(filtered_query)
subquery = order_query(subquery)
subquery = subquery.offset((page.to_i - 1) * per_page.to_i).limit(per_page.to_i) if page && per_page
# Fetching related tables after filtering the results for better performance
query = Carto::Visualization.from(subquery, 'visualizations')
with_associations(query)
end
def order_query(query)
# Search has its own ordering criteria
return query if @tainted_search_pattern
orderer = Carto::VisualizationQueryOrderer.new(query)
orderer.order(@order, @direction)
end
def with_include_of(association)
@include_associations << association
self
end
def with_eager_load_of(association)
@eager_load_associations << association
self
end
def with_associations(query)
query = query.includes(@include_associations) unless @include_associations.empty?
query = query.eager_load(@eager_load_associations) unless @eager_load_associations.empty?
query
end
def with_ordering_associations(query)
query = with_favorited(query) unless @filtering_params[:liked_by_user_id]
query = with_dependent_visualization_count(query)
query
end
def with_favorited(query)
# We have to include favorites if we're not filtering by them
# Why? Both of them include a join with the likes table: favorited uses
# a left-join one and the filter will use an inner-join.
# So what is the problem? It'll fail because is not possible to include two
# joins for the same table
# And what is the difference?
# - Filtering leaves only the favorited/liked visualizations by the user
# - With favorited we add the like/favorite data to the visualization information
return query unless @order&.include?('favorited') && @current_user_id
Carto::VisualizationQueryIncluder.new(query).include_favorited(@current_user_id)
end
def with_dependent_visualization_count(query)
return query unless @order&.include?('dependent_visualizations')
with_prefetch_dependent_visualizations
Carto::VisualizationQueryIncluder.new(query).include_dependent_visualization_count(@filtering_params)
end
def offdatabase_order?
Carto::VisualizationQueryOrderer::SUPPORTED_OFFDATABASE_ORDERS.any? do |offdatabase_order|
@order&.include?(offdatabase_order)
end
end
end