shrine-rom 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 78bc603cf077c91b940f0df8d4cd0304c68b4182b0ae6b086e911eb1107a1465
4
+ data.tar.gz: c0eabd2c656ddd614a21cf729f0c344de88e2dec2874d9c70052262e748b4107
5
+ SHA512:
6
+ metadata.gz: cea2d2928e38caaf138e9246dd5df636aed3e3de01ee7860a1fedec4c69ce5ba197632423d36be05b644d8d383646e9d86b56ff5b40f1f99795f0a575b82db8a
7
+ data.tar.gz: bd9e6a7cf0ac72be3ae91a3ed10665c172f202f45aa6523d53c72b06edba96905e24933a1dcf8246af691821e756706afb306a4f08837f1a22382c6973479d0f
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Janko Marohnić
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # Shrine::Plugins::Rom
2
+
3
+ Provides [ROM] integration for [Shrine].
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ $ bundle add shrine-rom
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ Let's asume we have "photos" that have an "image" attachment. We start by
14
+ configuring Shrine in our initializer, and loading the `rom` plugin provided by
15
+ shrine-rom:
16
+
17
+ ```rb
18
+ # Gemfile
19
+ gem "shrine", "~> 3.0"
20
+ gem "shrine-rom"
21
+ ```
22
+ ```rb
23
+ require "shrine"
24
+ require "shrine/storage/file_system"
25
+
26
+ Shrine.storages = {
27
+ cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
28
+ store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), # permanent
29
+ }
30
+
31
+ Shrine.plugin :rom # ROM integration, provided by shrine-rom
32
+ Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
33
+ Shrine.plugin :rack_file # for accepting Rack uploaded file hashes
34
+ Shrine.plugin :form_assign # for assigning file from form fields
35
+ Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
36
+ Shrine.plugin :validation_helpers # for validating uploaded files
37
+ Shrine.plugin :determine_mime_type # determine MIME type from file content
38
+ ```
39
+
40
+ Next, we run a migration that adds an `image_data` text or JSON column to our
41
+ `photos` table:
42
+
43
+ ```rb
44
+ ROM::SQL.migration do
45
+ change do
46
+ add_column :photos, :image_data, :text # or :jsonb
47
+ end
48
+ end
49
+ ```
50
+
51
+ Now we can define an `ImageUploader` class and include an attachment module
52
+ into our `Photo` entity:
53
+
54
+ ```rb
55
+ class ImageUploader < Shrine
56
+ # we add some basic validation
57
+ Attacher.validate do
58
+ validate_max_size 20*1024*1024
59
+ validate_mime_type %w[image/jpeg image/png image/webp]
60
+ validate_extension %w[jpg jpeg png webp]
61
+ end
62
+ end
63
+ ```
64
+ ```rb
65
+ class PhotoRepo < ROM::Repository[:photos]
66
+ commands :create, update: :by_pk, delete: :by_pk
67
+ struct_namespace Entities
68
+
69
+ def find(id)
70
+ photos.fetch(id)
71
+ end
72
+ end
73
+ ```
74
+ ```rb
75
+ module Entities
76
+ class Photo < ROM::Struct
77
+ include ImageUploader::Attachment[:image]
78
+ end
79
+ end
80
+ ```
81
+
82
+ Let's now add fields for our `image` attachment to our HTML form for creating
83
+ photos:
84
+
85
+ ```rb
86
+ # with Forme gem:
87
+ form @photo, action: "/photos", enctype: "multipart/form-data", namespace: "photo" do |f|
88
+ f.input :title, type: :text
89
+ f.input :image, type: :hidden, value: @attacher&.cached_data
90
+ f.input :image, type: :file
91
+ f.button "Create"
92
+ end
93
+ ```
94
+
95
+ Now in our controller we can attach the uploaded file from request params.
96
+ We'll assume you're using [dry-validation] for validating user input.
97
+
98
+ ```rb
99
+ post "/photos" do
100
+ @photo = Entities::Photo.new
101
+ @attacher = @photo.image_attacher
102
+
103
+ @attacher.form_assign(params["photo"]) # assigns file and performs validation
104
+
105
+ contract = CreatePhotoContract.new(image_attacher: @attacher)
106
+ result = contract.call(params["photo"])
107
+
108
+ if result.success?
109
+ @attacher.finalize # upload cached file to permanent storage
110
+
111
+ attributes = result.to_h
112
+ attributes.merge!(@attacher.column_values)
113
+
114
+ photo_repo.create(attributes)
115
+ # ...
116
+ else
117
+ # ... render view with form ...
118
+ end
119
+ end
120
+ ```
121
+ ```rb
122
+ class CreatePhotoContract < Dry::Validation::Contract
123
+ option :image_attacher
124
+
125
+ params do
126
+ required(:title).filled(:string)
127
+ end
128
+
129
+ # copy any attacher's validation errors into our dry-validation contract
130
+ rule(:image) do
131
+ key.failure("must be present") unless image_attacher.attached?
132
+ image_attacher.errors.each { |message| key.failure(message) }
133
+ end
134
+ end
135
+ ```
136
+
137
+ Once the image has been successfully attached to our photo, we can retrieve the
138
+ image URL by calling `#image_url` on the entity:
139
+
140
+ ```erb
141
+ <img src="<%= @photo.image_url %>" />
142
+ ```
143
+
144
+ If you want to see a complete example with direct uploads and backgrounding,
145
+ see the [demo app][demo].
146
+
147
+ ## Understanding
148
+
149
+ The `rom` plugin builds upon Shrine's [`entity`][entity] plugin, providing
150
+ persistence functionality.
151
+
152
+ The attachment module included into the entity provides convenience methods for
153
+ reading the data attribute:
154
+
155
+ ```rb
156
+ photo.image_data #=> '{"id":"path/to/file","storage":"store","metadata":{...}}'
157
+
158
+ photo.image #=> #<Shrine::UploadedFile @id="path/to/file" @storage_key=:store ...>
159
+ photo.image_url #=> "https://s3.amazonaws.com/..."
160
+ photo.image_attacher #=> #<Shrine::Attacher ...>
161
+ ```
162
+
163
+ ### Updating
164
+
165
+ When updating the attached file for an existing record, it's important to
166
+ initialize the attacher from that record's current attachment. That way the old
167
+ file will be automatically deleted on `Attacher#finalize`.
168
+
169
+ ```rb
170
+ photo = photo_repo.find(photo_id)
171
+ photo.image #=> #<Shrine::UploadedFile @id="foo" ...>
172
+
173
+ attacher = photo.image_attacher # has current attachment
174
+ attacher.assign(file)
175
+
176
+ photo_repo.update(photo_id, attacher.column_values)
177
+
178
+ attacher.finalize # deletes previous attachment
179
+ ```
180
+
181
+ ### Attacher state
182
+
183
+ Unlike the [`model`][model] plugin, the `entity` plugin doesn't memoize the
184
+ `Shrine::Attacher` instance:
185
+
186
+ ```rb
187
+ photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe564085d8>
188
+ photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe53b2f378> (different instance)
189
+ ```
190
+
191
+ So, if you want to update the attacher state, you need to first assign it to a
192
+ variable:
193
+
194
+ ```rb
195
+ attacher = photo.image_attacher
196
+ attacher.assign(file)
197
+ attacher.finalize
198
+ ```
199
+
200
+ ### Persisting
201
+
202
+ Normally you'd persist attachment changes explicitly, by using
203
+ `Attacher#column_data` or `Attacher#column_values`:
204
+
205
+ ```rb
206
+ attacher = photo.image_attacher
207
+ attacher.attach(file)
208
+
209
+ photo_repo.create(image_data: attacher.column_data)
210
+ # or
211
+ photo_repo.create(attacher.column_values)
212
+ ```
213
+
214
+ ## Backgrounding
215
+
216
+ If you want to delay promotion into a background job, you need to call
217
+ `Attacher#finalize` _after_ you've persisted the cached file, so that your
218
+ background job is able to retrieve the record. We'll assume your repository
219
+ objects are registered using [dry-container].
220
+
221
+ ```rb
222
+ Shrine.plugin :backgrounding
223
+ Shrine::Attacher.destroy_block { Attachment::DestroyJob.perform_async(self.class, data) }
224
+ ```
225
+ ```rb
226
+ attacher = photo.image_attacher
227
+ attacher.assign(file)
228
+
229
+ photo = photo_repo.create(attacher.column_values)
230
+
231
+ attacher.promote_block do |attacher|
232
+ Attachment::PromoteJob.perform_async(:photo_repo, photo.id, :image, attacher.file_data)
233
+ end
234
+
235
+ attacher.finalize # calls the promote block
236
+ ```
237
+ ```rb
238
+ class Attachment::PromoteJob
239
+ include Sidekiq::Worker
240
+
241
+ def perform(repo_name, record_id, name, file_data)
242
+ repo = Application[repo_name] # retrieve repo from container
243
+ entity = repo.find(record_id)
244
+
245
+ attacher = Shrine::Attacher.retrieve(
246
+ entity: entity,
247
+ name: name,
248
+ file: file_data,
249
+ repository: repo, # repository needs to be passed in
250
+ )
251
+
252
+ attacher.atomic_promote
253
+ rescue Shrine::AttachmentChanged, # attachment has changed
254
+ ROM::TupleCountMismatchError # record has been deleted
255
+ end
256
+ end
257
+ ```
258
+ ```rb
259
+ class Attachment::DestroyJob
260
+ include Sidekiq::Worker
261
+
262
+ def perform(attacher_class, data)
263
+ attacher = Object.const_get(attacher_class).from_data(data)
264
+ attacher.destroy
265
+ end
266
+ end
267
+ ```
268
+
269
+ ## Contributing
270
+
271
+ Tests are run with:
272
+
273
+ ```sh
274
+ $ bundle exec rake test
275
+ ```
276
+
277
+ ## Code of Conduct
278
+
279
+ Everyone interacting in the Shrine::Rom project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/janko/shrine-rom/blob/master/CODE_OF_CONDUCT.md).
280
+
281
+ ## License
282
+
283
+ [MIT](/LICENSE.txt)
284
+
285
+ [ROM]: https://rom-rb.org
286
+ [Shrine]: https://shrinerb.com
287
+ [entity]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/entity.md#readme
288
+ [model]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/model.md#readme
289
+ [dry-validation]: https://dry-rb.org/gems/dry-validation/
290
+ [dry-container]: https://dry-rb.org/gems/dry-container/
291
+ [demo]: /demo
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ module Rom
6
+ def self.load_dependencies(uploader)
7
+ uploader.plugin :entity
8
+ uploader.plugin :_persistence, plugin: self
9
+ end
10
+
11
+ module AttachmentMethods
12
+ # Disables model behaviour for ROM::Struct and Hanami::Entity
13
+ # subclasses.
14
+ def included(klass)
15
+ @model = false if klass < ::Dry::Struct
16
+ super
17
+ end
18
+ end
19
+
20
+ module AttacherMethods
21
+ attr_reader :repository
22
+
23
+ def initialize(repository: nil, **options)
24
+ super(**options)
25
+ @repository = repository
26
+ end
27
+
28
+ # The _persistence plugin uses #rom_persist, #rom_reload and #rom? to
29
+ # implement the following methods:
30
+ #
31
+ # * Attacher#persist
32
+ # * Attacher#atomic_persist
33
+ # * Attacher#atomic_promote
34
+ private
35
+
36
+ # Updates the record with attachment column values. Used by the
37
+ # _persistence plugin.
38
+ def rom_persist
39
+ rom.update_record(column_values)
40
+ end
41
+
42
+ # Locks the database row and yields the reloaded record. Used by the
43
+ # _persistence plugin.
44
+ def rom_reload
45
+ rom.retrieve_record { |entity| yield entity }
46
+ end
47
+
48
+ # Returns true if the data attribute represents a JSON or JSONB column.
49
+ # Used by the _persistence plugin to determine whether serialization
50
+ # should be skipped.
51
+ def rom_hash_attribute?
52
+ return false unless repository
53
+
54
+ column = rom.column_type(attribute)
55
+ column && [:json, :jsonb].include?(column.to_sym)
56
+ end
57
+
58
+ # Returns whether the record is a ROM entity. Used by the _persistence
59
+ # plugin.
60
+ def rom?
61
+ record.is_a?(::ROM::Struct)
62
+ end
63
+
64
+ # Returns internal ROM wrapper object.
65
+ def rom
66
+ fail Shrine::Error, "repository is missing" unless repository
67
+
68
+ RomWrapper.new(repository: repository, record: record)
69
+ end
70
+ end
71
+
72
+ class RomWrapper
73
+ attr_reader :repository, :record_pk
74
+
75
+ def initialize(repository:, record: nil)
76
+ @repository = repository
77
+ @record_pk = record.send(relation.primary_key)
78
+ end
79
+
80
+ def update_record(attributes)
81
+ repository.update(record_pk, attributes)
82
+ end
83
+
84
+ def retrieve_record
85
+ case adapter
86
+ when :sql
87
+ repository.transaction do
88
+ yield record_relation.lock.one!
89
+ end
90
+ else
91
+ yield record_relation.one!
92
+ end
93
+ end
94
+
95
+ def column_type(attribute)
96
+ # sends "json" or "jsonb" string for JSON or JSONB column.
97
+ # returns nil for String column
98
+ relation.schema[attribute].type.meta[:db_type]
99
+ end
100
+
101
+ private
102
+
103
+ def record_relation
104
+ case adapter
105
+ when :sql, :mongo then relation.by_pk(record_pk)
106
+ when :elasticsearch then relation.get(record_pk)
107
+ else
108
+ fail Shrine::Error, "unsupported ROM adapter: #{adapter.inspect}"
109
+ end
110
+ end
111
+
112
+ def adapter
113
+ relation.adapter
114
+ end
115
+
116
+ def relation
117
+ repository.root
118
+ end
119
+ end
120
+ end
121
+
122
+ register_plugin(:rom, Rom)
123
+ end
124
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "shrine-rom"
3
+ gem.version = "0.1.0"
4
+
5
+ gem.required_ruby_version = ">= 2.3"
6
+
7
+ gem.summary = "Provides rom-rb integration for Shrine."
8
+ gem.homepage = "https://github.com/shrinerb/shrine-rom"
9
+ gem.authors = ["Janko Marohnić", "Ahmad Musaffa"]
10
+ gem.email = ["janko.marohnic@gmail.com", "musaffa_csemm@yahoo.com"]
11
+ gem.license = "MIT"
12
+
13
+ gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "*.gemspec"]
14
+ gem.require_path = "lib"
15
+
16
+ gem.add_dependency "shrine", "~> 3.0"
17
+ gem.add_dependency "rom", "~> 5.0"
18
+
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "minitest"
21
+ gem.add_development_dependency "rom-sql"
22
+ gem.add_development_dependency "sqlite3"
23
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shrine-rom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Janko Marohnić
8
+ - Ahmad Musaffa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-06-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: shrine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rom
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rom-sql
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: sqlite3
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description:
99
+ email:
100
+ - janko.marohnic@gmail.com
101
+ - musaffa_csemm@yahoo.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - LICENSE.txt
107
+ - README.md
108
+ - lib/shrine/plugins/rom.rb
109
+ - shrine-rom.gemspec
110
+ homepage: https://github.com/shrinerb/shrine-rom
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '2.3'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.3.3
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Provides rom-rb integration for Shrine.
133
+ test_files: []