tessa 1.2.3 → 6.0.0.rc1

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/Gemfile +1 -0
  3. data/README.md +76 -3
  4. data/app/assets/javascripts/tessa.esm.js +212 -173
  5. data/app/assets/javascripts/tessa.js +206 -167
  6. data/app/javascript/activestorage/file_checksum.ts +76 -0
  7. data/app/javascript/tessa/index.ts +264 -0
  8. data/app/javascript/tessa/types.ts +34 -0
  9. data/config/routes.rb +0 -1
  10. data/docs/tessa-activestorage-sequence-diagram.drawio.png +0 -0
  11. data/lib/tessa/simple_form/asset_input.rb +24 -25
  12. data/lib/tessa/version.rb +1 -1
  13. data/lib/tessa.rb +0 -80
  14. data/package.json +7 -2
  15. data/rollup.config.js +5 -5
  16. data/spec/dummy/app/models/single_asset_model.rb +1 -1
  17. data/spec/dummy/app/models/single_asset_model_form.rb +3 -5
  18. data/spec/dummy/bin/rails +2 -2
  19. data/spec/dummy/bin/rake +2 -2
  20. data/spec/dummy/bin/setup +14 -6
  21. data/spec/dummy/bin/yarn +9 -3
  22. data/spec/dummy/config/application.rb +11 -9
  23. data/spec/dummy/config/boot.rb +1 -1
  24. data/spec/dummy/config/environment.rb +1 -1
  25. data/spec/dummy/config/environments/development.rb +34 -5
  26. data/spec/dummy/config/environments/production.rb +49 -10
  27. data/spec/dummy/config/environments/test.rb +28 -12
  28. data/spec/dummy/config/initializers/backtrace_silencers.rb +4 -3
  29. data/spec/dummy/config/initializers/content_security_policy.rb +5 -0
  30. data/spec/dummy/config/initializers/filter_parameter_logging.rb +3 -1
  31. data/spec/dummy/config/initializers/new_framework_defaults_6_1.rb +67 -0
  32. data/spec/dummy/config/initializers/permissions_policy.rb +11 -0
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +5 -0
  34. data/spec/dummy/config/locales/en.yml +1 -1
  35. data/spec/dummy/config/routes.rb +1 -1
  36. data/spec/dummy/config/storage.yml +31 -0
  37. data/spec/dummy/config.ru +2 -1
  38. data/spec/dummy/db/migrate/20230406194400_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  39. data/spec/dummy/db/migrate/20230406194401_create_active_storage_variant_records.active_storage.rb +27 -0
  40. data/spec/dummy/db/schema.rb +15 -7
  41. data/tessa.gemspec +4 -6
  42. data/tsconfig.json +10 -0
  43. data/yarn.lock +74 -7
  44. metadata +37 -85
  45. data/app/javascript/activestorage/file_checksum.js +0 -53
  46. data/app/javascript/tessa/index.js.coffee +0 -183
  47. data/lib/tasks/tessa.rake +0 -7
  48. data/lib/tessa/active_storage/asset_wrapper.rb +0 -32
  49. data/lib/tessa/asset/failure.rb +0 -37
  50. data/lib/tessa/asset.rb +0 -47
  51. data/lib/tessa/asset_change.rb +0 -49
  52. data/lib/tessa/asset_change_set.rb +0 -49
  53. data/lib/tessa/config.rb +0 -24
  54. data/lib/tessa/controller_helpers.rb +0 -16
  55. data/lib/tessa/jobs/migrate_assets_job.rb +0 -225
  56. data/lib/tessa/model/dynamic_extensions.rb +0 -162
  57. data/lib/tessa/model/field.rb +0 -77
  58. data/lib/tessa/model.rb +0 -104
  59. data/lib/tessa/rack_upload_proxy.rb +0 -41
  60. data/lib/tessa/response_factory.rb +0 -15
  61. data/lib/tessa/upload/uploads_file.rb +0 -20
  62. data/lib/tessa/upload.rb +0 -31
  63. data/lib/tessa/view_helpers.rb +0 -23
  64. data/spec/dummy/app/models/multiple_asset_model.rb +0 -8
  65. data/spec/support/remote_call_macro.rb +0 -45
  66. data/spec/tessa/asset/failure_spec.rb +0 -48
  67. data/spec/tessa/asset_change_set_spec.rb +0 -198
  68. data/spec/tessa/asset_change_spec.rb +0 -86
  69. data/spec/tessa/asset_spec.rb +0 -196
  70. data/spec/tessa/config_spec.rb +0 -112
  71. data/spec/tessa/controller_helpers_spec.rb +0 -55
  72. data/spec/tessa/jobs/migrate_assets_job_spec.rb +0 -247
  73. data/spec/tessa/model_field_spec.rb +0 -72
  74. data/spec/tessa/model_spec.rb +0 -578
  75. data/spec/tessa/rack_upload_proxy_spec.rb +0 -83
  76. data/spec/tessa/upload/uploads_file_spec.rb +0 -72
  77. data/spec/tessa/upload_spec.rb +0 -125
