Browse Source

Feature: Mobile - Add a service layer to easily share code between application controllers and GraphQL queries/mutations/subscriptions.

Florian Liebe 2 years ago
parent
commit
ed3bee6e68

+ 1 - 0
app/controllers/application_controller.rb

@@ -15,4 +15,5 @@ class ApplicationController < ActionController::Base
   include ApplicationController::LogsHttpAccess
   include ApplicationController::Authorizes
   include ApplicationController::Klass
+  include ApplicationController::HandlesServices
 end

+ 17 - 0
app/controllers/application_controller/handles_services.rb

@@ -0,0 +1,17 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+module ApplicationController::HandlesServices
+  extend ActiveSupport::Concern
+
+  included do
+    # Easy build method to directly get a service object for a defined class.
+    def use_service(klass)
+      klass.new(current_user: current_user)
+    end
+
+    # Easy build method to directly call the 'execute' method of a service.
+    def execute_service(klass, ...)
+      use_service(klass).execute(...)
+    end
+  end
+end

+ 11 - 111
app/controllers/search_controller.rb

@@ -22,89 +22,19 @@ class SearchController < ApplicationController
                 Setting.get('models_searchable')
               end
 
-    # get priorities of result
-    objects_in_order = []
-    objects_in_order_hash = {}
-    objects.each do |object|
-      local_class = object.constantize
-      preferences = local_class.search_preferences(current_user)
-      next if !preferences
-
-      objects_in_order_hash[preferences[:prio]] = local_class
-    end
-    objects_in_order_hash.keys.sort.reverse_each do |prio|
-      objects_in_order.push objects_in_order_hash[prio]
-    end
-
-    generic_search_params = {
-      query:        query,
-      limit:        limit,
-      current_user: current_user,
-      ids:          params[:ids],
-    }
-
-    # try search index backend
     assets = {}
     result = []
-    if SearchIndexBackend.enabled?
-
-      # get direct search index based objects
-      objects_with_direct_search_index = []
-      objects_without_direct_search_index = []
-      objects.each do |object|
-        preferences = object.constantize.search_preferences(current_user)
-        next if !preferences
-
-        if preferences[:direct_search_index]
-          objects_with_direct_search_index.push object
-        else
-          objects_without_direct_search_index.push object
-        end
-      end
-
-      # do only one query to index search backend
-      if objects_with_direct_search_index.present?
-        items = SearchIndexBackend.search(query, objects_with_direct_search_index, limit: limit, ids: params[:ids])
-        items.each do |item|
-          local_class = item[:type].constantize
-          record = local_class.lookup(id: item[:id])
-          next if !record
-
-          assets = record.assets(assets)
-          item[:type] = local_class.to_app_model.to_s
-          result.push item
-        end
-      end
-
-      # e. g. do ticket query by Ticket class to handle ticket permissions
-      objects_without_direct_search_index.each do |object|
-        object_result = search_generic_backend(object.constantize, assets, generic_search_params)
-        if object_result.present?
-          result.concat(object_result)
-        end
-      end
-
-      # sort order by object priority
-      result_in_order = []
-      objects_in_order.each do |object|
-        result.each do |item|
-          next if item[:type] != object.to_app_model.to_s
-
-          item[:id] = item[:id].to_i
-          result_in_order.push item
-        end
-      end
-      result = result_in_order
-
-    else
-
-      # do query
-      objects_in_order.each do |object|
-        object_result = search_generic_backend(object, assets, generic_search_params)
-        if object_result.present?
-          result.concat(object_result)
-        end
-      end
+    execute_service(
+      SearchService,
+      term:    query,
+      objects: objects.map(&:constantize),
+      options: { limit: limit, ids: params[:ids] },
+    ).each do |item|
+      assets = item.assets(assets)
+      result << {
+        type: item.class.to_app_model.to_s,
+        id:   item[:id],
+      }
     end
 
     render json: {
@@ -112,34 +42,4 @@ class SearchController < ApplicationController
       result: result,
     }
   end
