nexo 0.1.5 → 0.1.6

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -0
  3. data/app/controllers/nexo/element_versions_controller.rb +28 -0
  4. data/app/controllers/nexo/elements_controller.rb +64 -0
  5. data/app/controllers/nexo/folders_controller.rb +41 -0
  6. data/app/controllers/nexo/nexo_controller.rb +5 -0
  7. data/app/jobs/nexo/api_clients.rb +20 -18
  8. data/app/jobs/nexo/base_job.rb +5 -3
  9. data/app/jobs/nexo/delete_remote_resource_job.rb +11 -1
  10. data/app/jobs/nexo/fetch_remote_resource_job.rb +61 -0
  11. data/app/jobs/nexo/folder_check_status_job.rb +10 -0
  12. data/app/jobs/nexo/folder_destroy_job.rb +2 -1
  13. data/app/jobs/nexo/folder_download_job.rb +15 -0
  14. data/app/jobs/nexo/folder_sync_job.rb +6 -0
  15. data/app/jobs/nexo/synchronizable_changed_job.rb +17 -2
  16. data/app/jobs/nexo/update_remote_resource_job.rb +58 -24
  17. data/app/lib/nexo/api_client/google_auth_service.rb +9 -8
  18. data/app/lib/nexo/api_client/google_calendar_service.rb +160 -37
  19. data/app/lib/nexo/api_client/google_calendar_sync_service.rb +84 -0
  20. data/app/lib/nexo/element_service.rb +236 -0
  21. data/app/lib/nexo/errors.rb +6 -4
  22. data/app/lib/nexo/event_receiver.rb +4 -0
  23. data/app/lib/nexo/folder_service.rb +55 -28
  24. data/app/lib/nexo/import_remote_element_version.rb +75 -0
  25. data/app/lib/nexo/policy_service.rb +27 -8
  26. data/app/models/concerns/nexo/calendar_event.rb +33 -2
  27. data/app/models/concerns/nexo/synchronizable.rb +25 -9
  28. data/app/models/nexo/element.rb +18 -35
  29. data/app/models/nexo/element_version.rb +12 -2
  30. data/app/models/nexo/folder.rb +29 -17
  31. data/app/models/nexo/integration.rb +0 -3
  32. data/app/models/nexo/token.rb +2 -0
  33. data/app/views/layouts/nexo.html.erb +42 -0
  34. data/app/views/nexo/element_versions/show.html.erb +16 -0
  35. data/app/views/nexo/elements/index.html.erb +32 -0
  36. data/app/views/nexo/elements/show.html.erb +56 -0
  37. data/app/views/nexo/folders/index.html.erb +22 -0
  38. data/app/views/nexo/folders/show.html.erb +22 -0
  39. data/config/environment.rb +1 -0
  40. data/config/routes.rb +25 -0
  41. data/config/spring.rb +1 -0
  42. data/db/migrate/20250604124821_element_sync_status.rb +11 -0
  43. data/db/migrate/20250612002919_google_sync_tokens.rb +5 -0
  44. data/db/migrate/20250623132502_folder_sync_direction.rb +8 -0
  45. data/db/migrate/20250718012839_synchronizable_nullable.rb +6 -0
  46. data/db/seeds.rb +3 -2
  47. data/lib/nexo/engine.rb +22 -5
  48. data/lib/nexo/version.rb +1 -1
  49. metadata +38 -4
  50. data/app/jobs/nexo/sync_element_job.rb +0 -93
  51. data/app/models/concerns/nexo/folder_policy.rb +0 -24
@@ -1,106 +1,220 @@
1
1
  module Nexo
2
2
  # Wrapper around +Google::Apis::CalendarV3+
3
3
  #
4
- # @raise [Google::Apis::ClientError] possible messages:
4
+ # TODO: handle ServerError to be retried
5
+ # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
6
+ # @raise [Google::Apis::AuthorizationError] Authorization is required
7
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
8
+ # possible messages:
5
9
  # - duplicate: The requested identifier already exists.
6
10
  # - notFound: Not Found (calendar not exists or was deleted)
7
11
  # - forbidden: Forbidden (event to update was deleted)
12
+ # - conditionNotMet: Precondition Failed (etag / if-match header verification failed)
13
+ # - invalid: Invalid sequence value. The specified sequence number is below
14
+ # the current sequence number of the resource. Re-fetch the resource and
15
+ # use its sequence number on the following request.
8
16
  #
9
17
  # TODO! when event to update was deleted, create a new one and warn
