qube_rails 0.2.0

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +161 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +967 -0
  5. data/app/assets/javascripts/qube_rails/question.js +457 -0
  6. data/app/assets/stylesheets/qube_rails.css +1115 -0
  7. data/app/controllers/concerns/qube_rails/run_authorization.rb +37 -0
  8. data/app/controllers/qube_rails/connections_controller.rb +141 -0
  9. data/app/controllers/qube_rails/questions_controller.rb +132 -0
  10. data/app/controllers/qube_rails/runs_controller.rb +54 -0
  11. data/app/controllers/qube_rails/storyboard_controller.rb +50 -0
  12. data/app/controllers/qube_rails/webhooks_controller.rb +99 -0
  13. data/app/jobs/qube_rails/prune_runs_job.rb +18 -0
  14. data/app/jobs/qube_rails/refresh_job.rb +18 -0
  15. data/app/jobs/qube_rails/webhook_job.rb +43 -0
  16. data/app/models/qube_rails/application_record.rb +17 -0
  17. data/app/models/qube_rails/connection.rb +303 -0
  18. data/app/models/qube_rails/notification.rb +12 -0
  19. data/app/models/qube_rails/queued_request.rb +66 -0
  20. data/app/models/qube_rails/quickbooks_record.rb +36 -0
  21. data/app/models/qube_rails/request_delivery.rb +12 -0
  22. data/app/models/qube_rails/run.rb +187 -0
  23. data/app/models/qube_rails/sync_mark.rb +21 -0
  24. data/app/views/qube_rails/_error.html.erb +13 -0
  25. data/app/views/qube_rails/_question.html.erb +21 -0
  26. data/app/views/qube_rails/_run_status.html.erb +22 -0
  27. data/app/views/qube_rails/question/_choice.html.erb +20 -0
  28. data/app/views/qube_rails/question/_details.html.erb +6 -0
  29. data/app/views/qube_rails/question/_field.html.erb +15 -0
  30. data/app/views/qube_rails/question/_footer.html.erb +7 -0
  31. data/app/views/qube_rails/question/_message.html.erb +2 -0
  32. data/app/views/qube_rails/question/_pick.html.erb +19 -0
  33. data/app/views/qube_rails/question/_picks.html.erb +6 -0
  34. data/app/views/qube_rails/question/_question.html.erb +24 -0
  35. data/app/views/qube_rails/question/fields/_boolean.html.erb +7 -0
  36. data/app/views/qube_rails/question/fields/_number.html.erb +7 -0
  37. data/app/views/qube_rails/question/fields/_rows.html.erb +23 -0
  38. data/app/views/qube_rails/question/fields/_search.html.erb +23 -0
  39. data/app/views/qube_rails/question/fields/_select.html.erb +8 -0
  40. data/app/views/qube_rails/question/fields/_text.html.erb +7 -0
  41. data/app/views/qube_rails/question/rows/_cell.html.erb +24 -0
  42. data/app/views/qube_rails/question/rows/_row.html.erb +22 -0
  43. data/app/views/qube_rails/question/rows/_segmented.html.erb +14 -0
  44. data/app/views/qube_rails/run_status/_detail.html.erb +7 -0
  45. data/app/views/qube_rails/run_status/_redeliver.html.erb +5 -0
  46. data/app/views/qube_rails/run_status/_retry.html.erb +4 -0
  47. data/app/views/qube_rails/run_status/_stall.html.erb +3 -0
  48. data/app/views/qube_rails/run_status/_status.html.erb +21 -0
  49. data/app/views/qube_rails/run_status/_summary.html.erb +2 -0
  50. data/app/views/qube_rails/storyboard/show.html.erb +86 -0
  51. data/config/locales/en.yml +46 -0
  52. data/config/routes.rb +35 -0
  53. data/db/migrate/20261025000000_create_qube_tables.rb +83 -0
  54. data/db/migrate/20261026230000_add_qube_sync_status_indexes.rb +13 -0
  55. data/db/migrate/20261027000000_create_qube_sync_marks.rb +17 -0
  56. data/db/migrate/20261028000000_create_qube_requests.rb +38 -0
  57. data/lib/generators/qube_rails/install_generator.rb +47 -0
  58. data/lib/generators/qube_rails/templates/connection_events.rb.tt +38 -0
  59. data/lib/generators/qube_rails/templates/initializer.rb.tt +82 -0
  60. data/lib/generators/qube_rails/workflows_generator.rb +77 -0
  61. data/lib/qube_rails/backend.rb +70 -0
  62. data/lib/qube_rails/cache_dedupe.rb +34 -0
  63. data/lib/qube_rails/configuration.rb +319 -0
  64. data/lib/qube_rails/connection_events.rb +167 -0
  65. data/lib/qube_rails/database_dedupe.rb +28 -0
  66. data/lib/qube_rails/engine.rb +33 -0
  67. data/lib/qube_rails/json_coder.rb +43 -0
  68. data/lib/qube_rails/operation.rb +464 -0
  69. data/lib/qube_rails/question_view.rb +219 -0
  70. data/lib/qube_rails/request.rb +289 -0
  71. data/lib/qube_rails/rows_view.rb +146 -0
  72. data/lib/qube_rails/run_status_view.rb +71 -0
  73. data/lib/qube_rails/storyboard/ask_about_add.json +60 -0
  74. data/lib/qube_rails/storyboard/choose.json +51 -0
  75. data/lib/qube_rails/storyboard/enum_select.json +56 -0
  76. data/lib/qube_rails/storyboard/missing_record.json +141 -0
  77. data/lib/qube_rails/storyboard/name_clash.json +59 -0
  78. data/lib/qube_rails/storyboard/near_match.json +77 -0
  79. data/lib/qube_rails/storyboard/number_boolean.json +54 -0
  80. data/lib/qube_rails/storyboard/record_search.json +90 -0
  81. data/lib/qube_rails/storyboard/refusal.json +53 -0
  82. data/lib/qube_rails/storyboard/rows.json +359 -0
  83. data/lib/qube_rails/storyboard.rb +165 -0
  84. data/lib/qube_rails/sync_status.rb +124 -0
  85. data/lib/qube_rails/version.rb +5 -0
  86. data/lib/qube_rails/view_helpers.rb +29 -0
  87. data/lib/qube_rails.rb +201 -0
  88. metadata +180 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When each connection's last completed run of a workflow started: a sync's next since, kept
