standard-file 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7507a20e41218ab29adb02614a3d56c8093b920e
4
- data.tar.gz: d03b8b5fe61838f446b550bc289a4ba34226903a
3
+ metadata.gz: 4713fd9c76de2ac82f16a1fe84b27a53a25d4c3a
4
+ data.tar.gz: c85729b0055a6b4df9038f4804a4442905ed4dc5
5
5
  SHA512:
6
- metadata.gz: 7f4b70e3fee538bf9709b2328f2832bed09d081042e779cf6762dc56453022a136797353c723315c910723f29d28ac4d417015c5845e31ed3d0b0d4fa03004ed
7
- data.tar.gz: c3a9908c6eae5dc5ea533482a6e25daa3a5d52ce3e635e2a4bcc5b6e9e19e0510028e34bc7e97fdbead59b3b044f278d307ec529632e186c8c8325d697429ba8
6
+ metadata.gz: 6d42f737a016dba90c00cc661170b5b93f21a830664e65a3a2568f778bfd120e2ee344bbb8fa63ebbb9b7eb1a880a320f0685cf042090337c2c13f85535a7da1
7
+ data.tar.gz: 94fe38ec72e42df859cbae76cb50ac1affd49b131e8fa5e5dc366a4f5134c4003a2e3611663b235d1ae58553c40ee8357d4fb724c315a31863b5c424b806c927
data/lib/standard_file.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  require "standard_file/engine"
2
- require_relative 'standard_file/sync_manager'
3
- require_relative 'standard_file/user_manager'
2
+ require_relative 'standard_file/abstract/sync_manager'
3
+ require_relative 'standard_file/abstract/user_manager'
4
+ require_relative 'standard_file/2016_12_15/sync_manager'
5
+ require_relative 'standard_file/2019_05_20/sync_manager'
6
+ require_relative 'standard_file/2016_12_15/user_manager'
7
+ require_relative 'standard_file/2019_05_20/user_manager'
4
8
  require_relative 'standard_file/jwt_helper'
5
9
 
6
10
  module StandardFile