10
18
  class GoogleCalendarService < CalendarService
11
19
  # Create an event in a Google Calendar
12
20
  #
13
- # @param [Folder] folder
21
+ # @param [Element] folder
14
22
  #
15
- # @todo Debería recibir un {Element}?
16
- def insert(folder, calendar_event)
17
- validate_folder_state!(folder)
23
+ def insert(element)
24
+ validate_folder_state!(element.folder)
18
25
 
19
- event = build_event(calendar_event)
20
- response = client.insert_event(folder.external_identifier, event)
21
- ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag, id: response.id)
26
+ event = build_event(element)
27
+ response = client.insert_event(element.folder.external_identifier, event)
28
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag, id: response.id)
22
29
  end
23
30
 
24
31
  # Update an event in a Google Calendar
25
32
  def update(element)
26
33
  validate_folder_state!(element.folder)
34
+ # sidebranch
35
+ # TODO!: validate uuid presence
36
+
37
+ event = build_event(element)
38
+
39
+ response = client.update_event(element.folder.external_identifier, element.uuid, event, options: ifmatch_options(element))
27
40
 
28
- event = build_event(element.synchronizable)
29
- response = client.update_event(element.folder.external_identifier, element.uuid, event)
30
- ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag)
41
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag)
42
+ rescue Google::Apis::ClientError => e
43
+ if e.message.match? /conditionNotMet/
44
+ raise Errors::ConflictingRemoteElementChange, e
45
+ else
46
+ raise
47
+ end
31
48
  end
32
49
 
33
50
  # Delete an event in a Google Calendar
34
51
  def remove(element)
35
52
  validate_folder_state!(element.folder)
53
+ # sidebranch
54
+ # TODO!: validate uuid presence
36
55
 
37
56
  # TODO: try with cancelled
38
- client.delete_event(element.folder.external_identifier, element.uuid)
57
+ client.delete_event(element.folder.external_identifier, element.uuid, options: ifmatch_options(element))
39
58
  ApiResponse.new(payload: nil, status: :ok, etag: nil)
59
+ rescue Google::Apis::ClientError => e
60
+ if e.message.match? /conditionNotMet/
61
+ raise Errors::ConflictingRemoteElementChange, e
62
+ else
63
+ raise
64
+ end
65
+ end
66
+
67
+ def get_event(element)
68
+ validate_folder_state!(element.folder)
69
+ # sidebranch
70
+ # TODO!: validate uuid presence
71
+
72
+ # would be nice to send If-None-Match header, but Google API doesn't seem
73
+ # to accept it
74
+ response = client.get_event(element.folder.external_identifier, element.uuid)
75
+
76
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag, id: response.id)
77
+ rescue Google::Apis::ClientError => e
78
+ if e.message.match? "notFound"
79
+ Nexo.logger.warn("Event not found for #{element.to_gid}")
80
+ nil
81
+ else
82
+ raise
83
+ end
84
+ end
85
+
86
+ def get_calendar(folder)
87
+ validate_folder_state!(folder)
88
+
89
+ response = client.get_calendar(folder.external_identifier)
90
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag, id: response.id)
40
91
  end
41
92
 
42
93
  # Create a Google calendar
43
94
  def insert_calendar(folder)
44
- unless folder.integration.token?
45
- raise Errors::Error, "folder has no token"
46
- end
95
+ validate_folder_state!(folder, verify_external_identifier_presence: false)
47
96
 
48
97
  cal = build_calendar(folder)
49
98
  response = client.insert_calendar(cal)
50
- ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag, id: response.id)
99
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag, id: response.id)
51
100
  end
52
101
 
53
- # Create a Google calendar
102
+ # Update a Google calendar
54
103
  def update_calendar(folder)
55
- unless folder.integration.token?
56
- raise Errors::Error, "folder has no token"
57
- end
104
+ validate_folder_state!(folder)
58
105
 
59
106
  cal = build_calendar(folder)
60
107
  response = client.update_calendar(folder.external_identifier, cal)
61
- ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag, id: response.id)
108
+ ApiResponse.new(payload: response.to_h, status: :ok, etag: response.etag, id: response.id)
62
109
  end
63
110
 
64
111
  def remove_calendar(folder)
112
+ validate_folder_state!(folder)
113
+
65
114
  client.delete_calendar(folder.external_identifier)
66
115
  ApiResponse.new(status: :ok)
67
116
  end
68
117
 