@@ -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,24 +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 ||= Faraday.new(url: url) do |conn|
14
- if conn.respond_to?(:basic_auth)
15
- conn.basic_auth username, password
16
- else # Faraday >= 1.0
17
- conn.request :authorization, :basic, username, password
18
- end
19
- conn.request :url_encoded
20
- conn.adapter Faraday.default_adapter
21
- end
22
- end
23
- end
24
- 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,225 +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 OpenURI::HTTPError => ex
76
- if ex.message == "404 Not Found"
77
- # clear out the field if the asset is missing
78
- record.public_send("#{field_state.tessa_field.name}=", nil)
79
- record.save!
80
- else
81
- Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
82
- end
83
- field_state.offset += 1
84
- rescue StandardError => ex
85
- Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
86
- field_state.offset += 1
87
- ensure
88
- processing_state.batch_count += 1
89
- end
90
- end
91
-
92
- return if field_state.fully_processed?
93
- end
94
- end
95
-
96
- def reupload(record, field_state)
97
- if field_state.tessa_field.multiple?
98
- reupload_multiple(record, field_state)
99
- else
100
- reupload_single(record, field_state)
101
- end
102
- end
103
-
104
- def reupload_single(record, field_state)
105
- # models with ActiveStorage uploads have nil in the column, but if you call
106
- # the method it hits the dynamic extensions and gives you the blob key
107
- database_id = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
108
- return unless database_id
109
-
110
- asset = Tessa::Asset.find(database_id)
111
-
112
- attachable = {
113
- io: URI.open(asset.private_download_url),
114
- filename: asset.meta[:name]
115
- }
116
-
117
- record.public_send("#{field_state.tessa_field.name}=", attachable)
118
- record.save!
119
- end
120
-
121
- def reupload_multiple(record, field_state)
122
- database_ids = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
123
- return unless database_ids
124
-
125
- assets = Tessa::Asset.find(database_ids)
126
-
127
- attachables = assets.map do |asset|
128
- {
129
- io: URI.open(asset.private_download_url),
130
- filename: asset.meta[:name]
131
- }
132
- end
133
-
134
- record.public_send("#{field_state.tessa_field.name}=", attachables)
135
- record.save!
136
- end
137
-
138
- def load_models_from_registry
139
- Rails.application.eager_load!
140
-
141
- # Load all Tessa models that can have attachments (not form objects)
142
- models = Tessa.model_registry
143
- .select { |m| m.respond_to?(:has_one_attached) }
144
-
145
- # Initialize our Record Keeping object
146
- ProcessingState.initialize_from_models(models)
147
- end
148
-
149
- ProcessingState = Struct.new(:model_queue, :batch_count) do
150
- def self.initialize_from_models(models)
151
- new(
152
- models.map do |model|
153
- ModelProcessingState.initialize_from_model(model)
154
- end,
155
- 0
156
- )
157
- end
158
-
159
- def next_model
160
- model_queue.detect { |i| !i.fully_processed? }
161
- end
162
-
163
- def fully_processed?
164
- model_queue.all?(&:fully_processed?)
165
- end
166
-
167
- def count
168
- model_queue.sum { |m| m.field_queue.sum { |f| f.count - f.offset } }
169
- end
170
- end
171
-
172
- ModelProcessingState = Struct.new(:class_name, :field_queue) do
173
- def self.initialize_from_model(model)
174
- new(
175
- model.name,
176
- model.tessa_fields.map do |name, _|
177
- FieldProcessingState.initialize_from_model(model, name)
178
- end
179
- )
180
- end
181
-
182
- def next_field
183
- field_queue.detect { |i| !i.fully_processed? }
184
- end
185
-
186
- def model
187
- @model ||= class_name.constantize
188
- end
189
-
190
- def fully_processed?
191
- field_queue.all?(&:fully_processed?)
192
- end
193
- end
194
-
195
- FieldProcessingState = Struct.new(:class_name, :field_name, :offset, :success_count) do
196
- def self.initialize_from_model(model, field_name)
197
- new(
198
- model.name,
199
- field_name,
200
- 0,
201
- 0
202
- )
203
- end
204
-
205
- def model
206
- @model ||= class_name.constantize
207
- end
208
-
209
- def tessa_field
210
- model.tessa_fields[field_name]
211
- end
212
-
213
- def query
214
- model.where.not(Hash[tessa_field.id_field, nil])
215
- end
216
-
217
- def count
218
- query.count
219
- end
220
-
221
- def fully_processed?
222
- offset >= count
223
- end
224
- end
225
- end
@@ -1,162 +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
-
26
- # fall back to old Tessa fetch if not present
27
- if field = self.class.tessa_fields["#{name}".to_sym]
28
- @#{name} ||= fetch_tessa_remote_assets(field.id(on: self))
29
- end
30
- end
31
-
32
- def #{field.id_field}
33
- # Use the attachment's key
34
- return #{name}_attachment.key if #{name}_attachment.present?
35
-
36
- # fallback to Tessa's database column
37
- super
38
- end
39
-
40
- def #{name}=(attachable)
41
- # Every new upload is going to ActiveStorage
42
- a = @active_storage_attached_#{name} ||=
43
- ::ActiveStorage::Attached::One.new("#{name}", self, dependent: :purge_later)
44
-
45
- case attachable
46
- when Tessa::AssetChangeSet
47
- attachable.changes.select(&:remove?).each { a.detach }
48
- attachable.changes.select(&:add?).each do |change|
49
- next if #{field.id_field} == change.id
50
-
51
- a.attach(change.id)
52
- end
53
- when nil
54
- a.detach
55
- else
56
- a.attach(attachable)
57
- end
58
-
59
- # overwrite the tessa ID in the database
60
- self.#{field.id_field} = nil
61
- end
62
-
63
- def attributes
64
- super.merge({
65
- '#{field.id_field}' => #{field.id_field},
66
- '_tessa_#{field.id_field}' => super['#{field.id_field}']
67
- })
68
- end
69
- CODE
70
- mod
71
- end
72
- end
73
-
74
- class MultipleRecord < ::Tessa::DynamicExtensions
75
- def build(mod)
76
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
77
- def #{name}
78
- field = self.class.tessa_fields["#{name}".to_sym]
79
- tessa_ids = field.id(on: self) - #{name}_attachments.map(&:key)
80
-
81
- @#{name} ||= [
82
- *#{name}_attachments.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) },
83
- *fetch_tessa_remote_assets(tessa_ids)
84
- ]
85
- end
86
-
87
- def #{field.id_field}
88
- [
89
- # Use the attachment's key
90
- *#{name}_attachments.map(&:key),
91
- # include from Tessa's database column
92
- *super
93
- ]
94
- end
95
-
96
- def #{name}=(attachables)
97
- # Every new upload is going to ActiveStorage
98
- a = @active_storage_attached_#{name} ||=
99
- ::ActiveStorage::Attached::Many.new("#{name}", self, dependent: :purge_later)
100
-
101
- case attachables
102
- when Tessa::AssetChangeSet
103
- attachables.changes.select(&:remove?).each do |change|
104
- if existing = #{name}_attachments.find { |a| a.key == change.id }
105
- existing.destroy
106
- else
107
- ids = self.#{field.id_field}
108
- ids.delete(change.id.to_i)
109
- self.#{field.id_field} = ids.any? ? ids : nil
110
- end
111
- end
112
- attachables.changes.select(&:add?).each do |change|
113
- next if #{field.id_field}.include? change.id
114
-
115
- a.attach(change.id)
116
- end
117
- when nil
118
- a.detach
119
- self.#{field.id_field} = nil
120
- else
121
- a.attach(*attachables)
122
- self.#{field.id_field} = nil
123
- end
124
- end
125
-
126
- def attributes
127
- super.merge({
128
- '#{field.id_field}' => #{field.id_field},
129
- '_tessa_#{field.id_field}' => super['#{field.id_field}']
130
- })
131
- end
132
- CODE
133
- mod
134
- end
135
- end
136
-
137
- class SingleFormObject < ::Tessa::DynamicExtensions
138
- def build(mod)
139
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
140
- attr_accessor :#{name}_id
141
- attr_writer :#{name}
142
- def #{name}
143
- @#{name} ||= fetch_tessa_remote_assets(#{name}_id)
144
- end
145
- CODE
146
- mod
147
- end
148
- end
149
-
150
- class MultipleFormObject < ::Tessa::DynamicExtensions
151
- def build(mod)
152
- mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
153
- attr_accessor :#{name}_ids
154
- attr_writer :#{name}
155
- def #{name}
156
- @#{name} ||= fetch_tessa_remote_assets(#{name}_ids)
157
- end
158
- CODE
159
- mod
160
- end
161
- end
162
- 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