tessa 2.0 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,37 +0,0 @@
1
- require 'delegate'
2
-
3
- class Tessa::Asset::Failure < SimpleDelegator
4
- attr_reader :message
5
-
6
- def initialize(id:, message:)
7
- @message = message
8
- super(::Tessa::Asset.new(id: id))
9
- end
10
-
11
- def self.factory(id:, response:)
12
- new(id: id, message: message_from_status(response.status))
13
- end
14
-
15
- def self.message_from_status(status)
16
- case status.to_s
17
- when /5\d{2}/
18
- "The service is unavailable at this time."
19
- when /4\d{2}/
20
- "There was a problem retrieving the data for this asset."
21
- else
22
- "An error occurred."
23
- end
24
- end
25
-
26
- def failure?
27
- true
28
- end
29
-
30
- def meta
31
- {
32
- name: "Not Found",
33
- size: "0",
34
- mime_type: "application/octet-stream"
35
- }
36
- end
37
- end
data/lib/tessa/asset.rb DELETED
@@ -1,47 +0,0 @@
1
- module Tessa
2
- class Asset
3
- include Virtus.model
4
- extend ResponseFactory
5
-
6
- attribute :id, Integer
7
- attribute :status, String
8
- attribute :strategy, String
9
- attribute :meta, Hash[Symbol => String]
10
- attribute :public_url, String
11
- attribute :private_url, String
12
- attribute :private_download_url, String
13
- attribute :delete_url, String
14
-
15
- def complete!(connection: Tessa.config.connection)
16
- Asset.new_from_response connection.patch("/assets/#{id}/completed")
17
- end
18
-
19
- def cancel!(connection: Tessa.config.connection)
20
- Asset.new_from_response connection.patch("/assets/#{id}/cancelled")
21
- end
22
-
23
- def delete!(connection: Tessa.config.connection)
24
- Asset.new_from_response connection.delete("/assets/#{id}")
25
- end
26
-
27
- def self.find(*ids,
28
- connection: Tessa.config.connection)
29
- new_from_response connection.get("/assets/#{ids.join(",")}")
30
- end
31
-
32
- def self.create(file:, **options)
33
- default_options = {
34
- size: File.size(file),
35
- name: File.basename(file),
36
- date: File.mtime(file),
37
- }
38
- Upload.create(default_options.merge(options)).upload_file(file)
39
- end
40
-
41
- def failure?
42
- false
43
- end
44
- end
45
- end
46
-
47
- require 'tessa/asset/failure'
@@ -1,49 +0,0 @@
1
- module Tessa
2
- class AssetChange
3
- include Virtus.model
4
-
5
- attribute :id, Integer
6
- attribute :action, String
7
-
8
- def initialize(args={})
9
- case args
10
- when Array
11
- id, attributes = args
12
- super attributes.merge(id: id)
13
- else
14
- super
15
- end
16
- end
17
-
18
- def apply
19
- if add?
20
- asset.complete!
21
- elsif remove?
22
- asset.delete!
23
- end
24
- end
25
-
26
- def hash
27
- [id, action].hash
28
- end
29
-
30
- def eql?(b)
31
- self.class == b.class &&
32
- self.hash == b.hash
33
- end
34
-
35
- def add?
36
- action == 'add'
37
- end
38
-
39
- def remove?
40
- action == 'remove'
41
- end
42
-
43
- private
44
-
45
- def asset
46
- Tessa::Asset.new(id: id)
47
- end
48
- end
49
- end
@@ -1,49 +0,0 @@
1
- module Tessa
2
- class AssetChangeSet
3
- include Virtus.model
4
-
5
- attribute :changes, Array[AssetChange]
6
- attribute :scoped_ids, Array[Integer]
7
-
8
- def scoped_ids=(new_ids)
9
- super new_ids.compact
10
- end
11
-
12
- def scoped_changes
13
- changes.select { |change| scoped_ids.include?(change.id) }
14
- end
15
-
16
- def apply
17
- scoped_changes.uniq.each(&:apply)
18
- end
19
-
20
- def +(b)
21
- self.changes = (self.changes + b.changes).uniq
22
- self.scoped_ids = (self.scoped_ids + b.scoped_ids).uniq
23
- self
24
- end
25
-
26
- def add(value)
27
- id = id_from_asset(value)
28
- changes << AssetChange.new(id: id, action: "add")
29
- scoped_ids << id
30
- end
31
-
32
- def remove(value)
33
- id = id_from_asset(value)
34
- changes << AssetChange.new(id: id, action: "remove")
35
- scoped_ids << id
36
- end
37
-
38
- private
39
-
40
- def id_from_asset(value)
41
- case value
42
- when Asset
43
- value.id
44
- when Fixnum
45
- value
46
- end
47
- end
48
- end
49
- end
data/lib/tessa/config.rb DELETED
@@ -1,16 +0,0 @@
1
- module Tessa
2
- class Config
3
- include Virtus.model
4
-
5
- DEFAULT_STRATEGY = "default"
6
-
7
- attribute :username, String, default: -> (*_) { ENV['TESSA_USERNAME'] }
8
- attribute :password, String, default: -> (*_) { ENV['TESSA_PASSWORD'] }
9
- attribute :url, String, default: -> (*_) { ENV['TESSA_URL'] }
10
- attribute :strategy, String, default: -> (*_) { ENV['TESSA_STRATEGY'] || DEFAULT_STRATEGY }
11
-
12
- def connection
13
- @connection ||= Tessa::FakeConnection.new
14
- end
15
- end
16
- end
@@ -1,16 +0,0 @@
1
- module Tessa
2
- module ControllerHelpers
3
-
4
- def params_for_asset(changes)
5
- Tessa::AssetChangeSet.new(
6
- changes: changes,
7
- scoped_ids: tessa_upload_asset_ids,
8
- )
9
- end
10
-
11
- def tessa_upload_asset_ids
12
- session[:tessa_upload_asset_ids] ||= []
13
- end
14
-
15
- end
16
- end
@@ -1,29 +0,0 @@
1
- module Tessa
2
- # Since we no longer connect to the Tessa service, fake out the Tessa connection
3
- # so that it always returns 503
4
- class FakeConnection
5
-
6
- [:get, :head, :put, :post, :patch, :delete].each do |method|
7
- define_method(method) do |*args|
8
- if defined?(Bugsnag)
9
- Bugsnag.notify("Tessa::FakeConnection##{method} invoked")
10
- end
11
- Tessa::FakeConnection::Response.new()
12
- end
13
- end
14
-
15
- class Response
16
- def success?
17
- false
18
- end
19
-
20
- def status
21
- 503
22
- end
23
-
24
- def body
25
- '{ "error": "The Tessa connection is no longer implemented." }'
26
- end
27
- end
28
- end
29
- end
@@ -1,222 +0,0 @@
1
- require 'open-uri'
2
-
3
- class Tessa::MigrateAssetsJob < ActiveJob::Base
4
-
5
- queue_as :low
6
-
7
- def perform(*args)
8
- options = args&.extract_options!
9
- options = {
10
- batch_size: 10,
11
- interval: 10.minutes.to_i
12
- }.merge!(options&.symbolize_keys || {})
13
-
14
- interval = options[:interval].seconds
15
- processing_state = args.first ? Marshal.load(args.first) : load_models_from_registry
16
-
17
- if processing_state.fully_processed?
18
- Rails.logger.info("Nothing to do - all models have transitioned to ActiveStorage")
19
- return
20
- end
21
-
22
- processing_state.batch_count = 0
23
- while processing_state.batch_count < options[:batch_size]
24
- model_state = processing_state.next_model
25
-
26
- process(processing_state, model_state, options)
27
-
28
- break if processing_state.fully_processed?
29
- end
30
-
31
- if processing_state.fully_processed?
32
- Rails.logger.info("Finished processing all Tessa assets")
33
- else
34
- remaining_batches = (processing_state.count / options[:batch_size].to_f).ceil
35
-
36
- Rails.logger.info("Continuing processing in #{interval}, "\
37
- "ETA #{(remaining_batches * interval).from_now}. "\
38
- "Working on #{processing_state.next_model.next_field}")
39
-
40
- processing_state.batch_count = 0
41
- self.class.set(wait: interval)
42
- .perform_later(Marshal.dump(processing_state), options)
43
- end
44
- end
45
-
46
- private
47
-
48
- def process(processing_state, model_state, options)
49
- while processing_state.batch_count < options[:batch_size]
50
- field_state = model_state.next_field
51
-
52
- process_field(processing_state, field_state, options)
53
-
54
- return if model_state.fully_processed?
55
- end
56
- end
57
-
58
- def process_field(processing_state, field_state, options)
59
- while processing_state.batch_count < options[:batch_size]
60
- remaining = options[:batch_size] - processing_state.batch_count
61
-
62
- next_batch = field_state.query
63
- .offset(field_state.offset)
64
- .limit(remaining)
65
-
66
- next_batch.each_with_index do |record, idx|
67
- begin
68
- # Wait 1 second in between records to slow things down and let the
69
- # system process for a bit
70
- sleep 1 if idx > 0
71
-
72
- reupload(record, field_state)
73
- Rails.logger.info("#{record.class}#{record.id}##{field_state.field_name}: success")
74
- field_state.success_count += 1
75
- rescue StandardError => ex
76
- Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
77
- field_state.offset += 1
78
- ensure
79
- processing_state.batch_count += 1
80
- end
81
- end
82
-
83
- return if field_state.fully_processed?
84
- end
85
- end
86
-
87
- def reupload(record, field_state)
88
- if field_state.tessa_field.multiple?
89
- reupload_multiple(record, field_state)
90
- else
91
- reupload_single(record, field_state)
92
- end
93
- end
94
-
95
- def reupload_single(record, field_state)
96
- # models with ActiveStorage uploads have nil in the column, but if you call
97
- # the method it hits the dynamic extensions and gives you the blob key
98
- database_id = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
99
- return unless database_id
100
-
101
- asset = Tessa::Asset.find(database_id)
102
-
103
- attachable = {
104
- io: URI.open(asset.private_download_url),
105
- filename: asset.meta[:name]
106
- }
107
-
108
- record.public_send("#{field_state.tessa_field.name}=", attachable)
109
- record.save!
110
- end
111
-
112
- def reupload_multiple(record, field_state)
113
- database_ids = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
114
- return unless database_ids
115
-
116
- assets = Tessa::Asset.find(database_ids)
117
-
118
- attachables = assets.map do |asset|
119
- {
120
- io: URI.open(asset.private_download_url),
121
- filename: asset.meta[:name]
122
- }
123
- end
124
-
125
- record.public_send("#{field_state.tessa_field.name}=", attachables)
126
- record.save!
127
- end
128
-
129
- def load_models_from_registry
130
- # Initialize our Record Keeping object
131
- ProcessingState.initialize_from_models
132
- end
133
-
134
- # Determines whether the migrate asset job is completed. If true, running the
135
- # job again will not do anything.
136
- def self.complete?
137
- ProcessingState.initialize_from_models.fully_processed?
138
- end
139
-
140
- ProcessingState = Struct.new(:model_queue, :batch_count) do
141
- def self.initialize_from_models(models = nil)
142
- unless models
143
- # Load all Tessa models that can have attachments (not form objects)
144
- Rails.application.eager_load!
145
- models = Tessa.model_registry.select { |m| m.respond_to?(:has_one_attached) }
146
- end
147
-
148
- new(
149
- models.map do |model|
150
- ModelProcessingState.initialize_from_model(model)
151
- end,
152
- 0
153
- )
154
- end
155
-
156
- def next_model
157
- model_queue.detect { |i| !i.fully_processed? }
158
- end
159
-
160
- def fully_processed?
161
- model_queue.all?(&:fully_processed?)
162
- end
163
-
164
- def count
165
- model_queue.sum { |m| m.field_queue.sum { |f| f.count - f.offset } }
166
- end
167
- end
168
-
169
- ModelProcessingState = Struct.new(:class_name, :field_queue) do
170
- def self.initialize_from_model(model)
171
- new(
172
- model.name,
173
- model.tessa_fields.map do |name, _|
174
- FieldProcessingState.initialize_from_model(model, name)
175
- end
176
- )
177
- end
178
-
179
- def next_field
180
- field_queue.detect { |i| !i.fully_processed? }
181
- end
182
-
183
- def model
184
- @model ||= class_name.constantize
185
- end
186
-
187
- def fully_processed?
188
- field_queue.all?(&:fully_processed?)
189
- end
190
- end
191
-
192
- FieldProcessingState = Struct.new(:class_name, :field_name, :offset, :success_count) do
193
- def self.initialize_from_model(model, field_name)
194
- new(
195
- model.name,
196
- field_name,
197
- 0,
198
- 0
199
- )
200
- end
201
-
202
- def model
203
- @model ||= class_name.constantize
204
- end
205
-
206
- def tessa_field
207
- model.tessa_fields[field_name]
208
- end
209
-
210
- def query
211
- model.where.not(Hash[tessa_field.id_field, nil])
212
- end
213
-
214
- def count
215
- query.count
216
- end
217
-
218
- def fully_processed?
219
- offset >= count
220
- end
221
- end
222
- end
@@ -1,145 +0,0 @@
1
- require 'forwardable'
2
-
3
- class Tessa::DynamicExtensions
4
- extend Forwardable
5
-
6
- attr_reader :field
7
-
8
- def name
9
- field.name
10
- end
11
-
12
- def initialize(field)
13
- @field = field
14
- end
15
-
16
- class SingleRecord < ::Tessa::DynamicExtensions
17
- def build(mod)
18
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
19
- def #{name}
20
- # has_one_attached defines the getter using class_eval so we can't call
21
- # super() here.
22
- if #{name}_attachment.present?
23
- return Tessa::ActiveStorage::AssetWrapper.new(#{name}_attachment)
24
- end
25
- end
26
-
27
- def #{field.id_field}
28
- # Use the attachment's key
29
- return #{name}_attachment.key if #{name}_attachment.present?
30
- end
31
-
32
- def #{name}=(attachable)
33
- # Every new upload is going to ActiveStorage
34
- a = @active_storage_attached_#{name} ||=
35
- ::ActiveStorage::Attached::One.new("#{name}", self, dependent: :purge_later)
36
-
37
- case attachable
38
- when Tessa::AssetChangeSet
39
- attachable.changes.select(&:remove?).each { a.detach }
40
- attachable.changes.select(&:add?).each do |change|
41
- next if #{field.id_field} == change.id
42
-
43
- a.attach(change.id)
44
- end
45
- when nil
46
- a.detach
47
- else
48
- a.attach(attachable)
49
- end
50
-
51
- # overwrite the tessa ID in the database
52
- self.#{field.id_field} = nil
53
- end
54
-
55
- def attributes
56
- super.merge({
57
- '#{field.id_field}' => #{field.id_field},
58
- '_tessa_#{field.id_field}' => super['#{field.id_field}']
59
- })
60
- end
61
- CODE
62
- mod
63
- end
64
- end
65
-
66
- class MultipleRecord < ::Tessa::DynamicExtensions
67
- def build(mod)
68
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
69
- def #{name}
70
- field = self.class.tessa_fields["#{name}".to_sym]
71
- tessa_ids = field.id(on: self) - #{name}_attachments.map(&:key)
72
-
73
- @#{name} ||= [
74
- *#{name}_attachments.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) },
75
- ]
76
- end
77
-
78
- def #{field.id_field}
79
- [
80
- # Use the attachment's key
81
- *#{name}_attachments.map(&:key),
82
- ]
83
- end
84
-
85
- def #{name}=(attachables)
86
- # Every new upload is going to ActiveStorage
87
- a = @active_storage_attached_#{name} ||=
88
- ::ActiveStorage::Attached::Many.new("#{name}", self, dependent: :purge_later)
89
-
90
- case attachables
91
- when Tessa::AssetChangeSet
92
- attachables.changes.select(&:remove?).each do |change|
93
- if existing = #{name}_attachments.find { |a| a.key == change.id }
94
- existing.destroy
95
- else
96
- ids = self.#{field.id_field}
97
- ids&.delete(change.id.to_i)
98
- self.#{field.id_field} = ids&.any? ? ids : nil
99
- end
100
- end
101
- attachables.changes.select(&:add?).each do |change|
102
- next if #{field.id_field}.include? change.id
103
-
104
- a.attach(change.id)
105
- end
106
- when nil
107
- a.detach
108
- self.#{field.id_field} = nil
109
- else
110
- a.attach(*attachables)
111
- self.#{field.id_field} = nil
112
- end
113
- end
114
-
115
- def attributes
116
- super.merge({
117
- '#{field.id_field}' => #{field.id_field},
118
- '_tessa_#{field.id_field}' => super['#{field.id_field}']
119
- })
120
- end
121
- CODE
122
- mod
123
- end
124
- end
125
-
126
- class SingleFormObject < ::Tessa::DynamicExtensions
127
- def build(mod)
128
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
129
- attr_accessor :#{name}_id
130
- attr_accessor :#{name}
131
- CODE
132
- mod
133
- end
134
- end
135
-
136
- class MultipleFormObject < ::Tessa::DynamicExtensions
137
- def build(mod)
138
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
139
- attr_accessor :#{name}_ids
140
- attr_accessor :#{name}
141
- CODE
142
- mod
143
- end
144
- end
145
- end
@@ -1,77 +0,0 @@
1
- module Tessa
2
- module Model
3
- class Field
4
- include Virtus.model
5
-
6
- attribute :model
7
- attribute :name, String
8
- attribute :multiple, Boolean, default: false
9
- attribute :id_field, String
10
-
11
- def id_field
12
- super || "#{name}#{default_id_field_suffix}"
13
- end
14
-
15
- def ids(on:)
16
- [*id(on: on)]
17
- end
18
-
19
- def id(on:)
20
- on.public_send(id_field)
21
- end
22
-
23
- def apply(set, on:)
24
- ids = ids(on: on)
25
-
26
- set.scoped_changes.each do |change|
27
- if change.add?
28
- ids << change.id
29
- elsif change.remove?
30
- ids.delete change.id
31
- end
32
- end
33
-
34
- if multiple?
35
- on.public_send(id_field_writer, ids)
36
- else
37
- on.public_send(id_field_writer, ids.first)
38
- end
39
- end
40
-
41
- def change_set_for(value)
42
- case value
43
- when AssetChangeSet
44
- value
45
- when Array
46
- value.map { |item| change_set_for(item) }.reduce(:+)
47
- when Asset
48
- AssetChangeSet.new.tap { |set| set.add(value) }
49
- else
50
- AssetChangeSet.new
51
- end
52
- end
53
-
54
- def difference_change_set(subtrahend_ids, on:)
55
- AssetChangeSet.new.tap do |change_set|
56
- (ids(on: on) - subtrahend_ids).each do |id|
57
- change_set.remove(id)
58
- end
59
- end
60
- end
61
-
62
- private
63
-
64
- def id_field_writer
65
- "#{id_field}="
66
- end
67
-
68
- def default_id_field_suffix
69
- if multiple
70
- "_ids"
71
- else
72
- "_id"
73
- end
74
- end
75
- end
76
- end
77
- end