Skip to content

Commit

Permalink
chore[Op#57902]: Introduce base bulk create service
Browse files Browse the repository at this point in the history
DRY up common logic in:

  - `CustomFields::CustomFieldProjects::BulkCreateService`
  - `ProjectCustomFieldProjectMappings::BulkCreateService`
  - `::Storages::ProjectStorages::BulkCreateService`
  • Loading branch information
akabiru committed Sep 20, 2024
1 parent 39a1a75 commit 155370c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 158 deletions.
153 changes: 153 additions & 0 deletions app/services/bulk_services/project_mappings/base_create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module BulkServices
module ProjectMappings
class BaseCreateService < ::BaseServices::BaseCallable
def initialize(user:, projects:, model:, include_sub_projects: false)
super()
@user = user
@projects = projects
@model = model
@include_sub_projects = include_sub_projects
end

def perform(params = {})
service_call = validate_permissions
service_call = validate_contract(service_call, incoming_mapping_ids, params) if service_call.success?
service_call = perform_bulk_create(service_call) if service_call.success?
service_call = after_perform(service_call, params) if service_call.success?

service_call
end

private

def validate_permissions
return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty?

if @user.allowed_in_project?(permission, incoming_projects)
ServiceResult.success
else
ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized"))
end
end

def validate_contract(service_call, project_ids, _params)
set_attributes_results = project_ids.map do |id|
set_attributes(project_id: id, model_foreign_key_id => @model.id)
end

if (failures = set_attributes_results.select(&:failure?)).any?
service_call.success = false
service_call.errors = failures.map(&:errors)
else
service_call.result = set_attributes_results.map(&:result)
end

service_call
end

def perform_bulk_create(service_call)
mapping_model_class.insert_all(
service_call.result.map { |model| model.attributes.slice("project_id", model_foreign_key_id.to_s) },
unique_by: %i[project_id].push(model_foreign_key_id.to_sym)
)

service_call
end

def after_perform(service_call, _params)
service_call # Subclasses can override this method to add additional logic
end

def incoming_mapping_ids
project_ids = incoming_projects.pluck(:id)
project_ids - existing_project_mappings(project_ids)
end

def incoming_projects
@projects.each_with_object(Set.new) do |project, projects_set|
next unless project.active?

projects_set << project
projects_set.merge(project.active_subprojects.to_a) if @include_sub_projects
end.to_a
end

def existing_project_mappings(project_ids)
mapping_model_class.where(
model_foreign_key_id => @model.id,
project_id: project_ids
).pluck(:project_id)
end

def set_attributes(params)
attributes_service_class
.new(user: @user,
model: instance(params),
contract_class: default_contract_class,
contract_options: {})
.call(params)
end

def instance(params)
mapping_model_class.new(params)
end

# @return [Symbol] the permission required to create the mapping
def permission
raise NotImplementedError
end

# @return [Symbol] the column name of the mapping
def model_foreign_key_id
raise NotImplementedError
end

# @return [Class] the model class of the mapping
def mapping_model_class
raise NotImplementedError
end

def attributes_service_class
"#{namespace}::SetAttributesService".constantize
end

def default_contract_class
"#{namespace}::UpdateContract".constantize
end

def namespace
self.class.name.deconstantize.pluralize
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -30,96 +30,14 @@

module CustomFields
module CustomFieldProjects
class BulkCreateService < ::BaseServices::BaseCallable
class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService
def initialize(user:, projects:, custom_field:, include_sub_projects: false)
super()
@user = user
@projects = projects
@custom_field = custom_field
@include_sub_projects = include_sub_projects
super(user:, projects:, model: custom_field, include_sub_projects:)
end

def perform
service_call = validate_permissions
service_call = validate_contract(service_call, incoming_mapping_ids) if service_call.success?
service_call = perform_bulk_create(service_call) if service_call.success?

service_call
end

private

def validate_permissions(permission: :select_custom_fields)
return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty?

if @user.allowed_in_project?(permission, incoming_projects)
ServiceResult.success
else
ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized"))
end
end

def validate_contract(service_call, project_ids)
set_attributes_results = project_ids.map do |id|
set_attributes(project_id: id, custom_field_id: @custom_field.id)
end

if (failures = set_attributes_results.select(&:failure?)).any?
service_call.success = false
service_call.errors = failures.map(&:errors)
else
service_call.result = set_attributes_results.map(&:result)
end

service_call
end

def perform_bulk_create(service_call)
custom_field_project_mapping_class.insert_all(
service_call.result.map { |model| model.attributes.slice("project_id", "custom_field_id") },
unique_by: %i[project_id custom_field_id]
)

service_call
end

def incoming_mapping_ids
project_ids = incoming_projects.pluck(:id)
project_ids - existing_project_mappings(project_ids)
end

def incoming_projects
@projects.each_with_object(Set.new) do |project, projects_set|
next unless project.active?

projects_set << project
projects_set.merge(project.active_subprojects.to_a) if @include_sub_projects
end.to_a
end

def existing_project_mappings(project_ids)
custom_field_project_mapping_class.where(
custom_field_id: @custom_field.id,
project_id: project_ids
).pluck(:project_id)
end

def set_attributes(params)
attributes_service_class
.new(user: @user,
model: instance(params),
contract_class: default_contract_class,
contract_options: {})
.call(params)
end

def instance(params)
custom_field_project_mapping_class.new(params)
end

def attributes_service_class = CustomFields::CustomFieldProjects::SetAttributesService
def default_contract_class = CustomFields::CustomFieldProjects::UpdateContract
def custom_field_project_mapping_class = CustomFieldsProject
def permission = :select_custom_fields
def model_foreign_key_id = :custom_field_id
def mapping_model_class = CustomFieldsProject
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,15 @@
#++

module ProjectCustomFieldProjectMappings
class BulkCreateService < ::CustomFields::CustomFieldProjects::BulkCreateService
class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService
def initialize(user:, projects:, project_custom_field:, include_sub_projects: false)
super(user:, projects:, custom_field: project_custom_field, include_sub_projects:)
super(user:, projects:, model: project_custom_field, include_sub_projects:)
end

private

def validate_permissions(permission: :select_project_custom_fields)
super
end

def attributes_service_class = ProjectCustomFieldProjectMappings::SetAttributesService
def default_contract_class = ProjectCustomFieldProjectMappings::UpdateContract
def custom_field_project_mapping_class = ProjectCustomFieldProjectMapping
def permission = :select_project_custom_fields
def model_foreign_key_id = :custom_field_id
def mapping_model_class = ProjectCustomFieldProjectMapping
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,12 @@
#++

module Storages::ProjectStorages
class BulkCreateService < ::BaseServices::BaseCallable
class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService
def initialize(user:, projects:, storage:, include_sub_projects: false)
super()
@user = user
@projects = projects
@storage = storage
@include_sub_projects = include_sub_projects
super(user:, projects:, model: storage, include_sub_projects:)
end

def perform(params = {})
service_call = validate_permissions
service_call = validate_contract(service_call, incoming_activations_ids, params) if service_call.success?
service_call = perform_bulk_create(service_call) if service_call.success?
def after_perform(service_call, params)
service_call = create_last_project_folders(service_call, params) if service_call.success?
broadcast_project_storages_created(params) if service_call.success?

Expand All @@ -50,21 +43,16 @@ def perform(params = {})

private

def validate_permissions
return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty?

if @user.allowed_in_project?(:manage_files_in_project, incoming_projects)
ServiceResult.success
else
ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized"))
end
end
def permission = :manage_files_in_project
def model_foreign_key_id = :storage_id
def mapping_model_class = ::Storages::ProjectStorage
def default_contract_class = ::Storages::ProjectStorages::CreateContract

def validate_contract(service_call, project_ids, params)
project_folder_params = params.slice(:project_folder_mode, :project_folder_id)

set_attributes_results = project_ids.map do |id|
set_attributes(project_id: id, storage_id: @storage.id, **project_folder_params)
set_attributes(project_id: id, storage_id: @model.id, **project_folder_params)
end

if (failures = set_attributes_results.select(&:failure?)).any?
Expand Down Expand Up @@ -103,49 +91,8 @@ def broadcast_project_storages_created(params)
event: :created,
project_folder_mode: params[:project_folder_mode],
project_folder_mode_previously_was: nil,
storage: @storage
storage: @model
)
end

def incoming_activations_ids
project_ids = incoming_projects.pluck(:id)
project_ids - existing_project_storages(project_ids)
end

def incoming_projects
@projects.each_with_object(Set.new) do |project, projects_set|
next unless project.active?

projects_set << project
projects_set.merge(project.active_subprojects.to_a) if @include_sub_projects
end.to_a
end

def existing_project_storages(project_ids)
::Storages::ProjectStorage
.where(storage_id: @storage.id, project_id: project_ids)
.pluck(:project_id)
end

def set_attributes(params)
attributes_service_class
.new(user: @user,
model: instance(params),
contract_class: default_contract_class,
contract_options: {})
.call(params)
end

def instance(params)
::Storages::ProjectStorage.new(params)
end

def attributes_service_class
::Storages::ProjectStorages::SetAttributesService
end

def default_contract_class
::Storages::ProjectStorages::CreateContract
end
end
end

0 comments on commit 155370c

Please sign in to comment.