@@ -0,0 +1,143 @@
1
+ module StandardFile
2
+ module V20161215
3
+ class SyncManager < StandardFile::AbstractSyncManager
4
+
5
+ def sync(item_hashes, options, request)
6
+ in_sync_token = options[:sync_token]
7
+ in_cursor_token = options[:cursor_token]
8
+ limit = options[:limit]
9
+ content_type = options[:content_type] # optional, only return items of these type if present
10
+
11
+ retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
12
+ last_updated = DateTime.now
13
+ saved_items, unsaved_items = _sync_save(item_hashes, request)
14
+ if saved_items.length > 0
15
+ last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
16
+ end
17
+
18
+ check_for_conflicts(saved_items, retrieved_items, unsaved_items)
19
+
20
+ # add 1 microsecond to avoid returning same object in subsequent sync
21
+ last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
22
+
23
+ sync_token = sync_token_from_datetime(last_updated)
24
+ return {
25
+ :retrieved_items => retrieved_items,
26
+ :saved_items => saved_items,
27
+ :unsaved => unsaved_items,
28
+ :sync_token => sync_token,
29
+ :cursor_token => cursor_token
30
+ }
31
+ end
32
+
33
+ def check_for_conflicts(saved_items, retrieved_items, unsaved_items)
34
+ # conflicts occur when you are trying to save an item for which there is a pending change already
35
+ min_conflict_interval = 20
36
+
37
+ if Rails.env.development?
38
+ min_conflict_interval = 1
39
+ end
40
+
41
+ saved_ids = saved_items.map{|x| x.uuid }
42
+ retrieved_ids = retrieved_items.map{|x| x.uuid }
43
+ conflicts = saved_ids & retrieved_ids # & is the intersection
44
+ # saved items take precedence, retrieved items are duplicated with a new uuid
45
+ conflicts.each do |conflicted_uuid|
46
+ # if changes are greater than min_conflict_interval seconds apart,
47
+ # push the retrieved item in the unsaved array so that the client can duplicate it
48
+ saved = saved_items.find{|i| i.uuid == conflicted_uuid}
49
+ conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
50
+ if (saved.updated_at - conflicted.updated_at).abs > min_conflict_interval
51
+ # puts "\n\n\n Creating conflicted copy of #{saved.uuid}\n\n\n"
52
+
53
+ unsaved_items.push({
54
+ :item => conflicted,
55
+ :error => {:tag => "sync_conflict"}
56
+ })
57
+
58
+ end
59
+
60
+ # We remove the item from retrieved items whether or not it satisfies the min_conflict_interval
61
+ # This is because the 'saved' value takes precedence, since that's the current value in the database.
62
+ # So by removing it from retrieved, we are forcing the client to ignore this change.
63
+ retrieved_items.delete(conflicted)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def _sync_save(item_hashes, request)
70
+ if !item_hashes
71
+ return [], []
72
+ end
73
+ saved_items = []
74
+ unsaved = []
75
+
76
+ item_hashes.each do |item_hash|
77
+ begin
78
+ item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
79
+ rescue => error
80
+ unsaved.push({
81
+ :item => item_hash,
82
+ :error => {:message => error.message, :tag => "uuid_conflict"}
83
+ })
84
+ next
85
+ end
86
+
87
+ item.last_user_agent = request.user_agent
88
+ item.update(item_hash.permit(*permitted_params))
89
+ # we want to force update the updated_at field, even if no changes were made
90
+ # item.touch
91
+
92
+
93
+ if item.deleted == true
94
+ set_deleted(item)
95
+ item.save
96
+ end
97
+
98
+ saved_items.push(item)
99
+ end
100
+
101
+ return saved_items, unsaved
102
+ end
103
+
104
+ def _sync_get(sync_token, input_cursor_token, limit, content_type)
105
+ cursor_token = nil
106
+ if limit == nil
107
+ limit = 100000
108
+ end
109
+
110
+ # if both are present, cursor_token takes precendence as that would eventually return all results
111
+ # the distinction between getting results for a cursor and a sync token is that cursor results use a
112
+ # >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
113
+ # typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
114
+ # by using >=, we don't miss those results on a subsequent call with a cursor token
115
+ if input_cursor_token
116
+ date = datetime_from_sync_token(input_cursor_token)
117
+ items = @user.items.order(:updated_at).where("updated_at >= ?", date)
118
+ elsif sync_token
119
+ date = datetime_from_sync_token(sync_token)
120
+ items = @user.items.order(:updated_at).where("updated_at > ?", date)
121
+ else
122
+ # if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
123
+ items = @user.items.order(:updated_at).where(:deleted => false)
124
+ end
125
+
126
+ if content_type
127
+ items = items.where(:content_type => content_type)
128
+ end
129
+
130
+ items = items.sort_by{|m| m.updated_at}
131
+
132
+ if items.count > limit
133
+ items = items.slice(0, limit)
134
+ date = items.last.updated_at
135
+ cursor_token = sync_token_from_datetime(date)
136
+ end
137
+
138
+ return items, cursor_token
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,7 @@
1
+ module StandardFile
2
+ module V20161215
3
+ class UserManager < StandardFile::AbstractUserManager
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,161 @@
1
+ module StandardFile
2
+ module V20190520
3
+ class SyncManager < StandardFile::AbstractSyncManager
4
+
5
+ def sync(item_hashes, options, request)
6
+ in_sync_token = options[:sync_token]
7
+ in_cursor_token = options[:cursor_token]
8
+ limit = options[:limit]
9
+ content_type = options[:content_type] # optional, only return items of these type if present
10
+
11
+ retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
12
+ last_updated = DateTime.now
13
+ saved_items, conflicts = _sync_save(item_hashes, request, retrieved_items)
14
+
15
+ if saved_items.length > 0
16
+ last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
17
+ end
18
+
19
+ # add 1 microsecond to avoid returning same object in subsequent sync
20
+ last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
21
+ sync_token = sync_token_from_datetime(last_updated)
22
+
23
+ return {
24
+ :retrieved_items => retrieved_items,
25
+ :saved_items => saved_items,
26
+ :conflicts => conflicts,
27
+ :sync_token => sync_token,
28
+ :cursor_token => cursor_token
29
+ }
30
+ end
31
+
32
+
33
+ # Ignore differences that are at most this many seconds apart
34
+ # Anything over this threshold will be conflicted.
35
+ MIN_CONFLICT_INTERVAL = 1.0
36
+
37
+ def _sync_save(item_hashes, request, retrieved_items)
38
+ if !item_hashes
39
+ return [], []
40
+ end
41
+
42
+ saved_items = []
43
+ conflicts = []
44
+
45
+ item_hashes.each do |item_hash|
46
+ is_new_record = false
47
+ begin
48
+ item = @user.items.find_or_create_by(:uuid => item_hash[:uuid]) do |created_item|
49
+ # this block is executed if this is a new record.
50
+ is_new_record = true
51
+ end
52
+ rescue => error
53
+ conflicts.push({
54
+ :unsaved_item => item_hash,
55
+ :type => "uuid_conflict"
56
+ })
57
+ next
58
+ end
59
+
60
+ # SFJS did not send updated_at prior to 0.3.59.
61
+ # updated_at value from client will not be saved, as it is not a permitted_param.
62
+ if item_hash['updated_at']
63
+ incoming_updated_at = DateTime.parse(item_hash['updated_at'])
64
+ else
65
+ # Default to epoch
66
+ incoming_updated_at = Time.at(0).to_datetime
67
+ end
68
+
69
+ if !is_new_record
70
+ # We want to check if this updated_at value is equal to the item's current updated_at value.
71
+ # If they differ, it means the client is attempting to save an item which hasn't been updated.
72
+ # In this case, if the incoming_item.updated_at < server_item.updated_at, always conflict.
73
+ # We don't want old items overriding newer ones.
74
+ # incoming_item.updated_at > server_item.updated_at would seem to be impossible, as only servers are responsible for setting updated_at.
75
+ # But assuming a rogue client has gotten away with it,
76
+ # we should also conflict in this case if the difference between the dates is greater than MIN_CONFLICT_INTERVAL seconds.
77
+
78
+ save_incoming = true
79
+
80
+ our_updated_at = item.updated_at
81
+ difference = incoming_updated_at.to_f - our_updated_at.to_f
82
+
83
+ if difference < 0
84
+ # incoming is less than ours. This implies stale data. Don't save if greater than interval
85
+ save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
86
+ elsif difference > 0
87
+ # incoming is greater than ours. Should never be the case. If so though, don't save.
88
+ save_incoming = difference.abs < MIN_CONFLICT_INTERVAL
89
+ else
90
+ # incoming is equal to ours (which is desired, healthy behavior), continue with saving.
91
+ save_incoming = true
92
+ end
93
+
94
+ if !save_incoming
95
+ # Dont save incoming and send it back. At this point the server item is likely to be included
96
+ # in retrieved_items in a subsequent sync, so when that value comes into the client,
97
+ server_value = item.as_json({})
98
+ conflicts.push({
99
+ :server_item => server_value, # as_json to get values as-is, befor modifying below,
100
+ :type => "sync_conflict"
101
+ })
102
+
103
+ retrieved_items.delete(item)
104
+ next
105
+ end
106
+ end
107
+
108
+ item.last_user_agent = request.user_agent
109
+ item.update(item_hash.permit(*permitted_params))
110
+
111
+ if item.deleted == true
112
+ set_deleted(item)
113
+ item.save
114
+ end
115
+
116
+ saved_items.push(item)
117
+ end
118
+
119
+ return saved_items, conflicts
120
+ end
121
+
122
+ def _sync_get(sync_token, input_cursor_token, limit, content_type)
123
+ cursor_token = nil
124
+ if limit == nil
125
+ limit = 100000
126
+ end
127
+
128
+ # if both are present, cursor_token takes precendence as that would eventually return all results
129
+ # the distinction between getting results for a cursor and a sync token is that cursor results use a
130
+ # >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
131
+ # typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
132
+ # by using >=, we don't miss those results on a subsequent call with a cursor token
133
+ if input_cursor_token
134
+ date = datetime_from_sync_token(input_cursor_token)
135
+ items = @user.items.order(:updated_at).where("updated_at >= ?", date)
136
+ elsif sync_token
137
+ date = datetime_from_sync_token(sync_token)
138
+ items = @user.items.order(:updated_at).where("updated_at > ?", date)
139
+ else
140
+ # if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
141
+ items = @user.items.order(:updated_at).where(:deleted => false)
142
+ end
143
+
144
+ if content_type
145
+ items = items.where(:content_type => content_type)
146
+ end
147
+
148
+ items = items.sort_by{|m| m.updated_at}
149
+
150
+ if items.count > limit
151
+ items = items.slice(0, limit)
152
+ date = items.last.updated_at
153
+ cursor_token = sync_token_from_datetime(date)
154
+ end
155
+
156
+ return items, cursor_token
157
+ end
158
+
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,7 @@
1
+ module StandardFile
2
+ module V20190520
3
+ class UserManager < StandardFile::AbstractUserManager
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,61 @@
1
+ module StandardFile
2
+ class AbstractSyncManager
3
+
4
+ attr_accessor :sync_fields
5
+
6
+ def initialize(user)
7
+ @user = user
8
+ raise "User must be set" unless @user
9
+ end
10
+
11
+ def set_sync_fields(val)
12
+ @sync_fields = val
13
+ end
14
+
15
+ def sync_fields
16
+ return @sync_fields || [:content, :enc_item_key, :content_type, :auth_hash, :deleted, :created_at]
17
+ end
18
+
19
+ def destroy_items(uuids)
20
+ items = @user.items.where(uuid: uuids)
21
+ items.destroy_all
22
+ end
23
+
24
+ private
25
+
26
+ def sync_token_from_datetime(datetime)
27
+ version = 2
28
+ Base64.encode64("#{version}:" + "#{datetime.to_f}")
29
+ end
30
+
31
+ def datetime_from_sync_token(sync_token)
32
+ decoded = Base64.decode64(sync_token)
33
+ parts = decoded.rpartition(":")
34
+ timestamp_string = parts.last
35
+ version = parts.first
36
+ if version == "1"
37
+ date = DateTime.strptime(timestamp_string,'%s')
38
+ elsif version == "2"
39
+ date = Time.at(timestamp_string.to_f).to_datetime.utc
40
+ end
41
+
42
+ return date
43
+ end
44
+
45
+ def set_deleted(item)
46
+ item.deleted = true
47
+ item.content = nil if item.has_attribute?(:content)
48
+ item.enc_item_key = nil if item.has_attribute?(:enc_item_key)
49
+ item.auth_hash = nil if item.has_attribute?(:auth_hash)
50
+ end
51
+
52
+ def item_params
53
+ params.permit(*permitted_params)
54
+ end
55
+
56
+ def permitted_params
57
+ sync_fields
58
+ end
59
+
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  module StandardFile
2
- class UserManager
2
+ class AbstractUserManager
3
3
 
