1# frozen_string_literal: true
2
3# This module is providing helpers for updating `ProjectStatistics` with `after_save` and `before_destroy` hooks.
4#
5# It deals with `ProjectStatistics.increment_statistic` making sure not to update statistics on a cascade delete from the
6# project, and keeping track of value deltas on each save. It updates the DB only when a change is needed.
7#
8# Example:
9#
10# module Ci
11#   class JobArtifact < ApplicationRecord
12#     include UpdateProjectStatistics
13#
14#     update_project_statistics project_statistics_name: :build_artifacts_size
15#   end
16# end
17#
18# Expectation:
19#
20# - `statistic_attribute` must be an ActiveRecord attribute
21# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
22module UpdateProjectStatistics
23  extend ActiveSupport::Concern
24  include AfterCommitQueue
25
26  class_methods do
27    attr_reader :project_statistics_name, :statistic_attribute
28
29    # Configure the model to update `project_statistics_name` on ProjectStatistics,
30    # when `statistic_attribute` changes
31    #
32    # - project_statistics_name: A column of `ProjectStatistics` to update
33    # - statistic_attribute: An attribute of the current model, default to `size`
34    def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
35      @project_statistics_name = project_statistics_name
36      @statistic_attribute = statistic_attribute
37
38      after_save(:update_project_statistics_after_save, if: :update_project_statistics_after_save?)
39      after_destroy(:update_project_statistics_after_destroy, if: :update_project_statistics_after_destroy?)
40    end
41
42    private :update_project_statistics
43  end
44
45  included do
46    private
47
48    def update_project_statistics_after_save?
49      update_project_statistics_attribute_changed?
50    end
51
52    def update_project_statistics_after_destroy?
53      !project_destroyed?
54    end
55
56    def update_project_statistics_after_save
57      attr = self.class.statistic_attribute
58      delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
59
60      schedule_update_project_statistic(delta)
61    end
62
63    def update_project_statistics_attribute_changed?
64      saved_change_to_attribute?(self.class.statistic_attribute)
65    end
66
67    def update_project_statistics_after_destroy
68      delta = -read_attribute(self.class.statistic_attribute).to_i
69
70      schedule_update_project_statistic(delta)
71    end
72
73    def project_destroyed?
74      project.pending_delete?
75    end
76
77    def schedule_update_project_statistic(delta)
78      return if delta == 0
79      return if project.nil?
80
81      run_after_commit do
82        ProjectStatistics.increment_statistic(
83          project, self.class.project_statistics_name, delta)
84      end
85    end
86  end
87end
88