nexo 0.1.6 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aca3e3e57c524de464408274ab6309fbea580931420c9da736556d7997ed6bd6
4
- data.tar.gz: 770c4033d4dc280ad32471800e59a6d6d5d5af7a150897d52e5fa69de043b393
3
+ metadata.gz: a18eacef8af04c17f7ce29a9d0101696ebc42f9337f7773cb1b7418a653f57e4
4
+ data.tar.gz: 5a7664dc27f2f2d9143e8bb4db675289f7c2d4ff063ef5603cac59a183e3a9b3
5
5
  SHA512:
6
- metadata.gz: 299fe496fcaa3041699cf37fa39e6d897005a4d7a5343c912694ff72cec18a91d19d317c91898c6cd1d55aeba606967298f1ce73ea40008cc14efa4acd62d807
7
- data.tar.gz: 63057165522974406b55ec157a46e1c664d17b81e341e7dd04a551e93266e94bc38a0501297d3151f4a0636ad7d8c01e1cd2f739a7bc37cc340f37f53b435d8b
6
+ metadata.gz: a16900fc806a754bb90cef361babb57e770e3322596909bf50d1312eabab836b9131941784c5d773ce2d00698e0db087de93bf41db7743d12596004a8b6c0782
7
+ data.tar.gz: 6bd7ef667816a643303898202e5ea7de7404ddf539120243ddb5b4c3d6bdcf86f5fab35d49cd2c19b740097fb7bd981f199336094abce06b8b3ddfada5a2e657
data/README.md CHANGED
@@ -28,15 +28,21 @@ Configure good_job
28
28
 
29
29
  ## Unhandled cases / TODO's
30
30
 
31
- - Restore locally discarded Events. It creates a new Google Calendar event?
31
+ - Buscar la manera de wrappear los jobs en un with_tenant
32
+ cuáles jobs?
33
+ los que se disparan desde Google webhooks
34
+ aunque quizá la posta es qeu esas routes estén scopeadas dentro del tid
35
+ - Testear el "no longer included in folder"
36
+ o sea, modificar una rule y ver qué ande
32
37
 
38
+ - Restore locally discarded Events. It creates a new Google Calendar event?
33
39
  - Recurring google events
34
-
35
40
  - IMPORTANTE: loggear las exceptions de los jobs
36
-
37
41
  - Llevar mucho control de los jobs y las exceptions que puedan surgir como
38
42
  ActiveRecord::PreparedStatementCacheExpired
39
43
 
44
+
45
+
40
46
  ## Contributing
41
47
  Contribution directions go here.
42
48
 
@@ -20,8 +20,6 @@ module Nexo
20
20
  end
21
21
 
22
22
  redirect_to @element_version, notice:
23
- rescue StandardError => e
24
- redirect_to @element_version, alert: e.message
25
23
  end
26
24
  end
27
25
  end
@@ -42,8 +42,6 @@ module Nexo
42
42
  EventReceiver.new.synchronizable_updated(event)
43
43
 
44
44
  redirect_to @element, notice: "Modified"
45
- rescue StandardError => e
46
- redirect_to @element, alert: e.message
47
45
  end
48
46
 
49
47
  def fetch_remote
@@ -56,8 +54,16 @@ module Nexo
56
54
  ElementService.new(element: @element).resolve_conflict!
57
55
 
58
56
  redirect_to @element, notice: "Conflict solved"
59
- rescue StandardError => e
60
- redirect_to @element, alert: e.message
57
+ end
58
+
59
+ def modify
60
+ case params[:operation]
61
+ when "delete"
62
+ DeleteRemoteResourceJob.perform_later(@element)
63
+ redirect_to @element, notice: "Enqueued DeleteRemoteResourceJob"
64
+ else
65
+ redirect_to @element, alert: "Unknown action"
66
+ end
61
67
  end
62
68
  end
63
69
  end
@@ -1,5 +1,11 @@
1
1
  module Nexo
2
- class NexoController < ApplicationController
2
+ class NexoController < Nexo.admin_controller_parent_class
3
3
  layout "nexo"
4
+
5
+ rescue_from StandardError do |e|
6
+ @exception = e
7
+
8
+ render "layouts/error"
9
+ end
4
10
  end
5
11
  end
@@ -3,18 +3,30 @@ module Nexo
3
3
  class DeleteRemoteResourceJob < BaseJob
4
4
  include ApiClients
5
5
 
6
- def perform(element)
7
- if element.discarded?
8
- raise "element already discarded"
9
- end
6
+ retry_on(Errors::ConflictingRemoteElementChange, attempts: 2)
10
7
 