4
4
  def initialize(user_class)
5
5
  @user_class = user_class
@@ -1,3 +1,3 @@
1
1
  module StandardFile
2
- VERSION = '0.3.1'
2
+ VERSION = '0.3.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-file
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Standard File
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-10 00:00:00.000000000 Z
11
+ date: 2019-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -64,10 +64,14 @@ files:
64
64
  - README.md
65
65
  - Rakefile
66
66
  - lib/standard_file.rb
67
+ - lib/standard_file/2016_12_15/sync_manager.rb
68
+ - lib/standard_file/2016_12_15/user_manager.rb
69
+ - lib/standard_file/2019_05_20/sync_manager.rb
70
+ - lib/standard_file/2019_05_20/user_manager.rb
71
+ - lib/standard_file/abstract/sync_manager.rb
72
+ - lib/standard_file/abstract/user_manager.rb
67
73
  - lib/standard_file/engine.rb
68
74
  - lib/standard_file/jwt_helper.rb
69
- - lib/standard_file/sync_manager.rb
70
- - lib/standard_file/user_manager.rb
71
75
  - lib/standard_file/version.rb
72
76
  - lib/tasks/standard_file_tasks.rake
73
77
  homepage: https://standardnotes.org
@@ -1,197 +0,0 @@
1
- module StandardFile
2
- class SyncManager
3
-
4
- attr_accessor :sync_fields
5
-
6
- def initialize(user)
7
- @user = user
8
- raise "User must be set" unless @user
9
- end
10
-
11
- def set_sync_fields(val)
12
- @sync_fields = val
13
- end
14
-
15
- def sync_fields
16
- return @sync_fields || [:content, :enc_item_key, :content_type, :auth_hash, :deleted, :created_at]
17
- end
18
-
19
- def sync(item_hashes, options, request)
20
-
21
- in_sync_token = options[:sync_token]
22
- in_cursor_token = options[:cursor_token]
23
- limit = options[:limit]
24
- content_type = options[:content_type] # optional, only return items of these type if present
25
-
26
- retrieved_items, cursor_token = _sync_get(in_sync_token, in_cursor_token, limit, content_type).to_a
27
- last_updated = DateTime.now
28
- saved_items, unsaved_items = _sync_save(item_hashes, request)
29
- if saved_items.length > 0
30
- last_updated = saved_items.sort_by{|m| m.updated_at}.last.updated_at
31
- end
32
-
33
- check_for_conflicts(saved_items, retrieved_items, unsaved_items)
34
-
35
- # add 1 microsecond to avoid returning same object in subsequent sync
36
- last_updated = (last_updated.to_time + 1/100000.0).to_datetime.utc
37
-
38
- sync_token = sync_token_from_datetime(last_updated)
39
- return {
40
- :retrieved_items => retrieved_items,
41
- :saved_items => saved_items,
42
- :unsaved => unsaved_items,
43
- :sync_token => sync_token,
44
- :cursor_token => cursor_token
45
- }
46
- end
47
-
48
- def check_for_conflicts(saved_items, retrieved_items, unsaved_items)
49
- # conflicts occur when you are trying to save an item for which there is a pending change already
50
- min_conflict_interval = 20
51
-
52
- if Rails.env.development?
53
- min_conflict_interval = 1
54
- end
55
-
56
- saved_ids = saved_items.map{|x| x.uuid }
57
- retrieved_ids = retrieved_items.map{|x| x.uuid }
58
- conflicts = saved_ids & retrieved_ids # & is the intersection
59
- # saved items take precedence, retrieved items are duplicated with a new uuid
60
- conflicts.each do |conflicted_uuid|
61
- # if changes are greater than min_conflict_interval seconds apart,
62
- # push the retrieved item in the unsaved array so that the client can duplicate it
63
- saved = saved_items.find{|i| i.uuid == conflicted_uuid}
64
- conflicted = retrieved_items.find{|i| i.uuid == conflicted_uuid}
65
- if (saved.updated_at - conflicted.updated_at).abs > min_conflict_interval
66
- # puts "\n\n\n Creating conflicted copy of #{saved.uuid}\n\n\n"
67
-
68
- unsaved_items.push({
69
- :item => conflicted,
70
- :error => {:tag => "sync_conflict"}
71
- })
72
-
73
- end
74
-
75
- # We remove the item from retrieved items whether or not it satisfies the min_conflict_interval
76
- # This is because the 'saved' value takes precedence, since that's the current value in the database.
77
- # So by removing it from retrieved, we are forcing the client to ignore this change.
78
- retrieved_items.delete(conflicted)
79
- end
80
- end
81
-
82
- def destroy_items(uuids)
83
- items = @user.items.where(uuid: uuids)
84
- items.destroy_all
85
- end
86
-
87
-
88
- private
89
-
90
- def sync_token_from_datetime(datetime)
91
- version = 2
92
- Base64.encode64("#{version}:" + "#{datetime.to_f}")
93
- end
94
-
95
- def datetime_from_sync_token(sync_token)
96
- decoded = Base64.decode64(sync_token)
97
- parts = decoded.rpartition(":")
98
- timestamp_string = parts.last
99
- version = parts.first
100
- if version == "1"
101
- date = DateTime.strptime(timestamp_string,'%s')
102
- elsif version == "2"
103
- date = Time.at(timestamp_string.to_f).to_datetime.utc
104
- end
105
-
106
- return date
107
- end
108
-
109
- def _sync_save(item_hashes, request)
110
- if !item_hashes
111
- return [], []
112
- end
113
- saved_items = []
114
- unsaved = []
115
-
116
- item_hashes.each do |item_hash|
117
- begin
118
- item = @user.items.find_or_create_by(:uuid => item_hash[:uuid])
119
- rescue => error
120
- unsaved.push({
121
- :item => item_hash,
122
- :error => {:message => error.message, :tag => "uuid_conflict"}
123
- })
124
- next
125
- end
126
-
127
- item.last_user_agent = request.user_agent
128
- item.update(item_hash.permit(*permitted_params))
129
- # we want to force update the updated_at field, even if no changes were made
130
- # item.touch
131
-
132
-
133
- if item.deleted == true
134
- set_deleted(item)
135
- item.save
136
- end
137
-
138
- saved_items.push(item)
139
- end
140
-
141
- return saved_items, unsaved
142
- end
143
-
144
- def _sync_get(sync_token, input_cursor_token, limit, content_type)
145
- cursor_token = nil
146
- if limit == nil
147
- limit = 100000
148
- end
149
-
150
- # if both are present, cursor_token takes precendence as that would eventually return all results
151
- # the distinction between getting results for a cursor and a sync token is that cursor results use a
152
- # >= comparison, while a sync token uses a > comparison. The reason for this is that cursor tokens are
153
- # typically used for initial syncs or imports, where a bunch of notes could have the exact same updated_at
154
- # by using >=, we don't miss those results on a subsequent call with a cursor token
155
- if input_cursor_token
156
- date = datetime_from_sync_token(input_cursor_token)
157
- items = @user.items.order(:updated_at).where("updated_at >= ?", date)
158
- elsif sync_token
159
- date = datetime_from_sync_token(sync_token)
160
- items = @user.items.order(:updated_at).where("updated_at > ?", date)
161
- else
162
- # if no cursor token and no sync token, this is an initial sync. No need to return deleted items.
163
- items = @user.items.order(:updated_at).where(:deleted => false)
164
- end
165
-
166
- if content_type
167
- items = items.where(:content_type => content_type)
168
- end
169
-
170
- items = items.sort_by{|m| m.updated_at}
171
-
172
- if items.count > limit
173
- items = items.slice(0, limit)
174
- date = items.last.updated_at
175
- cursor_token = sync_token_from_datetime(date)
176
- end
177
-
178
- return items, cursor_token
179
- end
180
-
181
- def set_deleted(item)
182
- item.deleted = true
183
- item.content = nil if item.has_attribute?(:content)
184
- item.enc_item_key = nil if item.has_attribute?(:enc_item_key)
185
- item.auth_hash = nil if item.has_attribute?(:auth_hash)
186
- end
187
-
188
- def item_params
189
- params.permit(*permitted_params)
190
- end
191
-
192
- def permitted_params
193
- sync_fields
194
- end
195
-
196
- end
197
- end