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
@@ -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