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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +161 -0
- data/LICENSE.txt +21 -0
- data/README.md +967 -0
- data/app/assets/javascripts/qube_rails/question.js +457 -0
- data/app/assets/stylesheets/qube_rails.css +1115 -0
- data/app/controllers/concerns/qube_rails/run_authorization.rb +37 -0
- data/app/controllers/qube_rails/connections_controller.rb +141 -0
- data/app/controllers/qube_rails/questions_controller.rb +132 -0
- data/app/controllers/qube_rails/runs_controller.rb +54 -0
- data/app/controllers/qube_rails/storyboard_controller.rb +50 -0
- data/app/controllers/qube_rails/webhooks_controller.rb +99 -0
- data/app/jobs/qube_rails/prune_runs_job.rb +18 -0
- data/app/jobs/qube_rails/refresh_job.rb +18 -0
- data/app/jobs/qube_rails/webhook_job.rb +43 -0
- data/app/models/qube_rails/application_record.rb +17 -0
- data/app/models/qube_rails/connection.rb +303 -0
- data/app/models/qube_rails/notification.rb +12 -0
- data/app/models/qube_rails/queued_request.rb +66 -0
- data/app/models/qube_rails/quickbooks_record.rb +36 -0
- data/app/models/qube_rails/request_delivery.rb +12 -0
- data/app/models/qube_rails/run.rb +187 -0
- data/app/models/qube_rails/sync_mark.rb +21 -0
- data/app/views/qube_rails/_error.html.erb +13 -0
- data/app/views/qube_rails/_question.html.erb +21 -0
- data/app/views/qube_rails/_run_status.html.erb +22 -0
- data/app/views/qube_rails/question/_choice.html.erb +20 -0
- data/app/views/qube_rails/question/_details.html.erb +6 -0
- data/app/views/qube_rails/question/_field.html.erb +15 -0
- data/app/views/qube_rails/question/_footer.html.erb +7 -0
- data/app/views/qube_rails/question/_message.html.erb +2 -0
- data/app/views/qube_rails/question/_pick.html.erb +19 -0
- data/app/views/qube_rails/question/_picks.html.erb +6 -0
- data/app/views/qube_rails/question/_question.html.erb +24 -0
- data/app/views/qube_rails/question/fields/_boolean.html.erb +7 -0
- data/app/views/qube_rails/question/fields/_number.html.erb +7 -0
- data/app/views/qube_rails/question/fields/_rows.html.erb +23 -0
- data/app/views/qube_rails/question/fields/_search.html.erb +23 -0
- data/app/views/qube_rails/question/fields/_select.html.erb +8 -0
- data/app/views/qube_rails/question/fields/_text.html.erb +7 -0
- data/app/views/qube_rails/question/rows/_cell.html.erb +24 -0
- data/app/views/qube_rails/question/rows/_row.html.erb +22 -0
- data/app/views/qube_rails/question/rows/_segmented.html.erb +14 -0
- data/app/views/qube_rails/run_status/_detail.html.erb +7 -0
- data/app/views/qube_rails/run_status/_redeliver.html.erb +5 -0
- data/app/views/qube_rails/run_status/_retry.html.erb +4 -0
- data/app/views/qube_rails/run_status/_stall.html.erb +3 -0
- data/app/views/qube_rails/run_status/_status.html.erb +21 -0
- data/app/views/qube_rails/run_status/_summary.html.erb +2 -0
- data/app/views/qube_rails/storyboard/show.html.erb +86 -0
- data/config/locales/en.yml +46 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20261025000000_create_qube_tables.rb +83 -0
- data/db/migrate/20261026230000_add_qube_sync_status_indexes.rb +13 -0
- data/db/migrate/20261027000000_create_qube_sync_marks.rb +17 -0
- data/db/migrate/20261028000000_create_qube_requests.rb +38 -0
- data/lib/generators/qube_rails/install_generator.rb +47 -0
- data/lib/generators/qube_rails/templates/connection_events.rb.tt +38 -0
- data/lib/generators/qube_rails/templates/initializer.rb.tt +82 -0
- data/lib/generators/qube_rails/workflows_generator.rb +77 -0
- data/lib/qube_rails/backend.rb +70 -0
- data/lib/qube_rails/cache_dedupe.rb +34 -0
- data/lib/qube_rails/configuration.rb +319 -0
- data/lib/qube_rails/connection_events.rb +167 -0
- data/lib/qube_rails/database_dedupe.rb +28 -0
- data/lib/qube_rails/engine.rb +33 -0
- data/lib/qube_rails/json_coder.rb +43 -0
- data/lib/qube_rails/operation.rb +464 -0
- data/lib/qube_rails/question_view.rb +219 -0
- data/lib/qube_rails/request.rb +289 -0
- data/lib/qube_rails/rows_view.rb +146 -0
- data/lib/qube_rails/run_status_view.rb +71 -0
- data/lib/qube_rails/storyboard/ask_about_add.json +60 -0
- data/lib/qube_rails/storyboard/choose.json +51 -0
- data/lib/qube_rails/storyboard/enum_select.json +56 -0
- data/lib/qube_rails/storyboard/missing_record.json +141 -0
- data/lib/qube_rails/storyboard/name_clash.json +59 -0
- data/lib/qube_rails/storyboard/near_match.json +77 -0
- data/lib/qube_rails/storyboard/number_boolean.json +54 -0
- data/lib/qube_rails/storyboard/record_search.json +90 -0
- data/lib/qube_rails/storyboard/refusal.json +53 -0
- data/lib/qube_rails/storyboard/rows.json +359 -0
- data/lib/qube_rails/storyboard.rb +165 -0
- data/lib/qube_rails/sync_status.rb +124 -0
- data/lib/qube_rails/version.rb +5 -0
- data/lib/qube_rails/view_helpers.rb +29 -0
- data/lib/qube_rails.rb +201 -0
- metadata +180 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: deb2eb2f7213591f12c48f2ecbf23e1eece273630d9b716b233c338fa4572a37
|
|
4
|
+
data.tar.gz: eeacddeebc0117e15dd7096d881d9a818eade8485a2d12f0e6565e0a5e015e56
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1989097d43b0a33d92b8adf5059d50d360250e5f967ca5bf60aa1e62b7703131fe95d2a21a803a78a1861d71ccb26b7cd5eda8cc8abaff384c70249091d00047
|
|
7
|
+
data.tar.gz: b8bc4a5015c9e93dd4433222aedbd842469691ed69c2bcac393d6743156ed792ec7b41fa9cdae571f1b635f48ccb84792264de18846bf5ea2b3b58f4a612eae9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (2026-10-27)
|
|
4
|
+
|
|
5
|
+
A rewrite on QuBe Sync's v2 API. The engine now holds the code QuBe Sync's integration kit used to
|
|
6
|
+
generate into every Rails app (the webhook endpoint, onboarding, questions, connections, runs and
|
|
7
|
+
QuickBooks ids), so your app keeps only its own operations, its connection events class and one
|
|
8
|
+
initializer, and gets fixes with `bundle update qube_rails`. Nothing from 0.1.0 carries over.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
**Install and configure**
|
|
13
|
+
- `rails g qube_rails:install`: the initializer, your app's `QuickbooksDesktop::ConnectionEvents`,
|
|
14
|
+
the engine's migrations and the mount line (`mount QubeRails::Engine => "/"`).
|
|
15
|
+
- `QubeRails.configure` in `config/initializers/qube_rails.rb`: operations, connection_events,
|
|
16
|
+
current_account, authorize_question, enqueue, queue, dedupe, parent_controller and the API
|
|
17
|
+
settings. A setting left unset raises `QubeRails::NotConfigured` naming that file.
|
|
18
|
+
|
|
19
|
+
**Connections and onboarding**
|
|
20
|
+
- Engine tables and models, from `bin/rails qube_rails:install:migrations`:
|
|
21
|
+
`QubeRails::Connection` (`qube_connections`; an account can have any number, one per company
|
|
22
|
+
file), `QubeRails::Run` (`qube_runs`: each run's reference, revision, state and question) and
|
|
23
|
+
`QubeRails::QuickbooksRecord` (`qube_quickbooks_records`: QuickBooks ids by connection). Your
|
|
24
|
+
own tables need no QuBe Sync columns. JSON is stored as text, the same on PostgreSQL, MySQL and
|
|
25
|
+
SQLite.
|
|
26
|
+
- Onboarding routes: `POST quickbooks/connections` (Connect) and
|
|
27
|
+
`POST quickbooks/connections/:id/onboarding`. Connect reuses the account's newest
|
|
28
|
+
never-connected connection; `new=1` makes another.
|
|
29
|
+
- Onboarding makes realtime connections (`config.connection_type`, `"realtime"` by default),
|
|
30
|
+
since workflows run only on realtime connections.
|
|
31
|
+
- Connect with `simulated=1`, outside production, makes a connection to a simulated QuickBooks
|
|
32
|
+
company file, connected as soon as it's made, for testing without a Windows computer.
|
|
33
|
+
- A connection event for a row that doesn't have its QuBe Sync id yet adopts the QuBe Sync
|
|
34
|
+
connection made with that row's webhook URL and name, created at or after the row (a
|
|
35
|
+
never-connected or simulated one), or raises `QubeRails::NotReady` to be retried.
|
|
36
|
+
- `DELETE quickbooks/connections/:id` (`connection_path`) disconnects a connection: QuBe Sync's
|
|
37
|
+
first, then the row with its runs, requests and links (`Connection#disconnect!`), after
|
|
38
|
+
`ConnectionEvents#disconnected`, where your app deletes its own rows that point at it.
|
|
39
|
+
|
|
40
|
+
**Starting workflows**
|
|
41
|
+
- `QubeRails::Operation`, the base of your operations: `start(record, connection)` returns the
|
|
42
|
+
run. A record's run that hasn't ended on that connection is returned (`run.reused?`) rather
|
|
43
|
+
than a second one started, so a double click can't add an invoice twice; an unconfirmed start
|
|
44
|
+
is sent again with the same reference (the Idempotency-Key) and input.
|
|
45
|
+
- A start that gets no answer stays `starting` with its input; `Run#retry_start!` and
|
|
46
|
+
`Operation.retry_start` resend it with the same Idempotency-Key. Only a 4xx, or an input that
|
|
47
|
+
raised, marks a run failed.
|
|
48
|
+
- `Operation#quickbooks_ref` and `#link_references`, and `QubeRails::MissingReference`, raised
|
|
49
|
+
from `input` when a reference QuickBooks requires has no link and no name, so the start fails
|
|
50
|
+
before anything is sent.
|
|
51
|
+
- Typed workflow starts: `bin/rails g qube_rails:workflows [--drafts] [--check]` writes
|
|
52
|
+
`app/qube/qube_workflows.rb` (`QubeWorkflows`, autoloaded) and `sig/qube_workflows.rbs` from
|
|
53
|
+
your app's workflow input types, with a typed start and input method per workflow and branch.
|
|
54
|
+
`Operation#input` can return one of those input methods' Hashes. `--check` exits 1 when the
|
|
55
|
+
file is out of date or the API key is refused, and warns but passes when QuBe Sync can't be
|
|
56
|
+
reached.
|
|
57
|
+
- A start QuBe Sync refuses because the connection isn't realtime raises
|
|
58
|
+
`QubeRails::RealtimeRequired`, whose message says how to upgrade.
|
|
59
|
+
|
|
60
|
+
**Plain requests**
|
|
61
|
+
- `QubeRails::Request`, the base of a plain v2 request (`OPERATION`, `SLUG`, `params`, and the
|
|
62
|
+
handlers `answered`, `done`, `failed`, `retrying`, `timed_out`). `.queue(record, connection)`
|
|
63
|
+
returns a `QubeRails::QueuedRequest`: one at a time per record, slug and connection, its
|
|
64
|
+
reference the Idempotency-Key, `retry_queue!` to send it again. Their webhooks arrive at
|
|
65
|
+
`POST webhooks/qube/requests/:id`; each delivery is taken once by `page_request_id:state`, and
|
|
66
|
+
`done` fires once every page is in.
|
|
67
|
+
|
|
68
|
+
**Webhooks**
|
|
69
|
+
- The webhook endpoint, `POST webhooks/qube/runs/:id` and `POST webhooks/qube/connections/:id`,
|
|
70
|
+
verifies each delivery and hands it to `QubeRails::WebhookJob` (ActiveJob), or to
|
|
71
|
+
`QubeRails.run(kind, id, body)` from your own queue. It answers 503 when the enqueue fails, so
|
|
72
|
+
QuBe Sync retries. A refused delivery's log line names its path and what to check (the webhook
|
|
73
|
+
secret, the clock, or a sender that isn't QuBe Sync).
|
|
74
|
+
- The reference, connection id, revision and delivery id are checked under the row's lock, and
|
|
75
|
+
notifications are deduplicated in the engine's `qube_notifications` table in the same
|
|
76
|
+
transaction as your handler, so a handler that raises runs again on the redelivery
|
|
77
|
+
(`QubeRails::CacheDedupe` is an option). `WebhookJob` retries (`job_attempts`, default 10) and
|
|
78
|
+
logs when it gives up. A delivery for a deleted run, connection or record is logged and dropped.
|
|
79
|
+
- `QubeRails::ConnectionEvents`, the base of your connection events class.
|
|
80
|
+
|
|
81
|
+
**Questions**
|
|
82
|
+
- The question routes, `POST qube/questions/runs/:id` and `GET .../search`, and the
|
|
83
|
+
`qube_rails/_question.html.erb` partial showing a record's latest run's question, which you can
|
|
84
|
+
override. `authorize_question` defaults to allowing only the account that owns the run's
|
|
85
|
+
connection; a refused one answers 404.
|
|
86
|
+
- When a question changed after the page showed it, the answer isn't sent and the page says
|
|
87
|
+
where it stands now.
|
|
88
|
+
- A rows field (the missing-records question) shows a block per record: its choice ("Create it",
|
|
89
|
+
"Use an existing one") as a segmented control, then only the fields that choice needs, with a
|
|
90
|
+
search per record column. "Set all to" and "Use for all N" fill every record at once. It never
|
|
91
|
+
scrolls sideways. A fixed list shows as a select.
|
|
92
|
+
- A question whose answer QuBe Sync took is `running` again at once (`Operation#answered(token,
|
|
93
|
+
taken: true)`), rather than waiting for the Web Connector's next poll.
|
|
94
|
+
|
|
95
|
+
**Runs and sync status**
|
|
96
|
+
- Every ending is kept on the run (state, outcome, error, cancel reason, ended at) before your
|
|
97
|
+
handler runs: `Run#ended?`, `#failed?`, `#cancelled?`, `#summary_text`, and the
|
|
98
|
+
`qube_rails/run_status` partial (taking `connection:`). A start unconfirmed for a minute reads
|
|
99
|
+
"Still waiting for QuickBooks" with a "Try sending again" button
|
|
100
|
+
(`POST qube/runs/:id/retry_start`).
|
|
101
|
+
- A run that looks stuck (`Run#looks_stuck?`) gets a "Check for updates" button, which has QuBe
|
|
102
|
+
Sync send its latest event again (`POST qube/runs/:id/redeliver`).
|
|
103
|
+
- While the connection a sending run waits on is stalled, the status reads "Waiting for
|
|
104
|
+
QuickBooks" and says why and what to do. `connection.stalled` and `connection.resumed` refresh
|
|
105
|
+
the pages of records with a run going on that connection.
|
|
106
|
+
- Sync status, derived from runs and links and never stored:
|
|
107
|
+
`connection.sync_status(record, operation:)` (`QubeRails::SyncStatus`: `:not_sent`,
|
|
108
|
+
`:sending`, `:needs_answer`, `:in_quickbooks`, `:done`, `:failed`, `:cancelled`, with `qb_id`,
|
|
109
|
+
`error` and `retry?`), `SyncStatus.latest`, and the connection's scopes `in_quickbooks`,
|
|
110
|
+
`not_in_quickbooks`, `needing_answer` and `sending`. Tested on SQLite, PostgreSQL and MySQL.
|
|
111
|
+
- The partials show why an answer or a retried start didn't go through, and with turbo-rails 2
|
|
112
|
+
a record's pages refresh when an event changes its run, and once more 5 seconds later
|
|
113
|
+
(`QubeRails::RefreshJob`) for a page that was still loading.
|
|
114
|
+
- `config.prune_runs_after` and `QubeRails::PruneRunsJob`: opt-in pruning of ended runs and
|
|
115
|
+
requests (`Run.prunable`), never within a day of their end; links stay. `qube_sync_marks` keeps
|
|
116
|
+
when each connection's last completed run of a workflow started
|
|
117
|
+
(`Connection#last_synced_at(workflow)`, `Operation#last_synced_at`), so a sync's next `since`
|
|
118
|
+
outlives pruned runs.
|
|
119
|
+
|
|
120
|
+
**Screens and styling** (README, "Styling")
|
|
121
|
+
- The question and run status partials (`render "qube_rails/question"`, `render
|
|
122
|
+
"qube_rails/run_status"`) are made of small pieces under `qube_rails/question/*`,
|
|
123
|
+
`qube_rails/run_status/*` and `qube_rails/_error`, each overridable at its path and rendered
|
|
124
|
+
from view-models whose fields are the contract (`QubeRails::QuestionView`, `ChoiceView`,
|
|
125
|
+
`FieldView`, `RowsView`, `RowView`, `CellView`, `RunStatusView`). A question's choices show by
|
|
126
|
+
kind: suggested records as option rows, each form in its own section with the one filled
|
|
127
|
+
button, and "None of these" and "Cancel" in a footer.
|
|
128
|
+
- `config.ui_classes`: your CSS framework's classes by piece, beside the `qube-` classes; an
|
|
129
|
+
unknown piece raises.
|
|
130
|
+
- `<%= qube_rails_assets %>` in your layout (`rails g qube_rails:install` adds it) serves the
|
|
131
|
+
behaviour script `qube_rails/question.js` (search boxes, rows fields), keyed only on
|
|
132
|
+
`data-qube-*` attributes, so it works on any markup, after Turbo navigation, with no CSP nonce.
|
|
133
|
+
`qube_rails_assets stylesheet: true` adds the opt-in stylesheet `qube_rails.css`: zero
|
|
134
|
+
specificity, themed by `--qube-*` variables, light and dark, no global styles.
|
|
135
|
+
- The screens never name QuBe Sync, runs, webhooks or ListIDs to your users, and every word comes
|
|
136
|
+
from I18n under `qube_rails.*` (`config/locales/en.yml`; `QubeRails.t`), so you can reword or
|
|
137
|
+
translate them.
|
|
138
|
+
- The storyboard, `GET qube/storyboard(/:screen)` (`config.storyboard`, on in development):
|
|
139
|
+
every screen from sample data, in your layout, with each piece's override path. Nothing is sent
|
|
140
|
+
or saved.
|
|
141
|
+
|
|
142
|
+
**QuickBooks ids**
|
|
143
|
+
- `Connection#link`, `#qb_id_for`, `#find_linked(qb_id, kind: nil)` and
|
|
144
|
+
`#quickbooks_record(qb_id, kind: nil)`. QuickBooks ids are kept by connection and list family
|
|
145
|
+
(`QuickbooksRecord.family_for`: one family for customers, vendors, employees and other names,
|
|
146
|
+
one for items, and each other list or transaction type its own), since QuickBooks gives out
|
|
147
|
+
ListIDs that way.
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
- Needs Ruby 3.3, Rails 7.0 and qube_sync 0.5.0 or later. The source stays Ruby 2.7 and Rails
|
|
151
|
+
5.2 compatible, and every call to qube_sync goes through `QubeRails::Backend`, so the kit can
|
|
152
|
+
vendor it into older apps.
|
|
153
|
+
|
|
154
|
+
### Removed
|
|
155
|
+
- Everything from 0.1.0 (v1): the generator's models, migrations, controllers and jobs, and the
|
|
156
|
+
path dependency on qube_sync.
|
|
157
|
+
|
|
158
|
+
## 0.1.0 (2024-01-21)
|
|
159
|
+
|
|
160
|
+
The v1 engine: generated QuickBooks entity models, connection and request tables, and a webhook
|
|
161
|
+
job for customer queries.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 QUBE Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|