1# frozen_string_literal: true 2 3# WARNING: This finder does not check permissions! 4# 5# Arguments: 6# params: 7# project: Project model - Find deployments for this project 8# updated_after: DateTime 9# updated_before: DateTime 10# finished_after: DateTime 11# finished_before: DateTime 12# environment: String 13# status: String (see Deployment.statuses) 14# order_by: String (see ALLOWED_SORT_VALUES constant) 15# sort: String (asc | desc) 16class DeploymentsFinder 17 attr_reader :params 18 19 # Warning: 20 # These const are directly used in Deployment Rest API, thus 21 # modifying these values could implicity change the API interface or introduce a breaking change. 22 # Also, if you add a sort value, make sure that the new query will stay 23 # performant with the other filtering/sorting parameters. 24 # The composed query could be significantly slower when the filtering and sorting columns are different. 25 # See https://gitlab.com/gitlab-org/gitlab/-/issues/325627 for example. 26 ALLOWED_SORT_VALUES = %w[id iid created_at updated_at ref finished_at].freeze 27 DEFAULT_SORT_VALUE = 'id' 28 29 ALLOWED_SORT_DIRECTIONS = %w[asc desc].freeze 30 DEFAULT_SORT_DIRECTION = 'asc' 31 32 InefficientQueryError = Class.new(StandardError) 33 34 def initialize(params = {}) 35 @params = params 36 37 validate! 38 end 39 40 def execute 41 items = init_collection 42 items = by_updated_at(items) 43 items = by_finished_at(items) 44 items = by_environment(items) 45 items = by_status(items) 46 items = preload_associations(items) 47 sort(items) 48 end 49 50 private 51 52 def validate! 53 if filter_by_updated_at? && filter_by_finished_at? 54 raise InefficientQueryError, 'Both `updated_at` filter and `finished_at` filter can not be specified' 55 end 56 57 # Currently, the inefficient parameters are allowed in order to avoid breaking changes in Deployment API. 58 # We'll switch to a hard error in https://gitlab.com/gitlab-org/gitlab/-/issues/328500. 59 if (filter_by_updated_at? && !order_by_updated_at?) || (!filter_by_updated_at? && order_by_updated_at?) 60 Gitlab::ErrorTracking.track_and_raise_for_dev_exception( 61 InefficientQueryError.new('`updated_at` filter and `updated_at` sorting must be paired') 62 ) 63 end 64 65 if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?) 66 raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired' 67 end 68 69 if filter_by_finished_at? && !filter_by_successful_deployment? 70 raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.' 71 end 72 73 if params[:environment].present? && !params[:project].present? 74 raise InefficientQueryError, '`environment` filter must be combined with `project` scope.' 75 end 76 end 77 78 def init_collection 79 if params[:project].present? 80 params[:project].deployments 81 elsif params[:group].present? 82 ::Deployment.for_projects(params[:group].all_projects) 83 else 84 ::Deployment.none 85 end 86 end 87 88 def sort(items) 89 sort_params = build_sort_params 90 optimize_sort_params!(sort_params) 91 items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord 92 end 93 94 def by_updated_at(items) 95 items = items.updated_before(params[:updated_before]) if params[:updated_before].present? 96 items = items.updated_after(params[:updated_after]) if params[:updated_after].present? 97 98 items 99 end 100 101 def by_finished_at(items) 102 items = items.finished_before(params[:finished_before]) if params[:finished_before].present? 103 items = items.finished_after(params[:finished_after]) if params[:finished_after].present? 104 105 items 106 end 107 108 def by_environment(items) 109 if params[:project].present? && params[:environment].present? 110 items.for_environment_name(params[:project], params[:environment]) 111 else 112 items 113 end 114 end 115 116 def by_status(items) 117 return items unless params[:status].present? 118 119 unless Deployment.statuses.key?(params[:status]) 120 raise ArgumentError, "The deployment status #{params[:status]} is invalid" 121 end 122 123 items.for_status(params[:status]) 124 end 125 126 def build_sort_params 127 order_by = ALLOWED_SORT_VALUES.include?(params[:order_by]) ? params[:order_by] : DEFAULT_SORT_VALUE 128 order_direction = ALLOWED_SORT_DIRECTIONS.include?(params[:sort]) ? params[:sort] : DEFAULT_SORT_DIRECTION 129 130 { order_by => order_direction } 131 end 132 133 def optimize_sort_params!(sort_params) 134 sort_direction = sort_params.each_value.first 135 136 # Implicitly enforce the ordering when filtered by `updated_at` column for performance optimization. 137 # See https://gitlab.com/gitlab-org/gitlab/-/issues/325627#note_552417509. 138 # We remove this in https://gitlab.com/gitlab-org/gitlab/-/issues/328500. 139 if filter_by_updated_at? 140 sort_params.replace('updated_at' => sort_direction) 141 end 142 143 if sort_params['created_at'] || sort_params['iid'] 144 # Sorting by `id` produces the same result as sorting by `created_at` or `iid` 145 sort_params.replace(id: sort_direction) 146 elsif sort_params['updated_at'] 147 # This adds the order as a tie-breaker when multiple rows have the same updated_at value. 148 # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20848. 149 sort_params.merge!(id: sort_direction) 150 end 151 end 152 153 def filter_by_updated_at? 154 params[:updated_before].present? || params[:updated_after].present? 155 end 156 157 def filter_by_finished_at? 158 params[:finished_before].present? || params[:finished_after].present? 159 end 160 161 def filter_by_successful_deployment? 162 params[:status].to_s == 'success' 163 end 164 165 def order_by_updated_at? 166 params[:order_by].to_s == 'updated_at' 167 end 168 169 def order_by_finished_at? 170 params[:order_by].to_s == 'finished_at' 171 end 172 173 # rubocop: disable CodeReuse/ActiveRecord 174 def preload_associations(scope) 175 scope.includes( 176 :user, 177 environment: [], 178 deployable: { 179 job_artifacts: [], 180 pipeline: { 181 project: { 182 route: [], 183 namespace: :route 184 } 185 }, 186 project: { 187 namespace: :route 188 } 189 } 190 ) 191 end 192 # rubocop: enable CodeReuse/ActiveRecord 193end 194