11
- unless element.flagged_for_removal?
12
- raise "element not flagged for removal"
13
- end
8
+ # Once discarded, an Element is obsolete and cant be restored because even
9
+ # though some APIs may allow it, others may not.
10
+ def perform(element)
11
+ raise "element already discarded" if element.discarded?
14
12
 
15
13
  ServiceBuilder.instance.build_protocol_service(element.folder).remove(element)
16
14
 
17
15
  ElementService.new(element:).discard!
16
+
17
+ # Fetch the removed/cancelled version
18
+ FetchRemoteResourceJob.perform_now(element)
19
+ rescue Errors::ConflictingRemoteElementChange => e
20
+ Nexo.logger.warn <<~STR
21
+ ConflictingRemoteElementChange for #{element.to_gid}. \
22
+
23
+ Performing now FetchRemoteResourceJob, so next attempt should succeed
24
+ STR
25
+
26
+ # NOTE: maybe this should be performed always directly in the first attempt?
27
+ FetchRemoteResourceJob.perform_now(element)
28
+
29
+ raise e
18
30
  end
19
31
  end
20
32
  end
@@ -70,10 +70,6 @@ module Nexo
70
70
  raise Errors::ElementDiscarded
71
71
  end
72
72
 
73
- if element.synchronizable.respond_to?(:discarded?) && element.synchronizable.discarded?
74
- raise Errors::SynchronizableDiscarded
75
- end
76
-
77
73
  if element.folder.discarded?
78
74
  raise Errors::FolderDiscarded
79
75
  end
@@ -53,7 +53,7 @@ module Nexo
53
53
  # sidebranch
54
54
  # TODO!: validate uuid presence
55
55
 
56
- # TODO: try with cancelled
56
+ # TODO: try with cancelled / maybe its the same
57
57
  client.delete_event(element.folder.external_identifier, element.uuid, options: ifmatch_options(element))
58
58
  ApiResponse.new(payload: nil, status: :ok, etag: nil)
59
59
  rescue Google::Apis::ClientError => e
@@ -211,6 +211,7 @@ module Nexo
211
211
  summary: calendar_event.summary,
212
212
  description: calendar_event.description,
213
213
  transparency: calendar_event.transparency,
214
+ status: calendar_event.nce_status,
214
215
  # sequence: calendar_event.sequence
215
216
  )
216
217
 
@@ -8,7 +8,8 @@ module Nexo
8
8
  def incremental_sync!(folder)
9
9
  raise Nexo::Errors::Error, "no sync token" if folder.google_next_sync_token.blank?
10
10
 
11
- Nexo.logger.debug("performing incremental sync")
11
+ Nexo.logger.info("performing incremental sync")
12
+
12
13
  sync!(folder, sync_token: folder.google_next_sync_token)
13
14
  rescue Google::Apis::ClientError => e
14
15
  if e.message.match /fullSyncRequired/
@@ -51,11 +52,14 @@ module Nexo
51
52
  events.items.each do |event|
52
53
  response = ApiResponse.new(payload: event.to_h, etag: event.etag, id: event.id)
53
54
 
54
- element = Element.where(uuid: response.id).first
55
+ element = Element.kept.where(uuid: response.id).first
56
+
55
57
  if element.present?
56
58
  Nexo.logger.debug("Element found for event")
57
59
 
58
60
  FetchRemoteResourceJob.new.handle_response(element, response)
61
+ elsif event.status == "cancelled"
62
+ Nexo.logger.debug("Skipping cancelled event")
59
63
  else
60
64
  element = ElementService.new.create_element_for_remote_resource!(folder, response)
61
65
 
@@ -26,13 +26,19 @@ module Nexo
26
26
 
27
27
  def discard!
28
28
  element.update!(discarded_at: Time.current)
29
+ _update_ne_status!
29
30
  end
30
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.
31
37
  def flag_for_removal!(removal_reason)
32
38
  Nexo.logger.debug("Flagging an element for removal")
33
39
 
34
- # TODO!: the reason for this? just monitoring?
35
40
  element.update!(flagged_for_removal: true, removal_reason:)
41
+ _update_ne_status!
36
42
  end
37
43
 
38
44
  # @raise ActiveRecord::RecordNotUnique
@@ -45,7 +51,12 @@ module Nexo
45
51
 
46
52
  # and set the Synchronizable fields according to the Folder#nexo_protocol
47
53
  synchronizable = element_version.element.synchronizable
48
- if synchronizable.present?
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?
49
60
  synchronizable.update_from_fields!(fields)
50
61
 
51
62
  # synchronizable could have been destroyed
@@ -226,6 +237,8 @@ module Nexo
226
237
  :pending_external_sync
227
238
  elsif local_change
228
239
  :pending_local_sync