4
+ # apart from qube_runs so pruning runs never loses it.
5
+ class CreateQubeSyncMarks < ActiveRecord::Migration[5.2]
6
+ def change
7
+ create_table :qube_sync_marks do |t|
8
+ t.bigint :connection_id, null: false
9
+ t.string :workflow, null: false, limit: 100
10
+ t.datetime :started_at, null: false
11
+ t.timestamps
12
+ end
13
+ add_index :qube_sync_marks, [:connection_id, :workflow], unique: true,
14
+ name: "index_qube_sync_marks_on_connection_and_workflow"
15
+ add_foreign_key :qube_sync_marks, :qube_connections, column: :connection_id, on_delete: :cascade
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Plain v2 requests your app queues (QubeRails::Request), and the deliveries each has taken, by
4
+ # page_request_id and state. params and error are JSON kept as text; never a response body.
5
+ class CreateQubeRequests < ActiveRecord::Migration[5.2]
6
+ def change
7
+ create_table :qube_requests do |t|
8
+ t.bigint :connection_id, null: false
9
+ t.string :operation, null: false, limit: 100
10
+ t.string :record_type, null: false, limit: 100
11
+ t.string :record_id, null: false
12
+ t.string :reference, null: false
13
+ t.string :qube_id
14
+ t.string :state, null: false, default: "queued", limit: 50
15
+ t.text :params
16
+ t.text :error
17
+ t.integer :pages_answered, null: false, default: 0
18
+ t.integer :last_page
19
+ t.datetime :ended_at
20
+ t.timestamps null: false
21
+ end
22
+ add_index :qube_requests, :reference, unique: true, name: "index_qube_requests_on_reference"
23
+ add_index :qube_requests, [:connection_id, :record_type, :record_id, :operation, :created_at],
24
+ name: "index_qube_requests_on_connection_and_record"
25
+ add_foreign_key :qube_requests, :qube_connections, column: :connection_id
26
+
27
+ create_table :qube_request_deliveries do |t|
28
+ t.bigint :request_id, null: false
29
+ t.string :dedupe_key, null: false, limit: 200
30
+ t.integer :page
31
+ t.string :state, null: false, limit: 50
32
+ t.datetime :created_at, null: false
33
+ end
34
+ add_index :qube_request_deliveries, [:request_id, :dedupe_key], unique: true,
35
+ name: "index_qube_request_deliveries_on_request_and_key"
36
+ add_foreign_key :qube_request_deliveries, :qube_requests, column: :request_id, on_delete: :cascade
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QubeRails
6
+ module Generators
7
+ # rails g qube_rails:install
8
+ #
9
+ # Writes the app's side of a QuBe Sync integration, for an app that doesn't start from the
10
+ # integration kit: config/initializers/qube_rails.rb (every setting, commented), your class
11
+ # for a connection's events (app/services/quickbooks_desktop/connection_events.rb), the
12
+ # engine's migration (qube_connections, qube_runs, qube_notifications and
13
+ # qube_quickbooks_records, through `qube_rails:install:migrations`), the engine's mount
14
+ # line in config/routes.rb, and qube_rails_assets in your layout. Your own tables need nothing.
15
+ class InstallGenerator < Rails::Generators::Base
16
+ source_root File.expand_path("templates", __dir__)
17
+
18
+ def create_initializer
19
+ template "initializer.rb.tt", "config/initializers/qube_rails.rb"
20
+ end
21
+
22
+ def create_connection_events
23
+ template "connection_events.rb.tt", "app/services/quickbooks_desktop/connection_events.rb"
24
+ end
25
+
26
+ def copy_migrations
27
+ rails_command "qube_rails:install:migrations"
28
+ end
29
+
30
+ def mount_engine
31
+ route 'mount QubeRails::Engine => "/"'
32
+ end
33
+
34
+ # The question's behaviour script (its search boxes and rows table), in the layout's <head>.
35
+ def include_assets
36
+ layout = "app/views/layouts/application.html.erb"
37
+ path = File.join(destination_root, layout)
38
+ unless File.exist?(path)
39
+ return say_status(:skip, "no #{layout}: add <%= qube_rails_assets %> to your layout's <head>", :yellow)
40
+ end
41
+ return if File.read(path).include?("qube_rails_assets")
42
+
43
+ insert_into_file layout, " <%= qube_rails_assets %>\n", before: " </head>"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # What your app does when one of its customers' QuickBooks Desktop connections changes. The
2
+ # qube_rails gem does the rest: it checks each event is this connection's and newer than the
3
+ # last one applied, under the connection row's lock, and updates the row's status
4
+ # ("connected", "setup_cancelled", "stalled") and stall_reason before your handler runs.
5
+ #
6
+ # connection is the QubeRails::Connection (one company file; an account can have several) and
7
+ # owner the account that connected it. Start a run on it with
8
+ # QuickbooksDesktop::<Operation>.start(record, connection).
9
+ module QuickbooksDesktop
10
+ class ConnectionEvents < QubeRails::ConnectionEvents
11
+ # QuickBooks is connected: show it, and start syncing.
12
+ def connected(event)
13
+ log_event(event)
14
+ end
15
+
16
+ # They chose "Cancel setup" on the setup page: offer to set it up again
17
+ # (POST /quickbooks/connections/<id>/onboarding).
18
+ def setup_cancelled(event)
19
+ log_event(event)
20
+ end
21
+
22
+ # Your requests are waiting and QuickBooks hasn't been reached. Show the customer, and tell
23
+ # them what connection.stall_reason says:
24
+ # - "not_checking_in": the Web Connector isn't calling in. Turn on the QuickBooks computer,
25
+ # and check the Web Connector runs.
26
+ # - "quickbooks_unavailable": it is calling in and can't reach QuickBooks. Open QuickBooks
27
+ # with the company file.
28
+ # A stall can send this again with a new reason: show the newest. resumed clears it.
29
+ def stalled(event)
30
+ log_event(event)
31
+ end
32
+
33
+ # Clear what stalled showed, whichever its reason.
34
+ def resumed(event)
35
+ log_event(event)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ # QuBe Sync (qube_rails): the only place the engine reads your app from. A setting left unset
2
+ # raises QubeRails::NotConfigured, naming this file, the first time it's needed. Fill in
3
+ # current_account before anything else.
4
+ #
5
+ # The engine's routes come from `mount QubeRails::Engine => "/"` in config/routes.rb:
6
+ # POST /quickbooks/connections a new connection for the signed-in account
7
+ # POST /quickbooks/connections/:id/onboarding one of its connections, through setup again
8
+ # POST /webhooks/qube/runs/:id QuBe Sync's webhooks (signed, no sign-in)
9
+ # POST /webhooks/qube/connections/:id
10
+ # POST /qube/questions/runs/:id an answer, behind your sign-in and CSRF
11
+ # GET /qube/questions/runs/:id/search a record field's matches
12
+ # GET /qube/storyboard every screen, sample data (config.storyboard)
13
+ #
14
+ # Its tables (qube_connections, qube_runs, qube_notifications, qube_quickbooks_records) keep
15
+ # every connection, run and QuickBooks id; your own tables need nothing.
16
+ QubeRails.configure do |config|
17
+ # Each operation by its SLUG, as class names (resolved when used, so code reloading works).
18
+ # Add one per QubeRails::Operation subclass in app/services/quickbooks_desktop/.
19
+ config.operations = {
20
+ # "create_invoice" => "QuickbooksDesktop::CreateInvoice",
21
+ }
22
+
23
+ # Your class for a connection's own events (connected, setup cancelled, stalled, resumed).
24
+ config.connection_events = "QuickbooksDesktop::ConnectionEvents"
25
+
26
+ # The signed-in customer's account, from the controller handling the request (your
27
+ # current_user and other helpers are on it). It owns the connections it makes, and only its
28
+ # own can be set up again; nil answers 401.
29
+ # config.current_account = ->(controller) { controller.current_user.account }
30
+
31
+ # Whether the signed-in user may answer or search the question run asks about record. By
32
+ # default, yes when the signed-in account (current_account) owns the run's connection, and no
33
+ # (404, logged) otherwise. Set it to check more, such as whether this user may see the record,
34
+ # or to let staff act across accounts. Only true allows.
35
+ # config.authorize_question = ->(controller, record, run) {
36
+ # run.connection.owner == controller.current_user.account &&
37
+ # controller.current_user.can_edit?(record)
38
+ # }
39
+
40
+ # How a verified webhook reaches a worker. The default is ActiveJob (QubeRails::WebhookJob) on
41
+ # config.queue. For another queue, hand it on and call QubeRails.run from its worker:
42
+ # config.enqueue = ->(delivery) {
43
+ # MyQueue.push(delivery[:kind], delivery[:id], delivery[:body])
44
+ # }
45
+ # config.queue = :default
46
+
47
+ # Delete ended runs after this long, when QubeRails::PruneRunsJob runs (schedule it). Off
48
+ # until set.
49
+ # config.prune_runs_after = 90.days
50
+
51
+ # Onboarding makes realtime connections: workflows run only on those (billed as realtime).
52
+ # config.connection_type = "realtime"
53
+ # How many times QubeRails::WebhookJob runs a delivery whose work raised, with a growing wait,
54
+ # before it logs an error and raises to your adapter. With Sidekiq, whose own retries follow,
55
+ # a smaller number (or 1, to leave retries to Sidekiq) avoids retrying twice over.
56
+ # config.job_attempts = 10
57
+
58
+ # Where notification delivery ids are remembered, so a redelivery runs its handler once. The
59
+ # default is the engine's qube_notifications table, written in the same transaction as the
60
+ # handler. Rails.cache instead needs a cache your workers share, and isn't transactional:
61
+ # config.dedupe = QubeRails::CacheDedupe.new(Rails.cache, expires_in: 1.day)
62
+
63
+ # Your CSS framework's classes for the engine's screens, by piece, added beside their qube-
64
+ # classes (pieces: QubeRails::Configuration::UI_PIECES; see the README's "Styling"):
65
+ # config.ui_classes = { button: "btn btn-primary", button_secondary: "btn btn-link",
66
+ # input: "form-control", select: "form-select", label: "form-label" }
67
+
68
+ # GET /qube/storyboard: every screen with sample data, in your layout. On in development.
69
+ # config.storyboard = Rails.env.development?
70
+
71
+ # The controller onboarding and the questions inherit, so your sign-in and CSRF apply.
72
+ # config.parent_controller = "ApplicationController"
73
+
74
+ # Read from the environment when unset: QUBE_API_KEY, QUBE_URL (https://qubesync.com) and
75
+ # QUBE_WEBHOOK_SECRET, which `qube env --write .env` writes, and QUBE_WEBHOOK_URL, the base of
76
+ # each webhook URL (`qube listen --write-env .env` in development; your app's /webhooks/qube in
77
+ # production). webhook_secret may be a list while a secret rotates. Never logged.
78
+ # config.api_key = ENV.fetch("QUBE_API_KEY")
79
+ # config.base_url = "https://qubesync.com"
80
+ # config.webhook_secret = [ENV.fetch("QUBE_WEBHOOK_SECRET"), ENV.fetch("QUBE_WEBHOOK_SECRET_OLD")]
81
+ # config.webhook_url = "https://example.com/webhooks/qube"
82
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module QubeRails
6
+ module Generators
7
+ # rails g qube_rails:workflows [--drafts] [--check]
8
+ #
9
+ # Writes your app's typed workflow starts: app/qube/qube_workflows.rb (QubeWorkflows, a
10
+ # module per workflow with a start method per branch of its input, autoloaded from app/qube)
11
+ # and sig/qube_workflows.rbs, from the workflow input types QuBe Sync serves for your app's
12
+ # API key (config.api_key). Each start uses QubeRails.client. Check both files in, and run it
13
+ # again when a workflow is published.
14
+ #
15
+ # --drafts types each workflow's draft where it has one. --check writes nothing, and exits 1
16
+ # when the file is out of date (or QuBe Sync refuses the key), for CI; when QuBe Sync can't be
17
+ # reached it warns and passes.
18
+ class WorkflowsGenerator < Rails::Generators::Base
19
+ RUBY = "app/qube/qube_workflows.rb"
20
+ SIG = "sig/qube_workflows.rbs"
21
+ COMMAND = "bin/rails g qube_rails:workflows"
22
+ CLIENT = "::QubeRails.client"
23
+
24
+ class_option :drafts, type: :boolean, default: false,
25
+ desc: "Type each workflow's draft where it has one"
26
+ class_option :check, type: :boolean, default: false,
27
+ desc: "Write nothing; exit 1 when #{RUBY} is out of date"
28
+
29
+ def write_or_check
30
+ options[:check] ? check : write
31
+ end
32
+
33
+ private
34
+
35
+ def write
36
+ document = Backend.workflow_types(QubeRails.client, include_drafts: options[:drafts])
37
+ files = Backend.render_workflow_types(document, source: source, drafts: options[:drafts], command: COMMAND,
38
+ client_expression: CLIENT)
39
+ warn_each(files.warnings)
40
+ create_file RUBY, files.ruby, force: true
41
+ create_file SIG, files.rbs, force: true
42
+ rescue Backend::APIError => e
43
+ fail_with(e.message)
44
+ rescue *Backend::NETWORK_ERRORS, SystemCallError => e
45
+ fail_with("couldn't reach QuBe Sync (#{e.class}: #{e.message}); nothing was written")
46
+ end
47
+
48
+ def check
49
+ path = File.join(destination_root, RUBY)
50
+ fail_with("#{RUBY} isn't there: run `#{COMMAND}` to write it") unless File.exist?(path)
51
+
52
+ result = Backend.check_workflow_types(File.read(path), client: QubeRails.client, source: source, path: RUBY,
53
+ command: COMMAND, client_expression: CLIENT)
54
+ warn_each(result.warnings)
55
+ case result.status
56
+ when :current then say_status :current, result.message, :green
57
+ when :unreachable then say_status :warning, result.message, :yellow
58
+ else fail_with(result.message)
59
+ end
60
+ end
61
+
62
+ def source
63
+ "#{QubeRails.config.base_url.to_s.sub(%r{/+\z}, '')}/api/v2/workflows/types"
64
+ end
65
+
66
+ def warn_each(warnings)
67
+ warnings.each { |warning| say_status :warning, warning, :yellow }
68
+ end
69
+
70
+ # A failing check stops the command with status 1, so CI fails.
71
+ def fail_with(message)
72
+ say_status :error, message, :red
73
+ exit 1
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "qube_sync/webhooks"
4
+ require "qube_sync/client"
5
+
6
+ module QubeRails
7
+ # Every call qube_rails makes to the qube_sync gem goes through here, and nowhere else: the
8
+ # client, verifying and parsing a webhook, and the errors they raise. An app on a Ruby the gem
9
+ # doesn't run on (it needs 3.3) gets a copy of qube_rails whose Backend does the same inline,
10
+ # with Net::HTTP and OpenSSL, so the rest of these files stay the same in both.
11
+ #
12
+ # What the rest of qube_rails relies on:
13
+ # - client(api_key:, base_url:, on_retry:) answers create_connection, update_connection,
14
+ # regenerate_onboarding, start_run, answer and search as QubeSync::Client does, returning the
15
+ # response's data, and raises APIError (status, body) for anything outside 2xx;
16
+ # - verify(raw_body, header, secrets:) raises SignatureError (with a reason: missing, malformed,
17
+ # stale or mismatch) unless the delivery checks out;
18
+ # - parse(raw_body) returns the typed event (QubeSync::Webhooks' WorkflowRunEvent,
19
+ # NotificationEvent, ConnectionEvent and the rest), raising ParseError for a body that isn't
20
+ # a JSON object;
21
+ # - workflow_types(client, include_drafts:, etag:), render_workflow_types(document, ...) and
22
+ # check_workflow_types(existing, ...) are `bin/rails g qube_rails:workflows`'s: the app's
23
+ # workflow input types as QuBe Sync serves them, rendered into app/qube/qube_workflows.rb and
24
+ # its RBS, and compared with a written file (QubeSync::WorkflowTypes). They need the gem's
25
+ # renderer, so the inline backend raises for them, saying typed workflows need the qube_sync
26
+ # gem (Ruby 3.3).
27
+ module Backend
28
+ APIError = QubeSync::APIError
29
+ SignatureError = QubeSync::Webhooks::SignatureError
30
+ ParseError = QubeSync::Webhooks::ParseError
31
+ # What a call that got no answer raises (a timeout, a refused or dropped connection).
32
+ NETWORK_ERRORS = QubeSync::Client::NETWORK_ERRORS
33
+
34
+ module_function
35
+
36
+ def client(api_key:, base_url:, on_retry: nil)
37
+ QubeSync::Client.new(api_key: api_key, base_url: base_url, on_retry: on_retry)
38
+ end
39
+
40
+ def verify(raw_body, header, secrets:)
41
+ QubeSync::Webhooks.verify(raw_body, header, secrets: secrets)
42
+ end
43
+
44
+ def parse(raw_body)
45
+ QubeSync::Webhooks.parse(raw_body)
46
+ end
47
+
48
+ # The app's workflow types document, or :not_modified when etag (its digest) is still current.
49
+ def workflow_types(client, include_drafts: false, etag: nil)
50
+ client.workflow_types(include_drafts: include_drafts, etag: etag)
51
+ end
52
+
53
+ # The Ruby and the RBS for a document (ruby, rbs, digest, drafts, warnings). source is the URL
54
+ # the header names, command the one it says to run again, client_expression what a start's
55
+ # client defaults to.
56
+ def render_workflow_types(document, source:, drafts:, command:, client_expression:)
57
+ require "qube_sync/workflow_types"
58
+ QubeSync::WorkflowTypes.render(document, source: source, drafts: drafts, command: command,
59
+ client: client_expression)
60
+ end
61
+
62
+ # A written file (its Ruby source) against what QuBe Sync serves now: status (:current,
63
+ # :stale, :unreachable or :error), message, changes, warnings and exit_status.
64
+ def check_workflow_types(existing, client:, source:, path:, command:, client_expression:)
65
+ require "qube_sync/workflow_types"
66
+ QubeSync::WorkflowTypes.check(existing, client: client, source: source, path: path, command: command,
67
+ client_expression: client_expression)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QubeRails
4
+ # A dedupe setting for an app that would rather not use the engine's table
5
+ # (QubeRails::DatabaseDedupe, the default): the notification delivery ids already taken, in
6
+ # Rails.cache, for a day. It needs a store your workers share (Redis, Solid Cache): with a
7
+ # per-process store (memory, or the file store on more than one machine) a retry that reaches
8
+ # another worker isn't caught, and the null store remembers nothing. It isn't transactional:
9
+ # the id is remembered after the handler commits, so a crash between the two runs the handler
10
+ # again on the redelivery.
11
+ # config.dedupe = QubeRails::CacheDedupe.new(Rails.cache, expires_in: 1.day)
12
+ class CacheDedupe
13
+ DAY = 24 * 60 * 60
14
+
15
+ def initialize(cache = nil, expires_in: DAY)
16
+ @cache = cache
17
+ @expires_in = expires_in
18
+ end
19
+
20
+ def seen?(key)
21
+ cache.exist?(key)
22
+ end
23
+
24
+ def remember(key)
25
+ cache.write(key, true, expires_in: @expires_in)
26
+ end
27
+
28
+ private
29
+
30
+ def cache
31
+ @cache || Rails.cache
32
+ end
33
+ end
34
+ end