nexo 0.1.5 → 0.1.7

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/app/controllers/nexo/element_versions_controller.rb +26 -0
  4. data/app/controllers/nexo/elements_controller.rb +70 -0
  5. data/app/controllers/nexo/folders_controller.rb +41 -0
  6. data/app/controllers/nexo/nexo_controller.rb +11 -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 +23 -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 +56 -26
  17. data/app/lib/nexo/api_client/google_auth_service.rb +9 -8
  18. data/app/lib/nexo/api_client/google_calendar_service.rb +162 -38
  19. data/app/lib/nexo/api_client/google_calendar_sync_service.rb +88 -0
  20. data/app/lib/nexo/element_service.rb +249 -0
  21. data/app/lib/nexo/errors.rb +6 -5
  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 +39 -2
  27. data/app/models/concerns/nexo/synchronizable.rb +28 -9
  28. data/app/models/nexo/element.rb +23 -34
  29. data/app/models/nexo/element_version.rb +17 -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/error.html.erb +8 -0
  34. data/app/views/layouts/nexo.html.erb +40 -0
  35. data/app/views/nexo/element_versions/show.html.erb +16 -0
  36. data/app/views/nexo/elements/index.html.erb +40 -0
  37. data/app/views/nexo/elements/show.html.erb +60 -0
  38. data/app/views/nexo/folders/index.html.erb +22 -0
  39. data/app/views/nexo/folders/show.html.erb +22 -0
  40. data/config/environment.rb +1 -0
  41. data/config/routes.rb +26 -0
  42. data/config/spring.rb +1 -0
  43. data/db/migrate/20250604124821_element_sync_status.rb +13 -0
  44. data/db/migrate/20250612002919_google_sync_tokens.rb +5 -0
  45. data/db/migrate/20250623132502_folder_sync_direction.rb +8 -0
  46. data/db/migrate/20250718012839_synchronizable_nullable.rb +6 -0
  47. data/db/seeds.rb +3 -2
  48. data/lib/nexo/engine.rb +26 -5
  49. data/lib/nexo/version.rb +1 -1
  50. metadata +39 -4
  51. data/app/jobs/nexo/sync_element_job.rb +0 -93
  52. data/app/models/concerns/nexo/folder_policy.rb +0 -24
@@ -1,106 +1,221 @@
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
- # TODO: try with cancelled
38
- client.delete_event(element.folder.external_identifier, element.uuid)
56
+ # TODO: try with cancelled / maybe its the same
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
+ status: calendar_event.nce_status,
215
+ # sequence: calendar_event.sequence
103
216
  )
217
+
218
+ base_event
104
219
  end
105
220
 
106
221
  def build_calendar(folder)
@@ -128,5 +243,14 @@ module Nexo
128
243
  end
129
244
  # :nocov:
130
245
  end
246
+
247
+ def ifmatch_options(element)
248
+ ifmatch = element.etag
249
+ Nexo.logger.debug { "ifmatch: #{ifmatch}" }
250
+
251
+ raise Errors::Error, "an etag is required to perform the request" if ifmatch.blank?
252
+
253
+ Google::Apis::RequestOptions.new(header: { "If-Match" => ifmatch })
254
+ end
131
255
  end
132
256
  end