240
+ elsif element.flagged_for_removal? && element.discarded_at.nil?
241
+ :pending_remote_delete
229
242
  else
230
243
  :synced
231
244
  end
@@ -12,7 +12,6 @@ module Nexo
12
12
  class MoreThanOneElementInFolderForSynchronizable < Error; end
13
13
  class InvalidFolderState < Error; end
14
14
  class FolderDiscarded < Error; end
15
- class SynchronizableDiscarded < Error; end
16
15
 
17
16
  # on ControllerHelper
18
17
  class InvalidParamsError < Error; end
@@ -4,6 +4,11 @@ module Nexo
4
4
 
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ # nce_status (Nexo Calendar Event Status)
8
+ #
9
+ # - confirmed
10
+ # - tentative (unused)
11
+ # - cancelled (deleted)
7
12
  define_protocol(:nexo_calendar_event, %i[
8
13
  date_from
9
14
  date_to
@@ -11,6 +16,7 @@ module Nexo
11
16
  time_to
12
17
  summary
13
18
  description
19
+ nce_status
14
20
  ])
15
21
 
16
22
  def change_is_significative_to_sequence?
@@ -8,6 +8,10 @@ module Nexo
8
8
  # inserta en un google calendar y luego se elimina, luego ya no se
9
9
  # puede volver a insertar con el mismo uuid, sería mejor que el id se
10
10
  # genere automáticamente por google y guardarlo en Element
11
+ #
12
+ # sequence
13
+ # autoincremental integer, starting at 0. its recommended that
14
+ # the attribute is always initialized with 0 value on creation
11
15
  %i[
12
16
  sequence
13
17
  ]
@@ -42,9 +46,8 @@ module Nexo
42
46
  fields = service.fields_from_payload(payload)
43
47
 
44
48
  synchronizable = new
45
- attributes = synchronizable.translate_fields(fields)
49
+ attributes = synchronizable.translate_fields(fields, for_create: true, folder:)
46
50
  synchronizable.assign_attributes(attributes)
47
- synchronizable.sequence = payload["sequence"]
48
51
  synchronizable.save!
49
52
 
50
53
  synchronizable
@@ -81,7 +84,7 @@ module Nexo
81
84
  raise "must be implemented in subclass"
82
85
  end
83
86
 
84
- def translate_fields(fields)
87
+ def translate_fields(fields, for_create: false, folder: nil)
85
88
  raise "must be implemented in subclass"
86
89
  end
87
90
  # :nocov:
@@ -32,7 +32,13 @@ module Nexo
32
32
  scope :kept, -> { where(discarded_at: nil) }
33
33
 
34
34
  enum :removal_reason, no_longer_included_in_folder: 0, synchronizable_destroyed: 1
35
- enum :ne_status, synced: 0, pending_external_sync: 1, pending_local_sync: 2, conflicted: 3
35
+
36
+ enum :ne_status,
37
+ synced: 0,
38
+ pending_external_sync: 1,
39
+ pending_local_sync: 2,
40
+ conflicted: 3,
41
+ pending_remote_delete: 4
36
42
 
37
43
  scope :conflicted, -> { where(ne_status: :conflicted) }
38
44
 
@@ -39,10 +39,15 @@ module Nexo
39
39
  synced: 1,
40
40
  ignored_in_conflict: 2,
41
41
  superseded: 3,
42
- ignored_by_sync_direction: 4
42
+ ignored_by_sync_direction: 4,
43
+ ignored_by_deletion: 5
43
44
 
44
45
  serialize :payload, coder: JSON
45
46
 
46
47
  validates :origin, presence: true
48
+
49
+ def remote_status
50
+ payload["status"] if payload.is_a?(Hash)
51
+ end
47
52
  end
48
53
  end
@@ -0,0 +1,8 @@
1
+ <h1>ERROR</h1>
2
+
3
+ <% if @exception.present? %>
4
+ <h4><%= @exception.message %></h4>
5
+ <pre>
6
+ <%= JSON.pretty_generate @exception.backtrace %>
7
+ </pre>
8
+ <% end %>
@@ -7,8 +7,6 @@
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
 
10
- <%= yield :head %>
11
-
12
10
  <style>