69
- # @!visibility private
70
- # :nocov: non-production
71
- def clear_calendars
72
- Folder.all.each do |folder|
73
- cid = folder.external_identifier
74
- events = client.list_events(cid).items
75
- events.each do |event|
76
- client.delete_event(cid, event.id)
77
- end
78
- end
118
+ def fields_from_payload(payload)
119
+ event = validate_version!(payload)
120
+
121
+ {
122
+ date_from: parse_date(event.start),
123
+ date_to: parse_date(event.end),
124
+ time_from: parse_time(event.start),
125
+ time_to: parse_time(event.end),
126
+ summary: event.summary,
127
+ description: event.description,
128
+
129
+ # Posible status values:
130
+ # - confirmed
131
+ # - tentative
132
+ # - cancelled (deleted)
133
+ status: event.status
134
+ }
79
135
  end
80
- # :nocov:
81
136
 
82
137
  private
83
138
 
84
- def validate_folder_state!(folder)
139
+ def validate_version!(payload)
140
+ event_data = ActiveSupport::HashWithIndifferentAccess.new(payload)
141
+ event = Google::Apis::CalendarV3::Event.new(**event_data)
142
+
143
+ validate_datetime!(event.start)
144
+ validate_datetime!(event.end)
145
+
146
+ event
147
+ end
148
+
149
+ def parse_date(datetime)
150
+ if datetime["date"].present?
151
+ Date.parse(datetime["date"])
152
+ else
153
+ Date.parse(datetime["date_time"])
154
+ end
155
+ end
156
+
157
+ def parse_time(datetime)
158
+ if datetime["date"].present?
159
+ nil
160
+ else
161
+ Time.parse(datetime["date_time"])
162
+ end
163
+ end
164
+
165
+ def validate_datetime!(datetime)
166
+ unless datetime.is_a?(Hash) && (datetime["date"].present? || datetime["date_time"].present?)
167
+ # :nocov: borderline
168
+ raise "invalid datetime"
169
+ # :nocov:
170
+ end
171
+ end
172
+
173
+ def validate_folder_state!(folder, verify_external_identifier_presence: true)
85
174
  unless folder.integration.token?
86
175
  raise Errors::Error, "folder has no token"
87
176
  end
88
177
 
89
- if folder.external_identifier.blank?
178
+ if verify_external_identifier_presence && folder.external_identifier.blank?
90
179
  raise Errors::InvalidFolderState, folder
91
180
  end
92
181
  end
93
182
 
94
- def build_event(calendar_event)
183
+ def build_event(element)
184
+ calendar_event = element.synchronizable
95
185
  estart = build_event_date_time(calendar_event.datetime_from)
96
186
  eend = build_event_date_time(calendar_event.datetime_to)
97
187
 
