tessa 2.0 → 6.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -18
  3. data/app/assets/javascripts/tessa.esm.js +212 -173
  4. data/app/assets/javascripts/tessa.js +206 -167
  5. data/app/javascript/activestorage/file_checksum.ts +76 -0
  6. data/app/javascript/tessa/index.ts +264 -0
  7. data/app/javascript/tessa/types.ts +34 -0
  8. data/config/routes.rb +0 -1
  9. data/lib/tessa/simple_form/asset_input.rb +24 -25
  10. data/lib/tessa/version.rb +1 -1
  11. data/lib/tessa.rb +0 -80
  12. data/package.json +7 -2
  13. data/rollup.config.js +5 -5
  14. data/spec/dummy/app/models/single_asset_model.rb +1 -1
  15. data/spec/dummy/app/models/single_asset_model_form.rb +3 -5
  16. data/spec/dummy/bin/rails +2 -2
  17. data/spec/dummy/bin/rake +2 -2
  18. data/spec/dummy/bin/setup +14 -6
  19. data/spec/dummy/bin/yarn +9 -3
  20. data/spec/dummy/config/application.rb +11 -9
  21. data/spec/dummy/config/boot.rb +1 -1
  22. data/spec/dummy/config/environment.rb +1 -1
  23. data/spec/dummy/config/environments/development.rb +34 -5
  24. data/spec/dummy/config/environments/production.rb +49 -10
  25. data/spec/dummy/config/environments/test.rb +28 -12
  26. data/spec/dummy/config/initializers/backtrace_silencers.rb +4 -3
  27. data/spec/dummy/config/initializers/content_security_policy.rb +5 -0
  28. data/spec/dummy/config/initializers/filter_parameter_logging.rb +3 -1
  29. data/spec/dummy/config/initializers/new_framework_defaults_6_1.rb +67 -0
  30. data/spec/dummy/config/initializers/permissions_policy.rb +11 -0
  31. data/spec/dummy/config/initializers/wrap_parameters.rb +5 -0
  32. data/spec/dummy/config/locales/en.yml +1 -1
  33. data/spec/dummy/config/routes.rb +1 -1
  34. data/spec/dummy/config/storage.yml +31 -0
  35. data/spec/dummy/config.ru +2 -1
  36. data/spec/dummy/db/migrate/20230406194400_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  37. data/spec/dummy/db/migrate/20230406194401_create_active_storage_variant_records.active_storage.rb +27 -0
  38. data/spec/dummy/db/schema.rb +15 -7
  39. data/tessa.gemspec +4 -5
  40. data/tsconfig.json +10 -0
  41. data/yarn.lock +74 -7
  42. metadata +36 -74
  43. data/app/javascript/activestorage/file_checksum.js +0 -53
  44. data/app/javascript/tessa/index.js.coffee +0 -183
  45. data/lib/tasks/tessa.rake +0 -18
  46. data/lib/tessa/active_storage/asset_wrapper.rb +0 -32
  47. data/lib/tessa/asset/failure.rb +0 -37
  48. data/lib/tessa/asset.rb +0 -47
  49. data/lib/tessa/asset_change.rb +0 -49
  50. data/lib/tessa/asset_change_set.rb +0 -49
  51. data/lib/tessa/config.rb +0 -16
  52. data/lib/tessa/controller_helpers.rb +0 -16
  53. data/lib/tessa/fake_connection.rb +0 -29
  54. data/lib/tessa/jobs/migrate_assets_job.rb +0 -222
  55. data/lib/tessa/model/dynamic_extensions.rb +0 -145
  56. data/lib/tessa/model/field.rb +0 -77
  57. data/lib/tessa/model.rb +0 -94
  58. data/lib/tessa/rack_upload_proxy.rb +0 -41
  59. data/lib/tessa/response_factory.rb +0 -15
  60. data/lib/tessa/upload/uploads_file.rb +0 -18
  61. data/lib/tessa/upload.rb +0 -31
  62. data/lib/tessa/view_helpers.rb +0 -23
  63. data/spec/dummy/app/models/multiple_asset_model.rb +0 -8
  64. data/spec/support/remote_call_macro.rb +0 -40
  65. data/spec/tessa/asset/failure_spec.rb +0 -48
  66. data/spec/tessa/asset_change_set_spec.rb +0 -198
  67. data/spec/tessa/asset_change_spec.rb +0 -86
  68. data/spec/tessa/asset_spec.rb +0 -196
  69. data/spec/tessa/config_spec.rb +0 -70
  70. data/spec/tessa/controller_helpers_spec.rb +0 -55
  71. data/spec/tessa/jobs/migrate_assets_job_spec.rb +0 -247
  72. data/spec/tessa/model_field_spec.rb +0 -72
  73. data/spec/tessa/model_spec.rb +0 -325
  74. data/spec/tessa/rack_upload_proxy_spec.rb +0 -83
  75. data/spec/tessa/upload/uploads_file_spec.rb +0 -72
  76. data/spec/tessa/upload_spec.rb +0 -125
  77. data/spec/tessa_spec.rb +0 -23
