jbr 3.4.0 → 3.5.1
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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +12 -7
- data/lib/graphql/client.rb +16 -2
- data/lib/jbr/job.rb +5 -5
- data/lib/jbr/jobs.rb +5 -3
- data/lib/jbr/line_item.rb +9 -14
- data/lib/jbr/mock/line_item.rb +0 -2
- data/lib/jbr/oauth.rb +5 -0
- data/lib/jbr/throttle.rb +3 -1
- data/lib/jbr/version.rb +1 -1
- data/lib/jbr/visits.rb +3 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5c1536ff754171e5bafc8e5501088b0372c673c98e3891786b43da0a69f5c75
|
|
4
|
+
data.tar.gz: 61bf56b9de47ec4ada6e406967c36240a95351d50a007730ce2506c0d7244864
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52d0070f75aa56d58874e301c0bb11b8b78fb108ac2327617094f9ee3676302fb8b59dee89942203dc993305356a43165b0fb053ac0257f1cb0f1299949ad5b3
|
|
7
|
+
data.tar.gz: 73c45d7ed55f796c7e0068eb19c33bc07e0a1ac2359a594b12eca096b43e8bf86058e4f67b3f03877d02e337a87ee81be3efca500df6deb6b9470a175065bdbc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## [3.5.1] - 2026-08-17
|
|
2
|
+
|
|
3
|
+
- [Change] A line item is how many of what, and nothing else. `description` was answered and
|
|
4
|
+
read out in `to_s` alongside the quantity and the name, and no caller ever wanted it — so it
|
|
5
|
+
is not asked of Jobber any more, which is a smaller query as well as a smaller class.
|
|
6
|
+
`quantified` goes with it: it existed only to be the half of `to_s` without a description,
|
|
7
|
+
and `to_s` is that on its own now. A job's summary is `line_items.to_sentence`, since
|
|
8
|
+
`to_sentence` reads each line's own string form
|
|
9
|
+
|
|
10
|
+
## [3.5.0] - 2026-08-17
|
|
11
|
+
|
|
12
|
+
- [Fix] A walk of jobs carrying their line items was refused outright: `Throttled`. Jobber
|
|
13
|
+
prices a query by the page it asks for and prices an *unbounded* connection at its own
|
|
14
|
+
maximum, so `lineItems` on a page of 40 jobs was charged as though every job carried the
|
|
15
|
+
largest job's worth of lines. The connection is bounded at 20 now, and a page of jobs is 20
|
|
16
|
+
rather than 40 — half the page is half the query, and a walk loses nothing by reading twice
|
|
17
|
+
as many. A job with more than 20 lines is summarized by its first 20
|
|
18
|
+
- [Fix] Jobber's own failures reach a caller as `Jbr::Error`, which is what this gem has
|
|
19
|
+
always said they would. `GraphQL::Error` was escaping instead, so an app that rescued
|
|
20
|
+
`Jbr::Error` — as the README tells it to — was not catching a throttle, a 500 or an
|
|
21
|
+
unreadable answer, and had its own job blow up rather than hearing that Jobber said no
|
|
22
|
+
- [Fix] A refusal for cost says what the cost was: `Throttled (cost 12400, 9500 of 10000
|
|
23
|
+
available, restoring 500/s)` rather than `Throttled`. Without the numbers there is no
|
|
24
|
+
telling a query too big to ever run from a bucket that needed another second
|
|
25
|
+
- [Change] A refused query prices the next one. Jobber reports the bucket when it says no as
|
|
26
|
+
readily as when it answers, and the throttle was only reading it on the way through — so a
|
|
27
|
+
walk that hit the ceiling then asked again immediately, at the same size, and was refused
|
|
28
|
+
again
|
|
29
|
+
|
|
1
30
|
## [3.4.0] - 2026-08-17
|
|
2
31
|
|
|
3
32
|
- [New] `line_items` on a job: what the work actually was, where the title is only what
|
data/README.md
CHANGED
|
@@ -119,16 +119,15 @@ job.line_items # => an Array of the lines the job is made of
|
|
|
119
119
|
line = job.line_items.first
|
|
120
120
|
line.quantity # => 3, whole where Jobber's own Float has nothing after the point, and 3.5
|
|
121
121
|
# where it has: `3 Faucets`, or `3.5 Hours` for what was really billed
|
|
122
|
-
line.quantified # => '3 Bathroom Faucet Installation', how many of what
|
|
123
122
|
line.name # => 'Bathroom Faucet Installation'
|
|
124
|
-
line.
|
|
125
|
-
|
|
126
|
-
# faucet)', and without the parenthesis where nobody described it
|
|
123
|
+
line.to_s # => '3 Bathroom Faucet Installation', how many of what, and the name alone where
|
|
124
|
+
# Jobber holds no quantity for the line
|
|
127
125
|
```
|
|
128
126
|
|
|
129
|
-
Every line Jobber holds is in the list, in the order it holds them and
|
|
130
|
-
quantified at. One it holds no quantity for reads as its name alone. Ask for
|
|
131
|
-
nothing arrives, so `oauth.jobs.first.line_items` is empty where the query never
|
|
127
|
+
Every line Jobber holds is in the list, up to twenty of them, in the order it holds them and
|
|
128
|
+
whatever each is quantified at. One it holds no quantity for reads as its name alone. Ask for
|
|
129
|
+
nothing and nothing arrives, so `oauth.jobs.first.line_items` is empty where the query never
|
|
130
|
+
named them.
|
|
132
131
|
|
|
133
132
|
### Invoices
|
|
134
133
|
|
|
@@ -205,6 +204,12 @@ done about either — every request waits for itself:
|
|
|
205
204
|
A request that follows no other waits for nothing, so a single `find` is as quick as it ever
|
|
206
205
|
was. Only a walk long enough to be a problem is slowed, and only by as much as it must be.
|
|
207
206
|
|
|
207
|
+
Where Jobber refuses for cost anyway, the `Jbr::Error` raised says what the query would have
|
|
208
|
+
cost against what was available — `Throttled (cost 12400, 9500 of 10000 available, restoring
|
|
209
|
+
500/s)` — so a query too big to ever run reads apart from a bucket that needed a moment. Every
|
|
210
|
+
connection this gem asks for is bounded, because Jobber prices an unbounded one at its own
|
|
211
|
+
maximum: twenty lines to a job, and twenty jobs or visits to a page.
|
|
212
|
+
|
|
208
213
|
### Events
|
|
209
214
|
|
|
210
215
|
Parse the payload of a Jobber event webhook:
|
data/lib/graphql/client.rb
CHANGED
|
@@ -23,13 +23,27 @@ module GraphQL
|
|
|
23
23
|
raise Unauthorized, response.body if response.code == '401'
|
|
24
24
|
raise Error, response.body unless response.is_a? Net::HTTPSuccess
|
|
25
25
|
body = JSON.parse(response.body)
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# Before the refusal, not after it: an endpoint that reports what it will still answer
|
|
27
|
+
# reports it when it says no, which is when a caller most needs to know.
|
|
28
28
|
yield body['extensions'] if block_given?
|
|
29
|
+
raise Error, refusal(body) if body['errors'].present?
|
|
29
30
|
body.fetch('data')
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
private
|
|
34
|
+
|
|
35
|
+
# What the endpoint refused, and — where it priced the refusal — what the query would have
|
|
36
|
+
# cost against what was available. `Throttled` on its own leaves a caller unable to tell a
|
|
37
|
+
# query too big to ever run from a bucket that only needed a moment.
|
|
38
|
+
def refusal(body)
|
|
39
|
+
message = body['errors'].map { |error| error['message'] }.join '; '
|
|
40
|
+
cost = body['extensions'].to_h['cost'].to_h
|
|
41
|
+
status = cost['throttleStatus'].to_h
|
|
42
|
+
return message if status.empty?
|
|
43
|
+
|
|
44
|
+
"#{message} (cost #{cost['requestedQueryCost']}, #{status['currentlyAvailable']} of " \
|
|
45
|
+
"#{status['maximumAvailable']} available, restoring #{status['restoreRate']}/s)"
|
|
46
|
+
end
|
|
33
47
|
def request_headers
|
|
34
48
|
{ 'Authorization' => "Bearer #{@token}", 'Content-Type' => 'application/json' }.merge @headers
|
|
35
49
|
end
|
data/lib/jbr/job.rb
CHANGED
|
@@ -10,11 +10,11 @@ module Jbr
|
|
|
10
10
|
def instructions = @node['instructions']
|
|
11
11
|
|
|
12
12
|
# What the job's lines add up to, each as how many of what: `3 Faucet install and 2 Valve
|
|
13
|
-
# change
|
|
14
|
-
#
|
|
15
|
-
# where the query never asked for them.
|
|
16
|
-
# @return [String] the lines as a sentence, or the title, or the ID.
|
|
17
|
-
def summary = line_items.
|
|
13
|
+
# change` — `to_sentence` reading each line's own string form. The lines say what the work
|
|
14
|
+
# was where a title only says what it was called, so this reads better than one, and falls
|
|
15
|
+
# back to {#name} where the job has no lines or the query never asked for them.
|
|
16
|
+
# @return [String] the lines as a sentence, or the title, or the ID. Never nil, never empty.
|
|
17
|
+
def summary = line_items.to_sentence.presence || name
|
|
18
18
|
|
|
19
19
|
# @return [String, nil] where Jobber files the job in its own workflow.
|
|
20
20
|
def status = @node['jobStatus']
|
data/lib/jbr/jobs.rb
CHANGED
|
@@ -29,12 +29,14 @@ module Jbr
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
32
|
+
# Twenty a page, not forty and not a hundred: Jobber prices a query by its page size, and
|
|
33
|
+
# what an includes brings back is charged for on top of every row of it — so a page of jobs
|
|
34
|
+
# carrying their lines, their property and its client priced past what a bucket holds. Half
|
|
35
|
+
# the page costs half the query and loses nothing, since a walk simply reads more pages.
|
|
34
36
|
def page
|
|
35
37
|
<<~GRAPHQL
|
|
36
38
|
query($after: String, $filter: JobFilterAttributes) {
|
|
37
|
-
jobs(first:
|
|
39
|
+
jobs(first: 20, after: $after, filter: $filter) {
|
|
38
40
|
nodes { #{FIELDS} #{selections} }
|
|
39
41
|
pageInfo { hasNextPage endCursor }
|
|
40
42
|
}
|
data/lib/jbr/line_item.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
module Jbr
|
|
2
|
-
# One line of the work a job is made of: how many of a thing, what it is called
|
|
3
|
-
# doing it involves.
|
|
2
|
+
# One line of the work a job is made of: how many of a thing, and what it is called.
|
|
4
3
|
class LineItem < Resource
|
|
5
4
|
# What Jobber calls each field of a line.
|
|
6
|
-
FIELDS = %w[quantity name
|
|
5
|
+
FIELDS = %w[quantity name]
|
|
6
|
+
|
|
7
|
+
# The most lines to read off one record. Bounded because Jobber prices a connection by the
|
|
8
|
+
# page it is asked for and prices an unbounded one at its own maximum, so the lines of a
|
|
9
|
+
# page of jobs were charged for as though every job had the largest job's worth of them.
|
|
10
|
+
PAGE = 20
|
|
7
11
|
|
|
8
12
|
# What to ask for wherever a record lists the lines it is made of.
|
|
9
|
-
SELECTION = "lineItems { nodes { #{FIELDS.join ' '} } }"
|
|
13
|
+
SELECTION = "lineItems(first: #{PAGE}) { nodes { #{FIELDS.join ' '} } }"
|
|
10
14
|
|
|
11
15
|
# @param nodes [Array<Hash>, nil] the lines as Jobber answered them, if it answered any.
|
|
12
16
|
# @return [Array<LineItem>] one per line, in the order Jobber holds them.
|
|
@@ -18,16 +22,9 @@ module Jbr
|
|
|
18
22
|
# @return [String, nil] what the line is called.
|
|
19
23
|
def name = @node['name']
|
|
20
24
|
|
|
21
|
-
# @return [String, nil] what doing it involves, in whoever wrote the line's own words.
|
|
22
|
-
def description = @node['description']
|
|
23
|
-
|
|
24
25
|
# @return [String] how many of what: `3 Bathroom Faucet Installation`, and the name alone
|
|
25
26
|
# where Jobber holds no quantity for the line.
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
# @return [String] how a line reads: `3 Faucet install (Fits a new faucet)`, and without
|
|
29
|
-
# the parenthesis where nobody described it.
|
|
30
|
-
def to_s = [ quantified, described ].compact.join ' '
|
|
27
|
+
def to_s = [ quantity, name ].compact.join ' '
|
|
31
28
|
|
|
32
29
|
private
|
|
33
30
|
|
|
@@ -35,7 +32,5 @@ module Jbr
|
|
|
35
32
|
# `3 Faucets` rather than `3.0 Faucets`. A fraction keeps its point — `3.5 Faucets` —
|
|
36
33
|
# since rounding it would lie about what was billed.
|
|
37
34
|
def whole(number) = number && ((number % 1).zero? ? number.to_i : number)
|
|
38
|
-
|
|
39
|
-
def described = ("(#{description})" if description.present?)
|
|
40
35
|
end
|
|
41
36
|
end
|
data/lib/jbr/mock/line_item.rb
CHANGED
data/lib/jbr/oauth.rb
CHANGED
|
@@ -41,6 +41,11 @@ module Jbr
|
|
|
41
41
|
client.query(statement, variables: variables) { |extensions| throttle.read extensions }
|
|
42
42
|
rescue GraphQL::Unauthorized
|
|
43
43
|
refresh ? retry : {}
|
|
44
|
+
rescue GraphQL::Error => error
|
|
45
|
+
# The transport's own class never leaves the gem: a caller told to rescue `Jbr::Error`
|
|
46
|
+
# was not catching a throttle, a 500 or an unreadable answer, and had its own job blow
|
|
47
|
+
# up instead of hearing that Jobber would not answer.
|
|
48
|
+
raise Error, error.message
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
# Delete a token. If the token is invalid, do nothing.
|
data/lib/jbr/throttle.rb
CHANGED
|
@@ -21,7 +21,9 @@ module Jbr
|
|
|
21
21
|
def read(extensions)
|
|
22
22
|
cost = extensions.to_h['cost'].to_h
|
|
23
23
|
status = cost['throttleStatus'].to_h
|
|
24
|
-
|
|
24
|
+
# What a refused query would have cost is what it costs: Jobber prices it either way,
|
|
25
|
+
# and only an answered one reports an actual.
|
|
26
|
+
@cost = (cost['actualQueryCost'] || cost['requestedQueryCost']).to_f
|
|
25
27
|
@available = status['currentlyAvailable'].to_f
|
|
26
28
|
@restore_rate = status['restoreRate'].to_f
|
|
27
29
|
end
|
data/lib/jbr/version.rb
CHANGED
data/lib/jbr/visits.rb
CHANGED
|
@@ -18,12 +18,12 @@ module Jbr
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
21
|
+
# Twenty a page, the same as jobs: Jobber prices a query by its page size, and what an
|
|
22
|
+
# includes brings back is charged for on top of every row of it.
|
|
23
23
|
def page
|
|
24
24
|
<<~GRAPHQL
|
|
25
25
|
query($after: String, $filter: VisitFilterAttributes) {
|
|
26
|
-
visits(first:
|
|
26
|
+
visits(first: 20, after: $after, filter: $filter) {
|
|
27
27
|
nodes { #{FIELDS} #{selections} }
|
|
28
28
|
pageInfo { hasNextPage endCursor }
|
|
29
29
|
}
|