-
-  private
-
-=begin
-
-search generic backend
-
-  SearchController#search_generic_backend(
-    Ticket, # object
-    {}, # assets
-    query:        "search query",
-    limit:        10,
-    current_user: user,
-  )
-
-=end
-
-  def search_generic_backend(object, assets, params)
-    found_objects = object.search(params)
-    result = []
-    found_objects.each do |found_object|
-      item = {
-        id:   found_object.id,
-        type: found_object.class.to_app_model.to_s
-      }
-      result.push item
-      assets = found_object.assets(assets)
-    end
-    result
-  end
 end

+ 7 - 41
app/controllers/users_controller.rb

@@ -767,53 +767,19 @@ curl http://localhost/api/v1/users/avatar -v -u #{login}:#{password} -H "Content
 =end
 
   def avatar_new
-    # get & validate image
-    begin
-      file_full = StaticAssets.data_url_attributes(params[:avatar_full])
-    rescue
-      render json: { error: __('The full-size image is invalid.') }, status: :unprocessable_entity
-      return
-    end
-
-    web_image_content_types = Rails.application.config.active_storage.web_image_content_types
-
-    if web_image_content_types.exclude?(file_full[:mime_type])
-      render json: { error: __('The MIME type of the full-size image is invalid.') }, status: :unprocessable_entity
-      return
-    end
-
-    begin
-      file_resize = StaticAssets.data_url_attributes(params[:avatar_resize])
-    rescue
-      render json: { error: __('The resized image is invalid.') }, status: :unprocessable_entity
+    file_full = execute_service(Avatar::ImageValidateService, image_data: params[:avatar_full])
+    if file_full[:error].present?
+      render json: { error: file_full[:message] }, status: :unprocessable_entity
       return
     end
 
-    if web_image_content_types.exclude?(file_resize[:mime_type])
-      render json: { error: __('The MIME type of the resized image is invalid.') }, status: :unprocessable_entity
+    file_resize = execute_service(Avatar::ImageValidateService, image_data: params[:avatar_resize])
+    if file_resize[:error].present?
+      render json: { error: file_resize[:message] }, status: :unprocessable_entity
       return
     end
 
-    avatar = Avatar.add(
-      object:    'User',
-      o_id:      current_user.id,
-      full:      {
-        content:   file_full[:content],
-        mime_type: file_full[:mime_type],
-      },
-      resize:    {
-        content:   file_resize[:content],
-        mime_type: file_resize[:mime_type],
-      },
-      source:    "upload #{Time.zone.now}",
-      deletable: true,
-    )
-
-    # update user link
-    user = User.find(current_user.id)
-    user.update!(image: avatar.store_hash)
-
-    render json: { avatar: avatar }, status: :ok
+    render json: { avatar: execute_service(Avatar::AddService, full_image: file_full, resize_image: file_resize) }, status: :ok
   end
 
   def avatar_set_default

+ 17 - 0
app/graphql/gql/concerns/handles_services.rb

@@ -0,0 +1,17 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+module Gql::Concerns::HandlesServices
+  extend ActiveSupport::Concern
+
+  included do
+    # Easy build method to directly get a service object for a defined class.
+    def use_service(klass)
+      klass.new(current_user: context.current_user)
+    end
+
+    # Easy build method to directly call the 'execute' method of a service.
+    def execute_service(klass, ...)
+      use_service(klass).execute(...)
+    end
+  end
+end

+ 5 - 28
app/graphql/gql/mutations/account/avatar/add.rb

@@ -12,36 +12,13 @@ module Gql::Mutations
       file_full   = images[:full]
       file_resize = images[:resize]
 
-      if file_full[:error_message].present? || file_resize[:error_message].present?
-        return error_response({
-                                message: file_full[:error_message] || file_resize[:error_message]
-                              })
+      if file_full[:error].present? || file_resize[:error].present?
+        return error_response({ message: file_full[:message] || file_resize[:message] })
       end
 
-      { avatar: store_avatar(file_full, file_resize) }
-    end
-
-    private
-
-    def store_avatar(file_full, file_resize)
-      avatar = Avatar.add(
-        object:    'User',
-        o_id:      context.current_user.id,
-        full:      {
-          content:   file_full[:content],
-          mime_type: file_full[:mime_type],
-        },
-        resize:    {
-          content:   file_resize[:content],
-          mime_type: file_resize[:mime_type],
-        },
-        source:    "upload #{Time.zone.now}",
-        deletable: true,
-      )
-
-      context.current_user.update!(image: avatar.store_hash)
-
-      avatar
+      {
+        avatar: execute_service(Avatar::AddService, full_image: file_full, resize_image: file_resize)
+      }
     end
   end
 end