@@ -0,0 +1,88 @@
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.info("performing incremental sync")
12
+
13
+ sync!(folder, sync_token: folder.google_next_sync_token)
14
+ rescue Google::Apis::ClientError => e
15
+ if e.message.match /fullSyncRequired/
16
+ Nexo.logger.warn "Full sync required: #{e}"
17
+ full_sync!(folder)
18
+ else
19
+ raise
20
+ end
21
+ end
22
+
23
+ def full_or_incremental_sync!(folder)
24
+ if folder.google_next_sync_token.present?
25
+ incremental_sync!(folder)
26
+ else
27
+ full_sync!(folder)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
34
+ # TODO: handle 410 (Gone) to discard the sync_token and schedule a full_sync
35
+ # (fullSyncRequired: Sync token is no longer valid, a full sync is required.
36
+ def sync!(folder, page_token: nil, sync_token: nil)
37
+ page_token = nil
38
+ loop do
39
+ if page_token.present?
40
+ Nexo.logger.debug("Calling list_events with page_token")
41
+ elsif sync_token.present?
42
+ Nexo.logger.debug("Calling list_events with sync_token")
43
+ # raise Google::Apis::ClientError, "fullSyncRequired: Sync token is no longer valid..."
44
+ else
45
+ Nexo.logger.debug("Calling list_events without sync_token")
46
+ end
47
+
48
+ events = client.list_events(folder.external_identifier, page_token:, sync_token:)
49
+ Nexo.logger.debug("retrieved a page with #{events.items.length} items")
50
+ sync_token = nil
51
+
52
+ events.items.each do |event|
53
+ response = ApiResponse.new(payload: event.to_h, etag: event.etag, id: event.id)
54
+
55
+ element = Element.kept.where(uuid: response.id).first
56
+
57
+ if element.present?
58
+ Nexo.logger.debug("Element found for event")
59
+
60
+ FetchRemoteResourceJob.new.handle_response(element, response)
61
+ elsif event.status == "cancelled"
62
+ Nexo.logger.debug("Skipping cancelled event")
63
+ else
64
+ element = ElementService.new.create_element_for_remote_resource!(folder, response)
65
+
66
+ # TODO!: rename handle_response
67
+ FetchRemoteResourceJob.new.handle_response(element, response)
68
+ end
69
+ end
70
+
71
+ page_token = events.next_page_token
72
+ if page_token
73
+ Nexo.logger.debug("Page token present. Fetching next page.")
74
+ else
75
+ sync_token = events.next_sync_token
76
+ if sync_token.present?
77
+ Nexo.logger.debug("Sync token present. Saving it to Folder")
78
+ folder.update!(google_next_sync_token: sync_token)
79
+ else
80
+ Nexo.logger.error("Sync token should have been present!")
81
+ end
82
+
83
+ break
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,249 @@
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
+ _update_ne_status!
30
+ end
31
+
32
+ # The reason of `flagged_for_removal` is that there is a time gap between
33
+ # the the user action of removing the element and the actual API call that
34
+ # deletes the remote element. At some point is necesary to know if the
35
+ # element is being deleted, like when fetching a remote version previous to
36
+ # the actual delete API call.
37
+ def flag_for_removal!(removal_reason)
38
+ Nexo.logger.debug("Flagging an element for removal")
39
+
40
+ element.update!(flagged_for_removal: true, removal_reason:)
41
+ _update_ne_status!
42
+ end
43
+
44
+ # @raise ActiveRecord::RecordNotUnique
45
+ def update_synchronizable!
46
+ Nexo.logger.debug("update_synchronizable!")
47
+
48
+ element.with_lock do
49
+ service = ServiceBuilder.instance.build_protocol_service(element_version.element.folder)
50
+ fields = service.fields_from_payload(element_version.payload)
51
+
52
+ # and set the Synchronizable fields according to the Folder#nexo_protocol
53
+ synchronizable = element_version.element.synchronizable
54
+ if element.flagged_for_removal?
55
+ Nexo.logger.info("Element flagged for removal")
56
+ ElementService.new(element_version:).update_element_version!(
57
+ nev_status: :ignored_by_deletion
58
+ )
59
+ elsif synchronizable.present?
60
+ synchronizable.update_from_fields!(fields)
61
+
62
+ # synchronizable could have been destroyed
63
+ if synchronizable.persisted?
64
+ # si esto se ejecuta en paralelo con SynchronizableChangedJob? (para otro
65
+ # element del mismo synchronizable) puede haber race conditions
66
+ synchronizable.increment_sequence!
67
+ synchronizable.reload
68
+
69
+ ElementService.new(element_version:).update_element_version!(
70
+ sequence: synchronizable.sequence,
71
+ nev_status: :synced
72
+ )
73
+
74
+ SynchronizableChangedJob.perform_later(synchronizable, excluded_folders: [ element.folder.id ])
75
+ else
76
+ Nexo.logger.debug("Synchronizable destroyed. Removing other elements")
77
+ ElementService.new(element_version:).update_element_version!(
78
+ sequence: nil,
79
+ nev_status: :synced
80
+ )
81
+
82
+ FolderService.new.destroy_elements(
83
+ synchronizable, :synchronizable_destroyed, exclude_elements: [ element.id ])
84
+ end
85
+ else
86
+ Nexo.logger.info("Synchronizable not found")
87
+ policies = PolicyService.instance.policies_for(element.folder)
88
+ importer_rule = policies.select { |p| p.import_payload?(element_version.payload) }.first
89
+ if importer_rule.present?
90
+ Nexo.logger.debug("Found an importer rule")
91
+ synchronizable = importer_rule.create_synchronizable_from_payload!(element_version.payload)
92
+ ElementService.new(element:).update_element!(synchronizable:)
93
+ ElementService.new(element_version:).update_element_version!(
94
+ nev_status: :synced,
95
+ sequence: synchronizable.sequence
96
+ )
97
+ else
98
+ Nexo.logger.info("No importer rule found for event. Skipping")
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def resolve_conflict!
105
+ unless element.conflicted?
106
+ raise "element not conflicted"
107
+ end
108
+
109
+ # both lock on Element and start a transaction
110
+ element.with_lock do
111
+ external_change = element.element_versions.where(origin: :external, nev_status: :pending_sync).order(:etag).last
112
+ local_change = element.element_versions.where(origin: :internal, nev_status: :pending_sync).order(:sequence).last
113
+ last_synced = element.element_versions.where(nev_status: :synced).order(:sequence).last
114
+
115
+ if local_change.sequence < last_synced.sequence
116
+ raise "there a newer synced sequence"
117
+ end
118
+
119
+ if external_change.etag < last_synced.etag
120
+ raise "there a newer synced etag"
121
+ end
122
+
123
+ Nexo.logger.debug { "resolving conflict" }
124
+ remote_update = Time.zone.parse(external_change.payload["updated"])
125
+ local_update = element.synchronizable.updated_at
126
+ Nexo.logger.debug { "Remote updated at: #{remote_update}. Local updated at #{local_update}" }
127
+ if remote_update > local_update
128
+ Nexo.logger.debug { "Remote wins, ignoring local change" }
129
+ _update_status_on_conflict_with_winner!(external_change)
130
+ ImportRemoteElementVersion.new.perform(external_change)
131
+ else
132
+ Nexo.logger.debug { "Local wins, discarding remote changes" }
133
+ _update_status_on_conflict_with_winner!(local_change)
134
+ UpdateRemoteResourceJob.perform_later(local_change)
135
+ end
136
+ end
137
+ end
138
+
139
+ def update_ne_status!
140
+ element.with_lock do
141
+ _update_ne_status!
142
+ end
143
+ end
144
+
145
+ def create_internal_version_if_none!
146
+ # NOTE: though synchronizable it's not locked and could change in between
147
+ # the block, this shouldn't run concurrently because its called from
148
+ # SynchronizableChangedJob that its limited to one perform at a time per
149
+ # synchronizable
150
+ element.with_lock do
151
+ if element.element_versions.where(sequence: element.synchronizable.sequence).any?
152
+ Nexo.logger.debug { "There is a version for current sequence, nothing to do" }
153
+ return
154
+ end
155
+
156
+ _create_internal_version!
157
+ end
158
+ end
159
+
160
+ def create_element_for_remote_resource!(folder, response)
161
+ Element.create!(
162
+ folder:,
163
+ uuid: response.id,
164
+ ne_status: :pending_external_sync
165
+ )
166
+ end
167
+
168
+ def create_element_for!(folder, synchronizable)
169
+ Element.transaction do
170
+ @element = Element.create!(
171
+ synchronizable:,
172
+ folder:,
173
+ ne_status: :pending_local_sync
174
+ )
175
+ Nexo.logger.debug { "Element created" }
176
+
177
+ _create_internal_version!
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def _update_status_on_conflict_with_winner!(win_version)
184
+ element.element_versions.where(nev_status: :pending_sync)
185
+ .where.not(id: win_version.id)
186
+ .update_all(nev_status: :ignored_in_conflict)
187
+
188
+ _update_ne_status!
189
+ end
190
+
191
+ def _create_internal_version!
192
+ nev_status =
193
+ element.folder.sync_internal_changes? ? :pending_sync : :ignored_by_sync_direction
194
+
195
+ _create_element_version!(
196
+ origin: :internal,
197
+ sequence: element.synchronizable.sequence,
198
+ nev_status:
199
+ ).tap do |element_version|
200
+ Nexo.logger.debug("ElementVersion created")
201
+
202
+ if element.pending_local_sync?
203
+ Nexo.logger.debug("Enqueuing UpdateRemoteResourceJob")
204
+
205
+ UpdateRemoteResourceJob.perform_later(element_version)
206
+ elsif element.conflicted?
207
+ Nexo.logger.info("Element conflicted, so not enqueuing UpdateRemoteResourceJob")
208
+ elsif element.synced?
209
+ Nexo.logger.info("Element's ne_status is: #{element.ne_status}. No need to push any changes.")
210
+ else
211
+ # :nocov: borderline
212
+ Nexo.logger.info("Element's ne_status is: #{element.ne_status}. That's weird.")
213
+ # :nocov:
214
+ end
215
+ end
216
+ end
217
+
218
+ def _create_element_version!(attributes)
219
+ ElementVersion.create!(attributes.merge(element:)).tap do
220
+ _update_ne_status!
221
+ end
222
+ end
223
+
224
+ def _update_ne_status!
225
+ external_change =
226
+ element.folder.sync_external_changes? &&
227
+ element.element_versions.where(origin: :external, nev_status: :pending_sync).any?
228
+
229
+ local_change =
230
+ element.folder.sync_internal_changes? &&
231
+ element.element_versions.where(origin: :internal, nev_status: :pending_sync).any?
232
+
233
+ element.ne_status =
234
+ if external_change && local_change
235
+ :conflicted
236
+ elsif external_change
237
+ :pending_external_sync
238
+ elsif local_change
239
+ :pending_local_sync
240
+ elsif element.flagged_for_removal? && element.discarded_at.nil?
241
+ :pending_remote_delete
242
+ else
243
+ :synced
244
+ end
245
+
246
+ element.save!
247
+ end
248
+ end
249
+ end
@@ -2,13 +2,16 @@ 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
10
14
  class FolderDiscarded < Error; end
11
- class SynchronizableDiscarded < Error; end
12
15
 
13
16
  # on ControllerHelper
14
17
  class InvalidParamsError < Error; end
@@ -19,8 +22,6 @@ module Nexo
19
22
  # on EventReceiver
20
23
  class InvalidSynchronizableState < Error; end
21
24
 
22
- # on SyncElementJob
23
- class SyncElementJobError < Errors::Error; end
24
25
  class SynchronizableConflicted < Error; end
25
26
  class ElementDiscarded < Error; end
26
27
  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