#require 'fiona7/write_obj' #require 'fiona7/released_obj' #require 'fiona7/edited_obj' require 'fiona7/builder/obj_builder' require 'fiona7/builder/obj_updater' require 'fiona7/naive_search_engine' require 'fiona7/verity_search_engine' require 'fiona7/facet_builder' require 'fiona7/assert' require 'fiona7/json/reverse_obj_decorator' require 'fiona7/json/typeless_obj_decorator' require 'fiona7/prefetch/obj_prefetch' require 'fiona7/prefetch/widget_resolver_prefetch' module Fiona7 module Controllers module RestAPI class ObjController def create(workspace_id, values) assert_writable(workspace_id) # some stupid creators give us an id. fck them values[:obj].delete("_id") values[:obj].delete(:_id) path = values[:obj][:_path] || values[:obj]["_path"] # This is a hack to make it seem as if the root obj does not exist if (path == "/") || ( obj = Fiona7::WriteObj.where(obj_class: 'X_Container', path: path).first ) obj = Builder::ObjUpdater.new(values[:obj].merge(_id: ((obj && obj.id) || 2001))).build else obj = Builder::ObjBuilder.new(values[:obj].dup).build end klass = EditedObj decorated = Fiona7::JSON::ReverseObjDecorator.new(klass, obj) ::Fiona7.run_callbacks(:create_obj, obj.id) return ::JSON.parse(::ActiveSupport::JSON.encode(decorated)) end def fetch_by_path(workspace_id, path) klass = matching_class(workspace_id) path = path.gsub('.', '_') obj = klass.find_by_path(path) Assert.input( !obj.nil?, "Obj with path #{path} not found in workspace #{workspace_id}", 404 ) decorated = Fiona7::JSON::ReverseObjDecorator.new(klass, obj) return ::JSON.parse(::ActiveSupport::JSON.encode(decorated)) end def fetch_multiple(revision_id, payload) # TODO: use implementation from ContentService controller # FIXME: code duplication with fetch_by_id obj_ids = payload.with_indifferent_access[:ids] klass = revision_id.start_with?('f') ? ReleasedObj : EditedObj # preloads all objects in one query objs_arr = Prefetch::ObjPrefetch.new(klass, obj_ids).find_many(obj_ids) # preloads all widgets in one query widget_prefetch = Prefetch::WidgetResolverPrefetch.new(klass, objs_arr.compact) decorated = objs_arr.map {|obj| obj ? Fiona7::JSON::ReverseObjDecorator.new(klass, obj, widget_prefetch.widget_resolver(obj)) : nil } response = ::JSON.parse(::ActiveSupport::JSON.encode({"results" => decorated})) return response end def fetch_multiple2(workspace_id, payload) revision_id = workspace_id == 'published' ? 'f' : 'b' return fetch_multiple(revision_id, payload) end def fetch_by_id(workspace_id, obj_id) klass = matching_class(workspace_id) obj = klass.where(obj_id: obj_id).first Assert.input( !obj.nil?, "Obj with id #{obj_id} not found in workspace #{workspace_id}", 404 ) decorated = Fiona7::JSON::ReverseObjDecorator.new(klass, obj) response = ::JSON.parse(::ActiveSupport::JSON.encode(decorated)) return response end def fetch_by_id_from_revision(revision_id, obj_id) # TODO: use implementation from ContentService controller # FIXME: code duplication with fetch_by_id klass = revision_id.start_with?('f') ? ReleasedObj : EditedObj obj = klass.where(obj_id: obj_id).first Assert.input( !obj.nil?, "Obj with id #{obj_id} not found in revision #{revision_id}", 404 ) decorated = Fiona7::JSON::ReverseObjDecorator.new(klass, obj) return ::JSON.parse(::ActiveSupport::JSON.encode(decorated)) end def search(workspace_id, params) klass = matching_class(workspace_id) params = params.symbolize_keys facets = params[:facets] || [] if params[:continuation] conti = ::JSON.parse(Base64.decode64(params[:continuation])).symbolize_keys #rescue {} else conti = {} end # This simulates the behavior of the backend: # In absence of other input backend always sorts by ID # or SCORE if an elastic search for such request would # produce a score. if (params[:query]||[]).any?{|op| (op[:operator]||op["operator"]).to_s =~ /search/ } default_sort_order = :desc default_sort_by = :_score else default_sort_order = :asc default_sort_by = :id end offset = conti[:offset] || params[:offset] || 0 # 10 is the default value in original implementation size = conti[:size] || params[:size] || 10 order = (conti[:sort_order] || params[:sort_order] || default_sort_order).to_sym sort = (conti[:sort_by] || params[:sort_by] || default_sort_by).to_sym if params[:query].present? query = normalize_query_params(params[:query]) Rails.logger.debug "Executing search for: #{query.inspect}" if use_naive_search_engine?(query) Rails.logger.debug "Using DB-native lookup for search" search = NaiveSearchEngine.new(klass, query, offset, size, sort, order) else Rails.logger.debug "Using configured search engine: #{::Fiona7.search_engine.name}" search = ::Fiona7.search_engine.new(klass, query, offset, size, sort, order) end result = search.results.map {|o_id| {id: o_id} } total = search.total response = { total: total, results: result } else query = [] response = { total: 0, results: [] } end if response[:total] > offset + size response[:continuation] = Base64.encode64({ size: size, offset: (offset+size), sort_order: order, sort_by: sort, }.to_json) end if !facets.empty? if Fiona7.facetting_enabled? response[:facets] = facets.map {|facet| FacetBuilder.new(facet.symbolize_keys, query, klass).build } else response[:facets] = [] end end return ::JSON.parse(::ActiveSupport::JSON.encode(response)) end def update(workspace_id, values) assert_writable(workspace_id) klass = EditedObj id = (values[:obj]["_id"] || values[:obj][:_id]).to_s.to_i # Scrivito SDK does not understand the difference # between create and update if (id < 2001 || id >= 2**31 || !klass.exists?(id)) return self.create(workspace_id, values) end obj = Builder::ObjUpdater.new(values[:obj].dup.with_indifferent_access).build decorated = Fiona7::JSON::ReverseObjDecorator.new(klass, obj) ::Fiona7.run_callbacks(:update_obj, obj.id) return ::JSON.parse(::ActiveSupport::JSON.encode(decorated)) end def destroy(workspace_id, obj_id) # TODO: cleanup /_widgets and /_uploads obj = WriteObj.find(obj_id) if !obj.children.empty? if obj.children.all? {|c| c.name == '_widgets' || c.name == '_uploads' } remove_obj_tree(obj) else Assert.input( false, "Cannot remove object #{obj_id} because it has children objects" ) end else obj.destroy end return {} end protected def remove_obj_tree(rc_obj) subtree_objects = WriteObj.where(["path like ?", "#{rc_obj.path}/%"]) tree = ([rc_obj] + subtree_objects).sort_by{|obj| obj.path}.reverse outside_objects = [] tree.each do |obj| obj.super_objects.each do |super_object| if !super_object.path.start_with?(rc_obj.path) outside_objects << super_object end end end if outside_objects.length != 0 outside_paths = outside_objects.map(&:path).join(" ") Assert.input( false, "Cannot remove object because #{outside_paths} have links pointing to it" ) end tree.each do |obj| obj.send(:crul_obj).remove_archived_contents! obj.send(:crul_obj).remove_active_contents! end tree.each do |obj| obj.send(:crul_obj).delete! end end def matching_class(workspace_id) workspace_id = workspace_id.to_sym Assert.input( workspace_id == :rtc || workspace_id == :published, "Workspace #{workspace_id} not found" ) klass = workspace_id == :published ? ReleasedObj : EditedObj end def assert_writable(workspace_id) workspace_id = workspace_id.to_sym Assert.input( workspace_id == :rtc, "Workspace #{workspace_id} is not writable, only workspace rtc is writable" ) end # This is required because the sdk is inconsistent # in its usage of search operators def normalize_query_params(query_params) result = [] query_params.each do |query_param| new_query_param = query_param.symbolize_keys new_query_param[:operator] = new_query_param[:operator].to_sym if new_query_param[:field].is_a?(Array) new_query_param[:field] = new_query_param[:field].map(&:to_sym) else new_query_param[:field] = new_query_param[:field].to_sym end case query_param[:operator] when :contains new_query_param[:operator] = :search when :contains_prefix new_query_param[:operator] = :prefix_search when :equals new_query_param[:operator] = :equal when :starts_with new_query_param[:operator] = :prefix when :is_greater_than new_query_param[:operator] = :greater_than when :is_less_than new_query_param[:operator] = :less_than end result << new_query_param end result end def use_naive_search_engine?(query) false || query.empty? || query.any? {|q| (q[:field] == :_path || q[:field] == :_name)&& (q[:operator] == :equal || q[:operator] == :prefix || query.length == 1)} || query.any? {|q| q[:field] == :_parent_path && (q[:operator] == :equal || query.length == 1)} || (query.length == 1 && query.first[:field] == :_modification) || (query.length == 1 && query.first[:field] == :_obj_class) || (query.length == 1 && query.first[:field] == :id) || (query.length == 1 && query.first[:field] == :_permalink) || false end end end end end