+ 1 - 0
app/graphql/gql/mutations/base_mutation.rb

@@ -5,6 +5,7 @@ module Gql::Mutations
   class BaseMutation < GraphQL::Schema::Mutation
     include Gql::Concerns::HandlesAuthorization
     include Gql::Concerns::HasNestedGraphqlName
+    include Gql::Concerns::HandlesServices
 
     include Gql::Mutations::Concerns::HandlesObjectAttributeValues
     include Gql::Mutations::Concerns::HandlesCoreWorkflow

+ 1 - 0
app/graphql/gql/queries/base_query.rb

@@ -4,6 +4,7 @@ module Gql::Queries
   class BaseQuery < GraphQL::Schema::Resolver
     include Gql::Concerns::HandlesAuthorization
     include Gql::Concerns::HasNestedGraphqlName
+    include Gql::Concerns::HandlesServices
 
     # Require authentication by default for queries.
     def self.authorize(_obj, ctx)

+ 6 - 54
app/graphql/gql/queries/search.rb

@@ -12,60 +12,12 @@ module Gql::Queries
     type [Gql::Types::SearchResultType, { null: false }], null: false
 
     def resolve(search:, only_in: nil, limit: 10)
-      if SearchIndexBackend.enabled?
-        # Performance optimization: some models may allow combining their Elasticsearch queries into one.
-        result_by_model = combined_backend_search(search: search, only_in: only_in, limit: limit)
-
-        # Other models require dedicated handling, e.g. for permission checks.
-        result_by_model.merge!(models(only_in: only_in, direct_search_index: false).index_with do |model|
-          model_search(model: model, search: search, limit: limit)
-        end)
-
-        # Finally, sort by object priority.
-        models(only_in: only_in).map do |model|
-          result_by_model[model]
-        end.flatten
-      else
-        models(only_in: only_in).map do |model|
-          model_search(model: model, search: search, limit: limit)
-        end.flatten
-      end
-    end
-
-    private
-
-    # Perform a direct, cross-module Elasticsearch query and map the results by class.
-    def combined_backend_search(search:, only_in:, limit:)
-      result_by_model = {}
-      models_with_direct_search_index = models(only_in: only_in, direct_search_index: true).map(&:to_s)
-      if models_with_direct_search_index
-        SearchIndexBackend.search(search, models_with_direct_search_index, limit: limit).each do |item|
-          klass = "::#{item[:type]}".constantize
-          record = klass.lookup(id: item[:id])
-          (result_by_model[klass] ||= []).push(record) if record
-        end
-      end
-      result_by_model
-    end
-
-    # Call the model specific search, which will query Elasticsearch if available,
-    #   or the Database otherwise.
-    def model_search(model:, search:, limit:)
-      model.search({ query: search, limit: limit, current_user: context.current_user })
-    end
-
-    # Get a prioritized list of searchable models
-    def models(only_in:, direct_search_index: nil)
-      models = only_in ? [only_in] : Gql::Types::SearchResultType.searchable_models
-      models.select do |model|
-        prefs = model.search_preferences(context.current_user)
-        next false if !prefs
-        next false if direct_search_index.present? && !prefs[:direct_search_index] != direct_search_index
-
-        true
-      end.sort_by do |model|
-        model.search_preferences(context.current_user)[:prio]
-      end
+      execute_service(
+        SearchService,
+        term:    search,
+        objects: only_in ? [only_in] : Gql::Types::SearchResultType.searchable_models,
+        options: { limit: limit }
+      )
     end
   end
 end

+ 2 - 0
app/graphql/gql/types/base_input_object.rb

@@ -2,6 +2,8 @@
 
 module Gql::Types
   class BaseInputObject < GraphQL::Schema::InputObject
+    include Gql::Concerns::HandlesServices
+
     argument_class Gql::Types::BaseArgument
   end
 end

Some files were not shown because too many files changed in this diff