data/lib/tessa/model.rb DELETED
@@ -1,94 +0,0 @@
1
- require 'tessa/model/field'
2
- require 'tessa/model/dynamic_extensions'
3
- require 'tessa/active_storage/asset_wrapper'
4
-
5
- module Tessa
6
- module Model
7
-
8
- def self.included(base)
9
- base.send :include, InstanceMethods
10
- base.extend ClassMethods
11
-
12
- Tessa.model_registry << base
13
- end
14
-
15
- module InstanceMethods
16
-
17
- def pending_tessa_change_sets
18
- @pending_tessa_change_sets ||= Hash.new { AssetChangeSet.new }
19
- end
20
-
21
- def apply_tessa_change_sets
22
- # Pretend like the application was successful but we didn't do anything
23
- # because everything is in ActiveStorage now
24
- pending_tessa_change_sets.clear
25
- end
26
-
27
- def remove_all_tessa_assets
28
- self.class.tessa_fields.each do |name, field|
29
- change_set = pending_tessa_change_sets[name]
30
- field.ids(on: self).each do |asset_id|
31
- change_set.remove(asset_id)
32
- end
33
- pending_tessa_change_sets[name] = change_set
34
- end
35
- end
36
-
37
- def fetch_tessa_remote_assets(ids)
38
- # This should just always return Tessa::AssetFailure
39
- Tessa.find_assets(ids)
40
- end
41
- end
42
-
43
- module ClassMethods
44
- def asset(name, args={})
45
- field = tessa_fields[name] = Field.new(args.merge(name: name))
46
-
47
- multiple = args[:multiple]
48
-
49
-
50
- if respond_to?(:has_one_attached)
51
- if multiple
52
- has_many_attached(name)
53
- else
54
- has_one_attached(name)
55
- end
56
-
57
- # We have to replace the after_destroy_commit callback added above
58
- callbacks = get_callbacks(:commit)
59
- callbacks.delete(callbacks.to_a.last)
60
- after_destroy_commit { public_send("#{name}_attachment")&.purge_later }
61
- end
62
-
63
- dynamic_extensions =
64
- if respond_to?(:has_one_attached)
65
- if multiple
66
- ::Tessa::DynamicExtensions::MultipleRecord.new(field)
67
- else
68
- ::Tessa::DynamicExtensions::SingleRecord.new(field)
69
- end
70
- else
71
- if multiple
72
- ::Tessa::DynamicExtensions::MultipleFormObject.new(field)
73
- else
74
- ::Tessa::DynamicExtensions::SingleFormObject.new(field)
75
- end
76
- end
77
- include dynamic_extensions.build(Module.new)
78
-
79
- # Undefine the activestorage default attribute method so it falls back
80
- # to our dynamic module
81
- remove_method "#{name}" rescue nil
82
- remove_method "#{name}=" rescue nil
83
- end
84
-
85
- def tessa_fields
86
- @tessa_fields ||= {}
87
- end
88
-
89
- def inherited(subclass)
90
- subclass.instance_variable_set(:@tessa_fields, @tessa_fields.dup)
91
- end
92
- end
93
- end
94
- end
@@ -1,41 +0,0 @@
1
- module Tessa
2
- class RackUploadProxy
3
-
4
- def call(env)
5
- request = Rack::Request.new(env)
6
- ::ActiveStorage::Current.host ||= request.base_url
7
-
8
- # Call in to ActiveStorage to create a DirectUpload blob
9
- params = env['rack.request.form_hash']
10
-
11
- blob = ::ActiveStorage::Blob.create_before_direct_upload!({
12
- filename: params["name"],
13
- byte_size: params["size"],
14
- content_type: params["mime_type"],
15
- checksum: params["checksum"]
16
- })
17
-
18
- env['rack.session'][:tessa_upload_asset_ids] ||= []
19
- env['rack.session'][:tessa_upload_asset_ids] << blob.signed_id
20
-
21
- response = {
22
- asset_id: blob.signed_id,
23
- upload_url: blob.service_url_for_direct_upload,
24
- upload_method: 'PUT', # ActiveStorage is always PUT
25
- upload_headers: blob.service_headers_for_direct_upload
26
- }
27
-
28
- [200, {"Content-Type" => "application/json"}, [response.to_json]]
29
- rescue Tessa::RequestFailed
30
- [500, {"Content-Type" => "application/json"}, [{ "error" => "Failed to retreive upload URL" }.to_json]]
31
- rescue ActiveRecord::NotNullViolation => e
32
- [400, {"Content-Type" => "application/json"}, [{ "error" => e.message }.to_json]]
33
- end
34
-
35
- def self.call(*args)
36
- new.call(*args)
37
- end
38
-
39
- end
40
- end
41
-
@@ -1,15 +0,0 @@
1
- module Tessa
2
- module ResponseFactory
3
-
4
- def new_from_response(response)
5
- raise RequestFailed.new("Tessa responded with #{response.status}", response) unless response.success?
6
- case json = JSON.parse(response.body)
7
- when Array
8
- json.map { |record| new record }
9
- when Hash
10
- new json
11
- end
12
- end
13
-
14
- end
15
- end
@@ -1,18 +0,0 @@
1
- class Tessa::Upload::UploadsFile
2
- attr_reader :upload, :connection
3
-
4
- def initialize(upload:, connection: self.class.connection_factory)
5
- @upload = upload
6
- @connection = connection
7
- end
8
-
9
- def call(file)
10
- connection
11
- .public_send(upload.upload_method, upload.upload_url, File.read(file))
12
- .success?
13
- end
14
-
15
- def self.connection_factory
16
- Tessa::FakeConnection.new
17
- end
18
- end
data/lib/tessa/upload.rb DELETED
@@ -1,31 +0,0 @@
1
- module Tessa
2
- class Upload
3
- include Virtus.model
4
- extend ResponseFactory
5
-
6
- attribute :asset_id, Integer
7
- attribute :upload_url, String
8
- attribute :upload_method, String
9
-
10
- def upload_file(file)
11
- if UploadsFile.new(upload: self).(file)
12
- asset.complete!
13
- else
14
- asset.cancel!
15
- false
16
- end
17
- end
18
-
19
- def self.create(connection: Tessa.config.connection,
20
- strategy: Tessa.config.strategy,
21
- **options)
22
- new_from_response connection.post('/uploads', options.merge(strategy: strategy))
23
- end
24
-
25
- private def asset
26
- Asset.new(id: asset_id)
27
- end
28
- end
29
- end
30
-
31
- require 'tessa/upload/uploads_file'
@@ -1,23 +0,0 @@
1
- module Tessa
2
- module ViewHelpers
3
- def tessa_image_tag(asset, private: false)
4
- handle_asset_failure(asset) do
5
- image_tag(
6
- private ? asset.private_url : asset.public_url
7
- )
8
- end
9
- end
10
-
11
- private
12
-
13
- def handle_asset_failure(asset)
14
- if asset.failure?
15
- content_tag(:div, asset.message, class: "tessa-asset-failure")
16
- else
17
- yield
18
- end
19
- end
20
- end
21
- end
22
-
23
- ActionView::Base.send :include, Tessa::ViewHelpers if defined?(ActionView)
@@ -1,8 +0,0 @@
1
- class MultipleAssetModel < ActiveRecord::Base
2
- # Necessary to have an array of integers in sqlite
3
- serialize :another_place, Array
4
-
5
- include Tessa::Model
6
-
7
- asset :multiple_field, multiple: true, id_field: "another_place"
8
- end
@@ -1,40 +0,0 @@
1
- RSpec.shared_examples_for "remote call macro" do |method, path, return_type|
2
- let(:remote_response) { {} }
3
- let(:faraday_stubs) {
4
- Faraday::Adapter::Test::Stubs.new do |stub|
5
- stub.send(method, path) { |env| [200, {}, remote_response.to_json] }
6
- end
7
- }
8
- let(:connection) { Faraday.new { |f| f.adapter :test, faraday_stubs } }
9
- let(:call_args) { { connection: connection } }
10
-
11
- it "calls #{method} method with #{path}" do
12
- expect(connection).to receive(method).with(path, any_args).and_call_original
13
- call
14
- end
15
-
16
- context "with no connection passed" do
17
- let(:call_args) { {} }
18
-
19
- it "defaults connection to Tessa.config.connection" do
20
- expect(Tessa.config).to receive(:connection).and_return(connection)
21
- expect(connection).to receive(method).and_call_original
22
- call
23
- end
24
- end
25
-
26
- context "when response is not successful" do
27
- let(:connection) { Tessa::FakeConnection.new }
28
-
29
- it "raises Tessa::RequestFailed" do
30
- expect{ call }.to raise_error { |error|
31
- expect(error).to be_a(Tessa::RequestFailed)
32
- }
33
- end
34
- end
35
-
36
- it "returns an instance of #{return_type}" do
37
- expect(call).to be_a(return_type)
38
- end
39
- end
40
-
@@ -1,48 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe Tessa::Asset::Failure do
4
- subject(:failure) { described_class.new(args) }
5
- let(:id) { rand(100) }
6
- let(:args) {
7
- { id: id, message: "Test" }
8
- }
9
-
10
- describe "#initialize" do
11
- it "sets id to attribute" do
12
- expect(failure.id).to eq(args[:id])
13
- end
14
-
15
- it "sets message to attribute" do
16
- expect(failure.message).to eq(args[:message])
17
- end
18
- end
19
-
20
- describe ".factory" do
21
- let(:message) { 'test message' }
22
- let(:response) { double(status: 500) }
23
- subject(:failure) { described_class.factory(id: id, response: response) }
24
-
25
-
26
- it "returns instance of Failure" do
27
- expect(described_class).to receive(:message_from_status)
28
- .with(response.status).and_return(message)
29
- expect(failure.id).to eq(id)
30
- expect(failure.message).to eq(message)
31
- end
32
- end
33
-
34
- it "responds like a blank asset" do
35
- asset = Tessa::Asset.new
36
- expect(failure.status).to eq(asset.status)
37
- expect(failure.strategy).to eq(asset.strategy)
38
- expect(failure.public_url).to eq(asset.public_url)
39
- expect(failure.private_url).to eq(asset.private_url)
40
- expect(failure.delete_url).to eq(asset.delete_url)
41
- end
42
-
43
- describe "#failure?" do
44
- it "returns true" do
45
- expect(failure).to be_failure
46
- end
47
- end
48
- end
@@ -1,198 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe Tessa::AssetChangeSet do
4
- subject(:set) { described_class.new(args) }
5
- let(:args) {
6
- {
7
- changes: Array.new(2) { Tessa::AssetChange.new },
8
- scoped_ids: [1, 2],
9
- }
10
- }
11
-
12
- describe "#initialize" do
13
- it "sets :changes to attribute" do
14
- expect(set.changes).to eq(args[:changes])
15
- end
16
-
17
- it "sets :scoped_ids to attribute" do
18
- expect(set.scoped_ids).to eq([1, 2])
19
- end
20
-
21
- context "with various scoped_id input types" do
22
- before { args[:scoped_ids] = ["1", 2, nil] }
23
-
24
- it "ensures all values are integers and ignores nils" do
25
- expect(set.scoped_ids).to eq([1, 2])
26
- end
27
- end
28
-
29
- context "with hash format for changes" do
30
- before do
31
- args[:changes] = { "123" => { "action" => "add" }, "456" => { "action" => "remove" } }
32
- end
33
-
34
- it "initializes AssetChange objects for each hash" do
35
- expect(set.changes[0].id).to eq(123)
36
- expect(set.changes[0].action).to eq("add")
37
- expect(set.changes[1].id).to eq(456)
38
- expect(set.changes[1].action).to eq("remove")
39
- end
40
- end
41
- end
42
-
43
- describe "#scoped_changes" do
44
- context "with changes not contained in scoped ids" do
45
- let(:args) {
46
- {
47
- changes: [
48
- Tessa::AssetChange.new(id: 1),
49
- Tessa::AssetChange.new(id: 2),
50
- Tessa::AssetChange.new(id: 3),
51
- Tessa::AssetChange.new(id: 4)],
52
- scoped_ids: [1, 2],
53
- }
54
- }
55
-
56
- it "only returns the scoped changes" do
57
- expect(set.scoped_changes.size).to eq(2)
58
- expect(set.scoped_changes.map(&:id)).to eq([1, 2])
59
- end
60
- end
61
- end
62
-
63
- describe "#apply" do
64
-
65
- context "with unscoped changes" do
66
- let(:args) {
67
- {
68
- changes: [
69
- Tessa::AssetChange.new(id: 1),
70
- Tessa::AssetChange.new(id: 2)],
71
- scoped_ids: [1],
72
- }
73
- }
74
-
75
- it "calls apply on each scoped change" do
76
- expect(set.changes[0]).to receive(:apply)
77
- expect(set.changes[1]).not_to receive(:apply)
78
- set.apply
79
- end
80
- end
81
-
82
- context "with duplicate changes" do
83
- let(:args) {
84
- {
85
- changes: [
86
- Tessa::AssetChange.new(id: 1, action: 'remove'),
87
- Tessa::AssetChange.new(id: 1, action: 'remove')],
88
- scoped_ids: [1],
89
- }
90
- }
91
-
92
- it "only calls apply on unique elements" do
93
- expect(el = set.changes.uniq.first).to receive(:apply)
94
- (set.changes - [el]).each { |o| expect(el).to_not receive(:apply) }
95
- set.apply
96
- end
97
- end
98
- end
99
-
100
- describe "#+" do
101
- let(:a) {
102
- described_class.new(
103
- changes: [Tessa::AssetChange.new(id: 1, action: 'add')],
104
- scoped_ids: [1],
105
- )
106
- }
107
- let(:b) {
108
- described_class.new(
109
- changes: [Tessa::AssetChange.new(id: 2, action: 'add')],
110
- scoped_ids: [2],
111
- )
112
- }
113
- subject(:sum) { a + b }
114
-
115
- it "concatenates the values of changes" do
116
- expect(sum.changes.collect(&:id)).to eq([1, 2])
117
- end
118
-
119
- it "concatenates the values of scoped_ids" do
120
- expect(sum.scoped_ids).to eq([1, 2])
121
- end
122
-
123
- context "with duplicate entries" do
124
- subject(:sum) { a + b + b }
125
-
126
- it "only includes unique values" do
127
- expect(sum.changes.collect(&:id)).to eq([1, 2])
128
- expect(sum.scoped_ids).to eq([1, 2])
129
- end
130
- end
131
- end
132
-
133
- describe "#add" do
134
- let(:args) { {} }
135
- let(:asset) { Tessa::Asset.new(id: 1) }
136
-
137
- shared_examples_for "adds the represented asset" do
138
- it "adds an 'add' change for this asset" do
139
- expect(set.changes[0].id).to eq(1)
140
- expect(set.changes[0].action).to eq("add")
141
- end
142
-
143
- it "adds the asset id to the scoped_ids array" do
144
- expect(set.scoped_ids).to eq([1])
145
- end
146
- end
147
-
148
- context "when passed an asset" do
149
- before do
150
- set.add(asset)
151
- end
152
-
153
- it_behaves_like "adds the represented asset"
154
- end
155
-
156
- context "when passed an integer id" do
157
- before do
158
- set.add(1)
159
- end
160
-
161
- it_behaves_like "adds the represented asset"
162
- end
163
- end
164
-
165
- describe "#remove" do
166
- let(:args) { {} }
167
- let(:asset) { Tessa::Asset.new(id: 1) }
168
-
169
- shared_examples_for "removes the represented asset" do
170
- it "adds a 'remove' change for this asset" do
171
- expect(set.changes[0].id).to eq(1)
172
- expect(set.changes[0].action).to eq("remove")
173
- end
174
-
175
- it "adds the asset id to the scoped_ids array" do
176
- expect(set.scoped_ids).to eq([1])
177
- end
178
- end
179
-
180
- context "when passed an asset" do
181
- before do
182
- set.remove(asset)
183
- end
184
-
185
- it_behaves_like "removes the represented asset"
186
- end
187
-
188
- context "when passed an integer id" do
189
- before do
190
- set.remove(1)
191
- end
192
-
193
- it_behaves_like "removes the represented asset"
194
- end
195
-
196
- end
197
-
198
- end
@@ -1,86 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe Tessa::AssetChange do
4
- subject(:change) { described_class.new(args) }
5
- let(:args) {
6
- {
7
- id: 123,
8
- action: "add",
9
- }
10
- }
11
-
12
- describe "#initialize" do
13
- context "with hash args" do
14
- it "sets id" do
15
- expect(subject.id).to eq(123)
16
- end
17
-
18
- it "sets action" do
19
- expect(subject.action).to eq("add")
20
- end
21
- end
22
-
23
- context "with array arg" do
24
- let(:args) { [123, { "action" => "add" }] }
25
-
26
- it "sets id" do
27
- expect(subject.id).to eq(123)
28
- end
29
-
30
- it "sets action" do
31
- expect(subject.action).to eq("add")
32
- end
33
- end
34
- end
35
-
36
- describe "#apply" do
37
- let(:asset) { instance_spy(Tessa::Asset) }
38
- before do
39
- expect(Tessa::Asset).to receive(:new).with(id: 123).and_return(asset)
40
- end
41
-
42
- context "with action 'add'" do
43
- before { args[:action] = "add" }
44
- it "calls complete! on asset with :id" do
45
- change.apply
46
- expect(asset).to have_received(:complete!)
47
- end
48
- end
49
-
50
- context "with action 'remove'" do
51
- before { args[:action] = "remove" }
52
- it "calls delete! on asset with :id" do
53
- change.apply
54
- expect(asset).to have_received(:delete!)
55
- end
56
- end
57
- end
58
-
59
- describe "#add?" do
60
- subject(:add?) { change.add? }
61
-
62
- context "action = 'add'" do
63
- before { change.action = 'add' }
64
- it { is_expected.to eq(true) }
65
- end
66
-
67
- context "action != 'add'" do
68
- before { change.action = 'something' }
69
- it { is_expected.to eq(false) }
70
- end
71
- end
72
-
73
- describe "#remove?" do
74
- subject(:remove?) { change.remove? }
75
-
76
- context "action = 'remove'" do
77
- before { change.action = 'remove' }
78
- it { is_expected.to eq(true) }
79
- end
80
-
81
- context "action != 'remove'" do
82
- before { change.action = 'something' }
83
- it { is_expected.to eq(false) }
84
- end
85
- end
86
- end