13
11
  table th {
14
12
  font-weight: bold;
@@ -5,22 +5,30 @@
5
5
  <table>
6
6
  <tr>
7
7
  <th></th>
8
+ <th>ID</th>
8
9
  <th>User</th>
9
10
  <th>Folder</th>
10
11
  <th>Synchronizable</th>
11
12
  <th>Status</th>
12
13
  <th>Updated at</th>
13
14
  <th>Created at</th>
15
+ <th>Remote status</th>
16
+ <th>Flagged for removal?</th>
17
+ <th>Discarded at</th>
14
18
  </tr>
15
19
  <% @elements.each do |element| %>
16
20
  <tr>
17
21
  <td><%= link_to "Show", element_path(element) %></td>
22
+ <td><%= element.id %></td>
18
23
  <td><%= element.folder.integration.user %></td>
19
24
  <td><%= element.folder %></td>
20
- <td><%= "#{element.synchronizable_type}: #{element.synchronizable}" %></td>
25
+ <td><%= truncate "#{element.synchronizable_type}: #{element.synchronizable}", length: 50 %></td>
21
26
  <td><%= element.ne_status %></td>
22
27
  <td><%= element.updated_at %></td>
23
28
  <td><%= element.created_at %></td>
29
+ <td><%= element.last_remote_version&.remote_status %></td>
30
+ <td><%= element.flagged_for_removal %></td>
31
+ <td><%= element.discarded_at %></td>
24
32
  </tr>
25
33
  <% end %>
26
34
  </table>
@@ -28,5 +36,5 @@
28
36
  <div class="paginator">
29
37
  <%= page_entries_info @elements %>
30
38
  <%= paginate @elements %>
39
+ <%#= paginate @elements, theme: 'kaminari' %>
31
40
  </div>
32
-
@@ -17,6 +17,10 @@
17
17
  <%= form_with(url: update_status_element_path(@element)) do |f| %>
18
18
  <%= f.submit "Update status" %>
19
19
  <% end %>
20
+
21
+ <%= form_with(url: modify_element_path(@element)) do |f| %>
22
+ <%= f.button(:operation, value: "delete") { "Delete" } %>
23
+ <% end %>
20
24
  <br>
21
25
 
22
26
  <h4>Element</h4>
@@ -47,7 +51,7 @@
47
51
  <td><%= version.origin %></td>
48
52
  <td><%= version.nev_status %></td>
49
53
  <td><%= version.sequence %></td>
50
- <td><%= version.payload["status"] %></td>
54
+ <td><%= version.remote_status %></td>
51
55
  <td><%= version.etag %></td>
52
56
  <td><%= version.created_at %></td>
53
57
  <td><%= version.updated_at %></td>
data/config/routes.rb CHANGED
@@ -14,6 +14,7 @@ Nexo::Engine.routes.draw do
14
14
  post :resolve_conflict
15
15
  post :modify_local
16
16
  post :update_status
17
+ post :modify
17
18
  end
18
19
  end
19
20
 
@@ -1,9 +1,11 @@
1
1
  class ElementSyncStatus < ActiveRecord::Migration[7.2]
2
2
  def change
3
- add_column :nexo_elements, :ne_status, :integer, null: false
3
+ add_column :nexo_elements, :ne_status, :integer
4
+ change_column_null :nexo_elements, :ne_status, false, -1
4
5
  remove_column :nexo_elements, :conflicted, :boolean, default: false, null: false
5
6
 
6
- add_column :nexo_element_versions, :nev_status, :integer, null: false
7
+ add_column :nexo_element_versions, :nev_status, :integer
8
+ change_column_null :nexo_element_versions, :nev_status, false, -1
7
9
 
8
10
  add_index :nexo_element_versions, [:element_id, :sequence], unique: true, where: "sequence IS NOT NULL"
9
11
  add_index :nexo_element_versions, [:element_id, :etag], unique: true, where: "etag IS NOT NULL"
data/lib/nexo/engine.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "googleauth"
2
2
  require "google-apis-oauth2_v2"
3
3
  require "google-apis-calendar_v3"
4
+ require "kaminari"
4
5
 
5
6
  module Nexo
6
7
  def self.folder_rules
@@ -20,6 +21,9 @@ module Nexo
20
21
  end
21
22
 
22
23
  mattr_accessor :api_jobs_throttle
24
+ mattr_accessor :admin_controller_parent_class
25
+
26
+ self.admin_controller_parent_class = ActionController::Base
23
27
 
24
28
  # @!visibility private
25
29
  class Engine < ::Rails::Engine
data/lib/nexo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # :nocov: non-viable
2
2
  module Nexo
3
- VERSION = "0.1.6"
3
+ VERSION = "0.1.7"
4
4
  end
5
5
  # :nocov:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martín Rosso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-18 00:00:00.000000000 Z
11
+ date: 2025-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -128,6 +128,7 @@ files:
128
128
  - app/models/nexo/folder.rb
129
129
  - app/models/nexo/integration.rb
130
130
  - app/models/nexo/token.rb
131
+ - app/views/layouts/error.html.erb
131
132
  - app/views/layouts/nexo.html.erb
132
133
  - app/views/nexo/element_versions/show.html.erb
133
134
  - app/views/nexo/elements/index.html.erb