jbr 3.8.0 → 3.9.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 +4 -4
- data/README.md +17 -6
- data/lib/jbr/jobs.rb +4 -25
- data/lib/jbr/listable.rb +73 -0
- data/lib/jbr/mock/jobs.rb +12 -11
- data/lib/jbr/mock/visits.rb +13 -12
- data/lib/jbr/resource.rb +4 -24
- data/lib/jbr/version.rb +1 -1
- data/lib/jbr/visits.rb +5 -24
- data/lib/jbr.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 815b57ea81f9bda41efb8b7d09a0767224763b895126a3ee56acabab6266b3fc
|
|
4
|
+
data.tar.gz: 127a174761ddb6bc0cc00cb34ffd9e49044152d575f3f5e4f42a030f222023a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3be4d269a045c6d9e0629a27922697f93c102259eee21762fcef02b0e5257f1b4ebd05e56812faa47e41b68a94ea440624c02621eaa95a4449db42dda94d62f4
|
|
7
|
+
data.tar.gz: d34a6393d787cb20628cf4a18e0c63ffe63bdecaa3cf00e7e84016f8423fca125567549cda5ced525770b07d6354103be476c00d54b21b936e6769a728efe8b8
|
data/README.md
CHANGED
|
@@ -113,13 +113,14 @@ job.completed_at # => 2026-05-18 11:36:13
|
|
|
113
113
|
|
|
114
114
|
Or walk the account's jobs, oldest first. Jobber is asked for a page at a time, and only
|
|
115
115
|
once the page before it runs out, so `first` costs one request where `to_a` costs as many
|
|
116
|
-
as the account has pages. A walk
|
|
117
|
-
[Rate limits](#rate-limits).
|
|
116
|
+
as the account has pages. A walk is priced by what its pages carry, and a long one can be
|
|
117
|
+
refused for it: see [Rate limits](#rate-limits).
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
jobs = oauth.jobs # => an Enumerable of every job, nothing fetched yet
|
|
121
|
-
oauth.jobs.past # =>
|
|
122
|
-
oauth.jobs.upcoming # =>
|
|
121
|
+
oauth.jobs.past # => the ones dated before now, nothing fetched yet
|
|
122
|
+
oauth.jobs.upcoming # => the ones dated from now on, nothing fetched yet
|
|
123
|
+
oauth.jobs.past.ids # => %w[Z2lkOi8vS ...], every page of them, and nothing else about them
|
|
123
124
|
|
|
124
125
|
job = jobs.first
|
|
125
126
|
job.name # => 'Furnace tune-up', or the job's ID where nobody titled it. Never nil or empty
|
|
@@ -175,8 +176,9 @@ Walk the account's visits, oldest first, the same way as its jobs:
|
|
|
175
176
|
|
|
176
177
|
```ruby
|
|
177
178
|
visits = oauth.visits # => an Enumerable of every visit, nothing fetched yet
|
|
178
|
-
oauth.visits.upcoming # =>
|
|
179
|
-
oauth.visits.past # =>
|
|
179
|
+
oauth.visits.upcoming # => the ones dated from now on, nothing fetched yet
|
|
180
|
+
oauth.visits.past # => the ones dated before now, nothing fetched yet
|
|
181
|
+
oauth.visits.upcoming.ids # => %w[Z2lkOi8vS ...], every page of them, and nothing else
|
|
180
182
|
|
|
181
183
|
visit = visits.first
|
|
182
184
|
visit.id # => 'Z2lkOi8vS'
|
|
@@ -247,6 +249,15 @@ holding a transaction open.
|
|
|
247
249
|
Every connection the gem asks for is bounded, to keep a query on the affordable side of that:
|
|
248
250
|
twenty lines to a job, and twenty jobs or visits to a page.
|
|
249
251
|
|
|
252
|
+
`ids` is the cheap way to walk an account. It asks for the ID and nothing else, which prices
|
|
253
|
+
a row at a fraction of a record and buys a hundred of them to a page — so five times the
|
|
254
|
+
account is read for a fraction of the budget. Reach for it where each record is then read on
|
|
255
|
+
its own with `find`, one background job at a time:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
oauth.jobs.past.ids.each { |id| ImportJob.perform_later id }
|
|
259
|
+
```
|
|
260
|
+
|
|
250
261
|
### Events
|
|
251
262
|
|
|
252
263
|
Parse the payload of a Jobber event webhook:
|
data/lib/jbr/jobs.rb
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
module Jbr
|
|
2
2
|
# The jobs on a Jobber account, oldest first, walked a page at a time.
|
|
3
3
|
class Jobs < Resource
|
|
4
|
-
include
|
|
4
|
+
include Includable, Listable
|
|
5
5
|
|
|
6
6
|
# What a job answers with wherever one is read, before anything it was asked to bring
|
|
7
7
|
# back with it.
|
|
8
8
|
FIELDS = 'id title instructions jobStatus total createdAt startAt completedAt ' \
|
|
9
9
|
'quote { id amounts { total } }'
|
|
10
10
|
|
|
11
|
-
# Every job on the account, past and future alike. Nothing is read until the walk starts,
|
|
12
|
-
# and a page is read only once the one before it runs out.
|
|
13
|
-
def each(&) = walk.each(&)
|
|
14
|
-
|
|
15
|
-
# @return [Enumerator<Job>] the jobs scheduled from now on.
|
|
16
|
-
def upcoming = walk from_now
|
|
17
|
-
|
|
18
|
-
# @return [Enumerator<Job>] the jobs that started before now.
|
|
19
|
-
def past = walk until_now
|
|
20
|
-
|
|
21
11
|
# Shadows Enumerable#find on purpose, the way Active Record does: a job is reached by the
|
|
22
12
|
# ID Jobber files it under, not by asking every job on the account whether it is the one.
|
|
23
13
|
# @param id [String] the Jobber ID of the job.
|
|
@@ -29,20 +19,7 @@ module Jbr
|
|
|
29
19
|
|
|
30
20
|
private
|
|
31
21
|
|
|
32
|
-
|
|
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.
|
|
36
|
-
def page
|
|
37
|
-
<<~GRAPHQL
|
|
38
|
-
query($after: String, $filter: JobFilterAttributes) {
|
|
39
|
-
jobs(first: 20, after: $after, filter: $filter) {
|
|
40
|
-
nodes { #{FIELDS} #{selections} }
|
|
41
|
-
pageInfo { hasNextPage endCursor }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
GRAPHQL
|
|
45
|
-
end
|
|
22
|
+
def page = paged "#{FIELDS} #{selections}", PAGE
|
|
46
23
|
|
|
47
24
|
def one
|
|
48
25
|
<<~GRAPHQL
|
|
@@ -54,6 +31,8 @@ module Jbr
|
|
|
54
31
|
|
|
55
32
|
def field = 'jobs'
|
|
56
33
|
|
|
34
|
+
def filtered = 'JobFilterAttributes'
|
|
35
|
+
|
|
57
36
|
def item(node) = Job.new node: node
|
|
58
37
|
end
|
|
59
38
|
end
|
data/lib/jbr/listable.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Jbr
|
|
2
|
+
# What a list of records answers, and how much of an account it costs to ask. Jobber prices
|
|
3
|
+
# a query by the page it asks for and by what every row of that page carries, so a list is
|
|
4
|
+
# read either as records or as the IDs alone, each with a page sized to what it carries.
|
|
5
|
+
module Listable
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# Records a page, not forty and not a hundred: what an includes brings back is charged for
|
|
9
|
+
# on top of every row of it, so a page of jobs carrying their lines, their property and its
|
|
10
|
+
# client priced past what a bucket holds. Half the page costs half the query and loses
|
|
11
|
+
# nothing, since a walk simply reads more pages.
|
|
12
|
+
PAGE = 20
|
|
13
|
+
|
|
14
|
+
# IDs a page, which the same budget affords five times over where a row carries one field
|
|
15
|
+
# and no nesting at all.
|
|
16
|
+
IDS_PAGE = 100
|
|
17
|
+
|
|
18
|
+
# Every record the list is narrowed to, oldest first. Nothing is read until the walk
|
|
19
|
+
# starts, and a page is read only once the one before it runs out.
|
|
20
|
+
def each(&) = walk(page).each(&)
|
|
21
|
+
|
|
22
|
+
# @return [Listable] the same list, narrowed to what is scheduled from now on.
|
|
23
|
+
def upcoming = narrowed from_now
|
|
24
|
+
|
|
25
|
+
# @return [Listable] the same list, narrowed to what started before now.
|
|
26
|
+
def past = narrowed until_now
|
|
27
|
+
|
|
28
|
+
# The ID Jobber files each record under, and nothing else about it: the cheapest question
|
|
29
|
+
# an account can be walked with, and the one to ask where every record is then read on its
|
|
30
|
+
# own through {#find}.
|
|
31
|
+
# @return [Array<String>] every ID in the list, every page of them read.
|
|
32
|
+
def ids = walk(ids_page).map(&:id)
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def narrowed(filter) = self.class.new(oauth: @oauth, includes: @includes, filter: filter)
|
|
37
|
+
|
|
38
|
+
# The two halves of a schedule, split at the moment they are asked for rather than per
|
|
39
|
+
# page: read page by page the boundary would slide, and something could cross it unseen.
|
|
40
|
+
def from_now = { startAt: { after: Time.now.iso8601 } }
|
|
41
|
+
|
|
42
|
+
def until_now = { startAt: { before: Time.now.iso8601 } }
|
|
43
|
+
|
|
44
|
+
# Every record a paged query answers, one at a time, a page read only once the one before
|
|
45
|
+
# it runs out. The filter is data: narrowed to nothing, the query narrows nothing.
|
|
46
|
+
def walk(statement)
|
|
47
|
+
Enumerator.new do |yielder|
|
|
48
|
+
after = nil
|
|
49
|
+
loop do
|
|
50
|
+
answered = @oauth.query statement, variables: { after: after, filter: @filter }.compact
|
|
51
|
+
current = answered.fetch field, {}
|
|
52
|
+
current.fetch('nodes', []).each { |node| yielder << item(node) }
|
|
53
|
+
break unless current.dig 'pageInfo', 'hasNextPage'
|
|
54
|
+
|
|
55
|
+
after = current.dig 'pageInfo', 'endCursor'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ids_page = paged 'id', IDS_PAGE
|
|
61
|
+
|
|
62
|
+
def paged(fields, size)
|
|
63
|
+
<<~GRAPHQL
|
|
64
|
+
query($after: String, $filter: #{filtered}) {
|
|
65
|
+
#{field}(first: #{size}, after: $after, filter: $filter) {
|
|
66
|
+
nodes { #{fields} }
|
|
67
|
+
pageInfo { hasNextPage endCursor }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
GRAPHQL
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/jbr/mock/jobs.rb
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
module Jbr
|
|
2
|
-
# The jobs an app under test asked {Jbr.mock} to answer with.
|
|
2
|
+
# The jobs an app under test asked {Jbr.mock} to answer with. Only the walk is mocked:
|
|
3
|
+
# narrowing a list, and reading it as records or as IDs, is the same code a real one runs.
|
|
3
4
|
class Mock::Jobs < Jobs
|
|
4
5
|
# The job filed under that ID where the app listed one, and otherwise the single job it
|
|
5
6
|
# named — which is every app that mocks a lookup without mocking a list.
|
|
6
7
|
# @return [Mock::Job] the job asked for.
|
|
7
8
|
def find(id) = Mock::Job.new node: listed(id) || Jbr.mock.job
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
def each(&) = mocked(Jbr.mock.jobs).each(&)
|
|
11
|
-
|
|
12
|
-
# @return [Enumerator<Mock::Job>] those it dated from now on, and any it left undated.
|
|
13
|
-
def upcoming = mocked Jbr.mock.jobs.reject { |job| started? job }
|
|
10
|
+
private
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
def walk(_statement)
|
|
13
|
+
Enumerator.new { |yielder| selected.each { |job| yielder << Mock::Job.new(node: job) } }
|
|
14
|
+
end
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
# The half of the schedule the list was narrowed to, from what the app dated each job.
|
|
17
|
+
# One it left undated is one nothing has started, so it counts as upcoming.
|
|
18
|
+
def selected
|
|
19
|
+
return Jbr.mock.jobs unless @filter
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
started = @filter.dig(:startAt, :before).present?
|
|
22
|
+
Jbr.mock.jobs.select { |job| started?(job) == started }
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def started?(job) = job[:scheduled_at] ? job[:scheduled_at] <= Time.now : false
|
data/lib/jbr/mock/visits.rb
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
module Jbr
|
|
2
|
-
# The visits an app under test asked {Jbr.mock} to answer with.
|
|
2
|
+
# The visits an app under test asked {Jbr.mock} to answer with. Only the walk is mocked:
|
|
3
|
+
# narrowing a list, and reading it as records or as IDs, is the same code a real one runs.
|
|
3
4
|
class Mock::Visits < Visits
|
|
4
|
-
# @return [Enumerator<Mock::Visit>] every visit the app named.
|
|
5
|
-
def each(&) = mocked(Jbr.mock.visits).each(&)
|
|
6
|
-
|
|
7
|
-
# @return [Enumerator<Mock::Visit>] those it dated from now on, and any it left undated.
|
|
8
|
-
def upcoming = mocked Jbr.mock.visits.reject { |visit| started? visit }
|
|
9
|
-
|
|
10
|
-
# @return [Enumerator<Mock::Visit>] those it dated before now.
|
|
11
|
-
def past = mocked Jbr.mock.visits.select { |visit| started? visit }
|
|
12
|
-
|
|
13
5
|
# @return [Mock::Visit] the visit the app listed under that ID.
|
|
14
6
|
def find(id) = Mock::Visit.new node: listed(id)
|
|
15
7
|
|
|
16
8
|
private
|
|
17
9
|
|
|
18
|
-
def
|
|
19
|
-
Enumerator.new { |yielder|
|
|
10
|
+
def walk(_statement)
|
|
11
|
+
Enumerator.new { |yielder| selected.each { |it| yielder << Mock::Visit.new(node: it) } }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The half of the schedule the list was narrowed to, from what the app dated each visit.
|
|
15
|
+
# One it left undated is one nothing has started, so it counts as upcoming.
|
|
16
|
+
def selected
|
|
17
|
+
return Jbr.mock.visits unless @filter
|
|
18
|
+
|
|
19
|
+
started = @filter.dig(:startAt, :before).present?
|
|
20
|
+
Jbr.mock.visits.select { |visit| started?(visit) == started }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def started?(visit) = visit[:starts_at] ? visit[:starts_at] <= Time.now : false
|
data/lib/jbr/resource.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
module Jbr
|
|
2
2
|
# What every Jobber resource shares: the node Jobber answered with, the credentials it was
|
|
3
|
-
# read through, and
|
|
3
|
+
# read through, and what a list of them was asked and narrowed to.
|
|
4
4
|
class Resource
|
|
5
5
|
# @param oauth [OAuth] the credentials to reach Jobber with, where reaching it is needed.
|
|
6
6
|
# @param node [Hash] the record as Jobber answered it.
|
|
7
7
|
# @param includes [Hash] what a list was asked to bring back beside its records.
|
|
8
|
-
|
|
8
|
+
# @param filter [Hash] what a list was narrowed to, in the shape Jobber filters by.
|
|
9
|
+
def initialize(oauth: nil, node: {}, includes: {}, filter: nil)
|
|
9
10
|
@oauth = oauth
|
|
10
11
|
@node = node
|
|
11
12
|
@id = node['id']
|
|
12
13
|
@includes = includes
|
|
14
|
+
@filter = filter
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
# @return [String, nil] the Jobber ID, from the node it came in or once one was created.
|
|
@@ -20,27 +22,5 @@ module Jbr
|
|
|
20
22
|
# @return [Time, nil] what Jobber answered under a key, as a time. An empty answer is no
|
|
21
23
|
# answer: Time.iso8601 raises on one, where nothing at all it simply has none of.
|
|
22
24
|
def time(key) = (Time.iso8601 @node[key] if @node[key].present?)
|
|
23
|
-
|
|
24
|
-
# Every item a paged query answers, one at a time, a page read only once the one before
|
|
25
|
-
# it runs out. The filter is data: handed none, the query narrows nothing.
|
|
26
|
-
def walk(filter = nil)
|
|
27
|
-
Enumerator.new do |yielder|
|
|
28
|
-
after = nil
|
|
29
|
-
loop do
|
|
30
|
-
answered = @oauth.query(page, variables: { after: after, filter: filter }.compact)
|
|
31
|
-
current = answered.fetch field, {}
|
|
32
|
-
current.fetch('nodes', []).each { |node| yielder << item(node) }
|
|
33
|
-
break unless current.dig 'pageInfo', 'hasNextPage'
|
|
34
|
-
|
|
35
|
-
after = current.dig 'pageInfo', 'endCursor'
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# The two halves of a schedule, split at the moment they are asked for rather than per
|
|
41
|
-
# page: read page by page the boundary would slide, and something could cross it unseen.
|
|
42
|
-
def from_now = { startAt: { after: Time.now.iso8601 } }
|
|
43
|
-
|
|
44
|
-
def until_now = { startAt: { before: Time.now.iso8601 } }
|
|
45
25
|
end
|
|
46
26
|
end
|
data/lib/jbr/version.rb
CHANGED
data/lib/jbr/visits.rb
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
module Jbr
|
|
2
2
|
# The visits on a Jobber account, oldest first, walked a page at a time.
|
|
3
3
|
class Visits < Resource
|
|
4
|
-
include
|
|
4
|
+
include Includable, Listable
|
|
5
5
|
|
|
6
6
|
# What a visit answers with, before anything it was asked to bring back with it.
|
|
7
7
|
FIELDS = 'id title startAt endAt allDay clientConfirmed job { id }'
|
|
8
8
|
|
|
9
|
-
# Every visit on the account, past and future alike. Nothing is read until the walk
|
|
10
|
-
# starts, and a page is read only once the one before it runs out.
|
|
11
|
-
def each(&) = walk.each(&)
|
|
12
|
-
|
|
13
|
-
# @return [Enumerator<Visit>] the visits scheduled from now on.
|
|
14
|
-
def upcoming = walk from_now
|
|
15
|
-
|
|
16
|
-
# @return [Enumerator<Visit>] the visits that started before now.
|
|
17
|
-
def past = walk until_now
|
|
18
|
-
|
|
19
9
|
# Shadows Enumerable#find on purpose, the way jobs do: a visit is reached by the ID Jobber
|
|
20
10
|
# files it under, not by asking every visit on the account whether it is the one.
|
|
21
11
|
# @param id [String] the Jobber ID of the visit.
|
|
@@ -27,6 +17,8 @@ module Jbr
|
|
|
27
17
|
|
|
28
18
|
private
|
|
29
19
|
|
|
20
|
+
def page = paged "#{FIELDS} #{selections}", PAGE
|
|
21
|
+
|
|
30
22
|
def one
|
|
31
23
|
<<~GRAPHQL
|
|
32
24
|
query($id: EncodedId!) {
|
|
@@ -35,21 +27,10 @@ module Jbr
|
|
|
35
27
|
GRAPHQL
|
|
36
28
|
end
|
|
37
29
|
|
|
38
|
-
# Twenty a page, the same as jobs: Jobber prices a query by its page size, and what an
|
|
39
|
-
# includes brings back is charged for on top of every row of it.
|
|
40
|
-
def page
|
|
41
|
-
<<~GRAPHQL
|
|
42
|
-
query($after: String, $filter: VisitFilterAttributes) {
|
|
43
|
-
visits(first: 20, after: $after, filter: $filter) {
|
|
44
|
-
nodes { #{FIELDS} #{selections} }
|
|
45
|
-
pageInfo { hasNextPage endCursor }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
GRAPHQL
|
|
49
|
-
end
|
|
50
|
-
|
|
51
30
|
def field = 'visits'
|
|
52
31
|
|
|
32
|
+
def filtered = 'VisitFilterAttributes'
|
|
33
|
+
|
|
53
34
|
def item(node) = Visit.new node: node
|
|
54
35
|
end
|
|
55
36
|
end
|
data/lib/jbr.rb
CHANGED
|
@@ -36,6 +36,7 @@ require 'jbr/account'
|
|
|
36
36
|
require 'jbr/property'
|
|
37
37
|
require 'jbr/properted'
|
|
38
38
|
require 'jbr/includable'
|
|
39
|
+
require 'jbr/listable'
|
|
39
40
|
# LineItem before Itemized, and both before Job: the lines a job is made of are asked for
|
|
40
41
|
# by a constant the include reads as it loads.
|
|
41
42
|
require 'jbr/line_item'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jbr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -120,6 +120,7 @@ files:
|
|
|
120
120
|
- lib/jbr/job.rb
|
|
121
121
|
- lib/jbr/jobs.rb
|
|
122
122
|
- lib/jbr/line_item.rb
|
|
123
|
+
- lib/jbr/listable.rb
|
|
123
124
|
- lib/jbr/mock.rb
|
|
124
125
|
- lib/jbr/mock/account.rb
|
|
125
126
|
- lib/jbr/mock/client.rb
|