98
- Google::Apis::CalendarV3::Event.new(
188
+ # TODO: sequence is managed by both google and nexo
189
+ # google accepts the sent sequence only if its valid (>= current_sequence)
190
+ # when we receive a remote change with updated sequence, we should
191
+ # update the secuence accordingly. and if we receive a remote change without a
192
+ # secuence increment, then we could:
193
+ # 1. not update the local secuence. but maybe thats not viable
194
+ # 2. increment the local secuence and update the remote
195
+ # 3. increment the local secuence and not update the remote
196
+ # 4. <current> ignore the sequence from google
197
+
198
+ from = element.last_remote_version
199
+
200
+ base_event =
201
+ if from.present?
202
+ event_data = ActiveSupport::HashWithIndifferentAccess.new(from.payload)
203
+ Google::Apis::CalendarV3::Event.new(**event_data)
204
+ else
205
+ Google::Apis::CalendarV3::Event.new
206
+ end
207
+
208
+ base_event.update!(
99
209
  start: estart,
100
210
  end: eend,
101
211
  summary: calendar_event.summary,
102
- description: calendar_event.description
212
+ description: calendar_event.description,
213
+ transparency: calendar_event.transparency,
214
+ # sequence: calendar_event.sequence
103
215
  )
216
+
217
+ base_event
104
218
  end
105
219
 
106
220
  def build_calendar(folder)
@@ -128,5 +242,14 @@ module Nexo
128
242
  end
129
243
  # :nocov:
130
244
  end
245
+
246
+ def ifmatch_options(element)
247
+ ifmatch = element.etag
248
+ Nexo.logger.debug { "ifmatch: #{ifmatch}" }
249
+
250
+ raise Errors::Error, "an etag is required to perform the request" if ifmatch.blank?
251
+
252
+ Google::Apis::RequestOptions.new(header: { "If-Match" => ifmatch })
253
+ end
131
254
  end
132
255
  end
@@ -0,0 +1,84 @@
1
+ module Nexo
2
+ class GoogleCalendarSyncService < GoogleCalendarService
3
+ def full_sync!(folder)
4
+ Nexo.logger.info("performing full sync")
5
+ sync!(folder)
6
+ end
7
+
8
+ def incremental_sync!(folder)
9
+ raise Nexo::Errors::Error, "no sync token" if folder.google_next_sync_token.blank?
10
+
11
+ Nexo.logger.debug("performing incremental sync")
12
+ sync!(folder, sync_token: folder.google_next_sync_token)
13
+ rescue Google::Apis::ClientError => e
14
+ if e.message.match /fullSyncRequired/
15
+ Nexo.logger.warn "Full sync required: #{e}"
16
+ full_sync!(folder)
17
+ else
18
+ raise
19
+ end
20
+ end
21
+
22
+ def full_or_incremental_sync!(folder)
23
+ if folder.google_next_sync_token.present?
24
+ incremental_sync!(folder)
25
+ else
26
+ full_sync!(folder)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
33
+ # TODO: handle 410 (Gone) to discard the sync_token and schedule a full_sync
34
+ # (fullSyncRequired: Sync token is no longer valid, a full sync is required.
35
+ def sync!(folder, page_token: nil, sync_token: nil)
36
+ page_token = nil
37
+ loop do
38
+ if page_token.present?
39
+ Nexo.logger.debug("Calling list_events with page_token")
40
+ elsif sync_token.present?
41
+ Nexo.logger.debug("Calling list_events with sync_token")
42
+ # raise Google::Apis::ClientError, "fullSyncRequired: Sync token is no longer valid..."
43
+ else
44
+ Nexo.logger.debug("Calling list_events without sync_token")
45
+ end
46
+
47
+ events = client.list_events(folder.external_identifier, page_token:, sync_token:)
48
+ Nexo.logger.debug("retrieved a page with #{events.items.length} items")
49
+ sync_token = nil
50
+
51
+ events.items.each do |event|
52
+ response = ApiResponse.new(payload: event.to_h, etag: event.etag, id: event.id)
53
+
54
+ element = Element.where(uuid: response.id).first
55
+ if element.present?
56
+ Nexo.logger.debug("Element found for event")
57
+
58
+ FetchRemoteResourceJob.new.handle_response(element, response)
59
+ else
60
+ element = ElementService.new.create_element_for_remote_resource!(folder, response)
61
+
62
+ # TODO!: rename handle_response
63
+ FetchRemoteResourceJob.new.handle_response(element, response)
64
+ end
65
+ end
66
+
67
+ page_token = events.next_page_token
68
+ if page_token
69
+ Nexo.logger.debug("Page token present. Fetching next page.")
70
+ else
71
+ sync_token = events.next_sync_token
72
+ if sync_token.present?
73
+ Nexo.logger.debug("Sync token present. Saving it to Folder")
74
+ folder.update!(google_next_sync_token: sync_token)
75
+ else
76
+ Nexo.logger.error("Sync token should have been present!")
77
+ end
78
+
79
+ break
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,236 @@
1
+ module Nexo
2
+ class ElementService
3
+ attr_accessor :element, :element_version
4
+
5
+ def initialize(element_version: nil, element: nil)
6
+ @element_version = element_version
7
+ @element = element || element_version&.element
8
+ end
9
+
10
+ def create_element_version!(attributes)
11
+ element.with_lock do
12
+ _create_element_version!(attributes)
13
+ end
14
+ end
15
+
16
+ def update_element_version!(attributes)
17
+ element.with_lock do
18
+ element_version.update!(attributes)
19
+ _update_ne_status!
20
+ end
21
+ end
22
+
23
+ def update_element!(attributes)
24
+ element.update!(attributes)
25
+ end
26
+
27
+ def discard!
28
+ element.update!(discarded_at: Time.current)
29
+ end
30
+
31
+ def flag_for_removal!(removal_reason)
32
+ Nexo.logger.debug("Flagging an element for removal")
33
+
34
+ # TODO!: the reason for this? just monitoring?
35
+ element.update!(flagged_for_removal: true, removal_reason:)
36
+ end
37
+
38
+ # @raise ActiveRecord::RecordNotUnique
39
+ def update_synchronizable!
40
+ Nexo.logger.debug("update_synchronizable!")
41
+
42
+ element.with_lock do
43
+ service = ServiceBuilder.instance.build_protocol_service(element_version.element.folder)
44
+ fields = service.fields_from_payload(element_version.payload)
45
+
46
+ # and set the Synchronizable fields according to the Folder#nexo_protocol
47
+ synchronizable = element_version.element.synchronizable
48
+ if synchronizable.present?
49
+ synchronizable.update_from_fields!(fields)
50
+
51
+ # synchronizable could have been destroyed
52
+ if synchronizable.persisted?
53
+ # si esto se ejecuta en paralelo con SynchronizableChangedJob? (para otro
54
+ # element del mismo synchronizable) puede haber race conditions
55
+ synchronizable.increment_sequence!
56
+ synchronizable.reload
57
+
58
+ ElementService.new(element_version:).update_element_version!(
59
+ sequence: synchronizable.sequence,
60
+ nev_status: :synced
61
+ )
62
+
63
+ SynchronizableChangedJob.perform_later(synchronizable, excluded_folders: [ element.folder.id ])
64
+ else
65
+ Nexo.logger.debug("Synchronizable destroyed. Removing other elements")
66
+ ElementService.new(element_version:).update_element_version!(
67
+ sequence: nil,
68
+ nev_status: :synced
69
+ )
70
+
71
+ FolderService.new.destroy_elements(
72
+ synchronizable, :synchronizable_destroyed, exclude_elements: [ element.id ])
73
+ end
74
+ else
75
+ Nexo.logger.info("Synchronizable not found")
76
+ policies = PolicyService.instance.policies_for(element.folder)
77
+ importer_rule = policies.select { |p| p.import_payload?(element_version.payload) }.first
78
+ if importer_rule.present?
79
+ Nexo.logger.debug("Found an importer rule")
80
+ synchronizable = importer_rule.create_synchronizable_from_payload!(element_version.payload)
81
+ ElementService.new(element:).update_element!(synchronizable:)
82
+ ElementService.new(element_version:).update_element_version!(
83
+ nev_status: :synced,
84
+ sequence: synchronizable.sequence
85
+ )
86
+ else
87
+ Nexo.logger.info("No importer rule found for event. Skipping")
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def resolve_conflict!
94
+ unless element.conflicted?
95
+ raise "element not conflicted"
96
+ end
97
+
98
+ # both lock on Element and start a transaction
99
+ element.with_lock do
100
+ external_change = element.element_versions.where(origin: :external, nev_status: :pending_sync).order(:etag).last
101
+ local_change = element.element_versions.where(origin: :internal, nev_status: :pending_sync).order(:sequence).last
102
+ last_synced = element.element_versions.where(nev_status: :synced).order(:sequence).last
103
+
104
+ if local_change.sequence < last_synced.sequence
105
+ raise "there a newer synced sequence"
106
+ end
107
+
108
+ if external_change.etag < last_synced.etag
109
+ raise "there a newer synced etag"
110
+ end
111
+
112
+ Nexo.logger.debug { "resolving conflict" }
113
+ remote_update = Time.zone.parse(external_change.payload["updated"])
114
+ local_update = element.synchronizable.updated_at
115
+ Nexo.logger.debug { "Remote updated at: #{remote_update}. Local updated at #{local_update}" }
116
+ if remote_update > local_update
117
+ Nexo.logger.debug { "Remote wins, ignoring local change" }
118
+ _update_status_on_conflict_with_winner!(external_change)
119
+ ImportRemoteElementVersion.new.perform(external_change)
120
+ else
121
+ Nexo.logger.debug { "Local wins, discarding remote changes" }
122
+ _update_status_on_conflict_with_winner!(local_change)
123
+ UpdateRemoteResourceJob.perform_later(local_change)
124
+ end
125
+ end
126
+ end
127
+
128
+ def update_ne_status!
129
+ element.with_lock do
130
+ _update_ne_status!
131
+ end
132
+ end
133
+
134
+ def create_internal_version_if_none!
135
+ # NOTE: though synchronizable it's not locked and could change in between
136
+ # the block, this shouldn't run concurrently because its called from
137
+ # SynchronizableChangedJob that its limited to one perform at a time per
138
+ # synchronizable
139
+ element.with_lock do
140
+ if element.element_versions.where(sequence: element.synchronizable.sequence).any?
141
+ Nexo.logger.debug { "There is a version for current sequence, nothing to do" }
142
+ return
143
+ end
144
+
145
+ _create_internal_version!
146
+ end
147
+ end
148
+
149
+ def create_element_for_remote_resource!(folder, response)
150
+ Element.create!(
151
+ folder:,
152
+ uuid: response.id,
153
+ ne_status: :pending_external_sync
154
+ )
155
+ end
156
+
157
+ def create_element_for!(folder, synchronizable)
158
+ Element.transaction do
159
+ @element = Element.create!(
160
+ synchronizable:,
161
+ folder:,
162
+ ne_status: :pending_local_sync
163
+ )
164
+ Nexo.logger.debug { "Element created" }
165
+
166
+ _create_internal_version!
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def _update_status_on_conflict_with_winner!(win_version)
173
+ element.element_versions.where(nev_status: :pending_sync)
174
+ .where.not(id: win_version.id)
175
+ .update_all(nev_status: :ignored_in_conflict)
176
+
177
+ _update_ne_status!
178
+ end
179
+
180
+ def _create_internal_version!
181
+ nev_status =
182
+ element.folder.sync_internal_changes? ? :pending_sync : :ignored_by_sync_direction
183
+
184
+ _create_element_version!(
185
+ origin: :internal,
186
+ sequence: element.synchronizable.sequence,
187
+ nev_status:
188
+ ).tap do |element_version|
189
+ Nexo.logger.debug("ElementVersion created")
190
+
191
+ if element.pending_local_sync?
192
+ Nexo.logger.debug("Enqueuing UpdateRemoteResourceJob")
193
+
194
+ UpdateRemoteResourceJob.perform_later(element_version)
195
+ elsif element.conflicted?
196
+ Nexo.logger.info("Element conflicted, so not enqueuing UpdateRemoteResourceJob")
197
+ elsif element.synced?
198
+ Nexo.logger.info("Element's ne_status is: #{element.ne_status}. No need to push any changes.")
199
+ else
200
+ # :nocov: borderline
201
+ Nexo.logger.info("Element's ne_status is: #{element.ne_status}. That's weird.")
202
+ # :nocov:
203
+ end
204
+ end
205
+ end
206
+
207
+ def _create_element_version!(attributes)
208
+ ElementVersion.create!(attributes.merge(element:)).tap do
209
+ _update_ne_status!
210
+ end
211
+ end
212
+
213
+ def _update_ne_status!
214
+ external_change =
215
+ element.folder.sync_external_changes? &&
216
+ element.element_versions.where(origin: :external, nev_status: :pending_sync).any?
217
+
218
+ local_change =
219
+ element.folder.sync_internal_changes? &&
220
+ element.element_versions.where(origin: :internal, nev_status: :pending_sync).any?
221
+
222
+ element.ne_status =
223
+ if external_change && local_change
224
+ :conflicted
225
+ elsif external_change
226
+ :pending_external_sync
227
+ elsif local_change
228
+ :pending_local_sync
229
+ else
230
+ :synced
231
+ end
232
+
233
+ element.save!
234
+ end
235
+ end
236
+ end
@@ -2,8 +2,12 @@ module Nexo
2
2
  class Errors
3
3
  class Error < StandardError; end
4
4
 
5
- class ElementConflicted < Error; end
6
- class ExternalUnsyncedChange < Error; end
5
+ class SynchronizableInvalid < Error; end
6
+ class ConflictingRemoteElementChange < Error; end
7
+ class UpdateRemoteVersionFailed < Error; end
8
+
9
+ # From here on, classes are subject to review
10
+ # A lot of them are never rescued explicitly
7
11
  class ElementAlreadySynced < Error; end
8
12
  class MoreThanOneElementInFolderForSynchronizable < Error; end
9
13
  class InvalidFolderState < Error; end
@@ -19,8 +23,6 @@ module Nexo
19
23
  # on EventReceiver
20
24
  class InvalidSynchronizableState < Error; end
21
25
 
22
- # on SyncElementJob
23
- class SyncElementJobError < Errors::Error; end
24
26
  class SynchronizableConflicted < Error; end
25
27
  class ElementDiscarded < Error; end
26
28
  class SynchronizableNotFound < Error; end
@@ -21,6 +21,8 @@ module Nexo
21
21
  end
22
22
 
23
23
  def folder_changed(folder)
24
+ return unless folder.sync_internal_changes?
25
+
24
26
  if folder.discarded?
25
27
  raise "folder discarded"
26
28
  end
@@ -28,6 +30,8 @@ module Nexo
28
30
  end
29
31
 
30
32
  def folder_discarded(folder)
33
+ return unless folder.sync_internal_changes?
34
+
31
35
  FolderDestroyJob.perform_later(folder)
32
36
  end
33
37