aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib/active_storage/attached/model.rb
blob: 4bd6b56b6c6591bdb2836977ceb7164299a9354c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# frozen_string_literal: true

require "active_support/core_ext/object/try"

module ActiveStorage
  # Provides the class-level DSL for declaring an Active Record model's attachments.
  module Attached::Model
    extend ActiveSupport::Concern

    class_methods do
      # Specifies the relation between a single attachment and the model.
      #
      #   class User < ActiveRecord::Base
      #     has_one_attached :avatar
      #   end
      #
      # There is no column defined on the model side, Active Storage takes
      # care of the mapping between your records and the attachment.
      #
      # To avoid N+1 queries, you can include the attached blobs in your query like so:
      #
      #   User.with_attached_avatar
      #
      # Under the covers, this relationship is implemented as a +has_one+ association to a
      # ActiveStorage::Attachment record and a +has_one-through+ association to a
      # ActiveStorage::Blob record. These associations are available as +avatar_attachment+
      # and +avatar_blob+. But you shouldn't need to work with these associations directly in
      # most circumstances.
      #
      # The system has been designed to having you go through the ActiveStorage::Attached::One
      # proxy that provides the dynamic proxy to the associations and factory methods, like +attach+.
      #
      # If the +:dependent+ option isn't set, the attachment will be purged
      # (i.e. destroyed) whenever the record is destroyed.
      def has_one_attached(name, dependent: :purge_later)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
            @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self)
          end

          def #{name}=(attachable)
            attachment_changes["#{name}"] =
              if attachable.nil?
                ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
              else
                ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
              end
          end
        CODE

        has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
        has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob

        scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }

        after_save { attachment_changes[name.to_s]&.save }

        after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }

        ActiveRecord::Reflection.add_attachment_reflection(
          self,
          name,
          ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
        )
      end

      # Specifies the relation between multiple attachments and the model.
      #
      #   class Gallery < ActiveRecord::Base
      #     has_many_attached :photos
      #   end
      #
      # There are no columns defined on the model side, Active Storage takes
      # care of the mapping between your records and the attachments.
      #
      # To avoid N+1 queries, you can include the attached blobs in your query like so:
      #
      #   Gallery.where(user: Current.user).with_attached_photos
      #
      # Under the covers, this relationship is implemented as a +has_many+ association to a
      # ActiveStorage::Attachment record and a +has_many-through+ association to a
      # ActiveStorage::Blob record. These associations are available as +photos_attachments+
      # and +photos_blobs+. But you shouldn't need to work with these associations directly in
      # most circumstances.
      #
      # The system has been designed to having you go through the ActiveStorage::Attached::Many
      # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+.
      #
      # If the +:dependent+ option isn't set, all the attachments will be purged
      # (i.e. destroyed) whenever the record is destroyed.
      def has_many_attached(name, dependent: :purge_later)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
            @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self)
          end

          def #{name}=(attachables)
            if ActiveStorage.replace_on_assign_to_many
              attachment_changes["#{name}"] =
                if attachables.nil? || Array(attachables).none?
                  ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
                else
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
                end
            else
              if !attachables.nil? || Array(attachables).any?
                attachment_changes["#{name}"] =
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
              end
            end
          end
        CODE

        has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: :destroy do
          def purge
            each(&:purge)
            reset
          end

          def purge_later
            each(&:purge_later)
            reset
          end
        end
        has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob

        scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }

        after_save { attachment_changes[name.to_s]&.save }

        after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }

        ActiveRecord::Reflection.add_attachment_reflection(
          self,
          name,
          ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
        )
      end
    end

    def attachment_changes #:nodoc:
      @attachment_changes ||= {}
    end

    def reload(*) #:nodoc:
      super.tap { @attachment_changes = nil }
    end
  end
end