printavo-ruby 0.18.1 → 0.19.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 +39 -0
- data/docs/CHANGELOG.md +54 -0
- data/lib/printavo/client.rb +6 -1
- data/lib/printavo/errors.rb +2 -0
- data/lib/printavo/graphql/contacts/all.graphql +36 -0
- data/lib/printavo/graphql_client.rb +119 -10
- data/lib/printavo/page.rb +3 -1
- data/lib/printavo/resources/contacts.rb +61 -0
- data/lib/printavo/response_envelope.rb +44 -0
- data/lib/printavo/version.rb +1 -1
- data/lib/printavo.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48b453128217df2c6051caddc404b2623acf14aa49f1c7ef9aab789359245ec4
|
|
4
|
+
data.tar.gz: e39850f5537b1a5362f01ac910e1501d3bf473470dca5cfce5465b3352fbb7ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a5effd363bbefcaff3ac665457d3b099f5f338911583e29885a4be4c2308ea912f753157a13d088b8b3e91105ea9b697fd78e41d5a02ab55fecebb4174d4f2e
|
|
7
|
+
data.tar.gz: 80760572b15654acd78928d16ef4e639961dae9ce4a95f9d812660a8612d0aeaf3205f5ac51f3bf6a4648e2bea00b37939afa077354bbf6baca540fbb4a18a86
|
data/README.md
CHANGED
|
@@ -22,6 +22,7 @@ A framework-agnostic Ruby SDK for the [Printavo](https://www.printavo.com) Graph
|
|
|
22
22
|
- Full [Printavo v2 GraphQL API](https://www.printavo.com/docs/api/v2) support
|
|
23
23
|
- Resource-oriented interface: `client.customers.all`, `client.orders.find(id)`
|
|
24
24
|
- Raw GraphQL access: `client.graphql.query("{ ... }")`
|
|
25
|
+
- Sanitized GraphQL response envelopes that preserve partial data and errors
|
|
25
26
|
- Rich domain models: `order.status`, `order.status?(:in_production)`, `order.customer`
|
|
26
27
|
- Rack-compatible webhook signature verification
|
|
27
28
|
- Multi-client support — no globals required
|
|
@@ -88,6 +89,44 @@ puts customer.full_name # => "Jane Smith"
|
|
|
88
89
|
puts customer.company # => "Acme Shirts"
|
|
89
90
|
```
|
|
90
91
|
|
|
92
|
+
### Bounded Contact Pages
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
page = client.contacts.page(
|
|
96
|
+
first: 50,
|
|
97
|
+
after: previous_cursor,
|
|
98
|
+
primary_only: true,
|
|
99
|
+
sort_on: Printavo::Enums::ContactSortField::CONTACT_NAME
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
page.records.each { |contact| puts contact.full_name }
|
|
103
|
+
next_cursor = page.end_cursor if page.has_next_page
|
|
104
|
+
|
|
105
|
+
# Partial GraphQL responses preserve successful records and sanitized errors.
|
|
106
|
+
warn(page.errors.inspect) unless page.success?
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`contacts.page` performs exactly one request and caps `first` at 100. It never
|
|
110
|
+
follows the next cursor automatically.
|
|
111
|
+
|
|
112
|
+
### GraphQL Response Envelopes
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
envelope = client.graphql.query_envelope(
|
|
116
|
+
"query Contact($id: ID!) { contact(id: $id) { id email } }",
|
|
117
|
+
variables: { id: "123" }
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
envelope.data
|
|
121
|
+
envelope.errors
|
|
122
|
+
envelope.metadata
|
|
123
|
+
envelope.partial?
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`query` and `mutate` retain their strict behavior and raise `Printavo::ApiError`
|
|
127
|
+
when GraphQL errors are present. Envelope methods preserve partial data without
|
|
128
|
+
retaining credentials, raw HTTP objects, or provider response bodies.
|
|
129
|
+
|
|
91
130
|
### Orders
|
|
92
131
|
|
|
93
132
|
```ruby
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,60 @@ All notable changes to this project will be documented in this file.
|
|
|
6
6
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
7
7
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [0.19.0] - 2026-07-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Contacts#page` for one bounded contacts request with cursor, primary-contact,
|
|
13
|
+
query, and documented sort controls
|
|
14
|
+
- `GraphqlClient#query_envelope` and `#mutate_envelope` for deeply immutable
|
|
15
|
+
partial data, sanitized errors, and allowlisted response metadata
|
|
16
|
+
- `Page#errors`, `#metadata`, `#success?`, and `#partial?`
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
- Provider transport failures now raise an identifier-only `TransportError`
|
|
20
|
+
without retaining a Faraday exception cause or response body
|
|
21
|
+
- GraphQL error details retain only allowlisted fields and redact configured
|
|
22
|
+
email and token values
|
|
23
|
+
|
|
24
|
+
## [0.18.1] - 2026-04-05
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- `examples/diagramming/workflow_diagram.rb` — generates Printavo status
|
|
28
|
+
workflows as ASCII, Mermaid, DOT, or SVG
|
|
29
|
+
- `examples/reporting/sales_report.rb` — summarizes invoice revenue for common
|
|
30
|
+
and custom reporting periods
|
|
31
|
+
- `examples/reporting/sales_tax_report.rb` — provides invoice-level sales-tax
|
|
32
|
+
estimates and fee-level taxable detail
|
|
33
|
+
- `examples/reporting/customers_export.rb` — exports customer data to CSV,
|
|
34
|
+
XLSX, XLS, vCard, HubSpot, Salesforce, Mailchimp, Constant Contact, Beehiiv,
|
|
35
|
+
Brevo, and ActiveCampaign formats
|
|
36
|
+
- `examples/reporting/outstanding_tasks.rb` — reports outstanding tasks by due
|
|
37
|
+
period and assignee and generates an HTML calendar
|
|
38
|
+
- `docs/VISUALIZATION.md` — documents the standalone workflow-diagram example,
|
|
39
|
+
supported formats, dependencies, and usage
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- GitHub Actions now use Node.js 24-compatible `actions/checkout@v4.3.1` and
|
|
43
|
+
`actions/upload-artifact@v4.6.2`
|
|
44
|
+
|
|
45
|
+
## [0.18.0] - 2026-04-05
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- Optional `cache:` and `default_ttl:` arguments on `Printavo::Client`; the
|
|
49
|
+
cache accepts any store implementing the `Rails.cache`-compatible
|
|
50
|
+
`fetch(key, expires_in:) { }` and `delete(key)` interface
|
|
51
|
+
- `Printavo::MemoryStore` — a thread-safe, TTL-aware in-memory cache for
|
|
52
|
+
applications that do not use Rails or an external cache
|
|
53
|
+
- Automatic GraphQL query caching with stable SHA-256 keys derived from the
|
|
54
|
+
normalized query document and variables
|
|
55
|
+
- Cache adapter and memory-store coverage in the client and GraphQL client
|
|
56
|
+
specs
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- GraphQL mutations always bypass the cache
|
|
60
|
+
- `docs/CACHING.md` now documents `Rails.cache`, `Printavo::MemoryStore`,
|
|
61
|
+
custom stores, cache-key generation, and mutation behavior
|
|
62
|
+
|
|
9
63
|
## [0.17.0] - 2026-04-01
|
|
10
64
|
|
|
11
65
|
### Added
|
data/lib/printavo/client.rb
CHANGED
|
@@ -47,7 +47,12 @@ module Printavo
|
|
|
47
47
|
max_retries: max_retries,
|
|
48
48
|
retry_on_rate_limit: retry_on_rate_limit
|
|
49
49
|
).build
|
|
50
|
-
@graphql = GraphqlClient.new(
|
|
50
|
+
@graphql = GraphqlClient.new(
|
|
51
|
+
connection,
|
|
52
|
+
cache: cache,
|
|
53
|
+
default_ttl: default_ttl,
|
|
54
|
+
sensitive_values: [email, token]
|
|
55
|
+
)
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def account
|
data/lib/printavo/errors.rb
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
query Contacts(
|
|
2
|
+
$first: Int
|
|
3
|
+
$after: String
|
|
4
|
+
$primaryOnly: Boolean
|
|
5
|
+
$query: String
|
|
6
|
+
$sortOn: ContactSortField
|
|
7
|
+
$sortDescending: Boolean
|
|
8
|
+
) {
|
|
9
|
+
contacts(
|
|
10
|
+
first: $first
|
|
11
|
+
after: $after
|
|
12
|
+
primaryOnly: $primaryOnly
|
|
13
|
+
query: $query
|
|
14
|
+
sortOn: $sortOn
|
|
15
|
+
sortDescending: $sortDescending
|
|
16
|
+
) {
|
|
17
|
+
nodes {
|
|
18
|
+
id
|
|
19
|
+
firstName
|
|
20
|
+
lastName
|
|
21
|
+
fullName
|
|
22
|
+
email
|
|
23
|
+
phone
|
|
24
|
+
fax
|
|
25
|
+
createdAt
|
|
26
|
+
updatedAt
|
|
27
|
+
customer {
|
|
28
|
+
id
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
pageInfo {
|
|
32
|
+
hasNextPage
|
|
33
|
+
endCursor
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -6,15 +6,29 @@ require 'json'
|
|
|
6
6
|
|
|
7
7
|
module Printavo
|
|
8
8
|
class GraphqlClient
|
|
9
|
+
SAFE_ERROR_EXTENSION_KEYS = %w[classification code].freeze
|
|
10
|
+
SAFE_METADATA_HEADERS = %w[
|
|
11
|
+
retry-after
|
|
12
|
+
x-request-id
|
|
13
|
+
x-ratelimit-limit
|
|
14
|
+
x-ratelimit-remaining
|
|
15
|
+
x-ratelimit-reset
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
9
18
|
# @param connection [Faraday::Connection]
|
|
10
19
|
# @param cache [#fetch, #delete, nil] any cache store implementing
|
|
11
20
|
# +fetch(key, expires_in:) { }+ and +delete(key)+,
|
|
12
21
|
# e.g. +Rails.cache+, +Printavo::MemoryStore.new+, or +nil+
|
|
13
22
|
# @param default_ttl [Integer] default TTL in seconds applied to cached queries (default: 300)
|
|
14
|
-
|
|
23
|
+
# @param sensitive_values [Array<String>] credentials to redact from provider messages
|
|
24
|
+
def initialize(connection, cache: nil, default_ttl: 300, sensitive_values: [])
|
|
15
25
|
@connection = connection
|
|
16
26
|
@cache = cache
|
|
17
27
|
@default_ttl = default_ttl
|
|
28
|
+
@sensitive_values = sensitive_values.filter_map do |value|
|
|
29
|
+
string = value.to_s
|
|
30
|
+
string unless string.empty?
|
|
31
|
+
end.freeze
|
|
18
32
|
end
|
|
19
33
|
|
|
20
34
|
# Executes a GraphQL query and returns the parsed `data` hash.
|
|
@@ -61,6 +75,23 @@ module Printavo
|
|
|
61
75
|
execute(mutation_string, variables: variables)
|
|
62
76
|
end
|
|
63
77
|
|
|
78
|
+
# Executes a GraphQL query without discarding partial data when GraphQL
|
|
79
|
+
# errors are present. HTTP and transport failures still raise their
|
|
80
|
+
# identifier-only SDK exceptions.
|
|
81
|
+
#
|
|
82
|
+
# @return [Printavo::ResponseEnvelope]
|
|
83
|
+
def query_envelope(query_string, variables: {})
|
|
84
|
+
execute_envelope(query_string, variables: variables)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Mutation equivalent of #query_envelope. Callers must decide whether a
|
|
88
|
+
# partial mutation response represents an uncertain write.
|
|
89
|
+
#
|
|
90
|
+
# @return [Printavo::ResponseEnvelope]
|
|
91
|
+
def mutate_envelope(mutation_string, variables: {})
|
|
92
|
+
execute_envelope(mutation_string, variables: variables)
|
|
93
|
+
end
|
|
94
|
+
|
|
64
95
|
# Iterates all pages of a paginated GraphQL query, yielding each page's
|
|
65
96
|
# nodes array. The query must accept `$first: Int` and `$after: String`
|
|
66
97
|
# variables, and the target connection must expose `nodes` and `pageInfo`.
|
|
@@ -106,28 +137,106 @@ module Printavo
|
|
|
106
137
|
end
|
|
107
138
|
|
|
108
139
|
def execute(document, variables: {})
|
|
140
|
+
envelope = execute_envelope(document, variables: variables)
|
|
141
|
+
return envelope.data if envelope.success?
|
|
142
|
+
|
|
143
|
+
messages = envelope.errors.map { |error| error.fetch('message') }.join(', ')
|
|
144
|
+
raise ApiError.new(messages, response: { 'errors' => envelope.errors }.freeze)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def execute_envelope(document, variables: {})
|
|
109
148
|
response = @connection.post('') do |req|
|
|
110
149
|
req.body = JSON.generate(query: document, variables: variables)
|
|
111
150
|
end
|
|
112
|
-
|
|
151
|
+
handle_envelope(response)
|
|
152
|
+
rescue Faraday::Error
|
|
153
|
+
raise TransportError, 'Printavo transport request failed', cause: nil
|
|
113
154
|
end
|
|
114
155
|
|
|
115
|
-
def
|
|
116
|
-
body = response.body
|
|
117
|
-
|
|
156
|
+
def handle_envelope(response)
|
|
118
157
|
case response.status
|
|
119
158
|
when 401 then raise AuthenticationError, 'Invalid credentials — check your email and token'
|
|
120
159
|
when 429 then raise RateLimitError, 'Printavo rate limit exceeded (10 req/5 sec)'
|
|
121
160
|
when 404 then raise NotFoundError, 'Resource not found'
|
|
122
161
|
end
|
|
123
162
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
body = response.body
|
|
164
|
+
return malformed_envelope(response) unless valid_envelope?(body)
|
|
165
|
+
|
|
166
|
+
ResponseEnvelope.new(
|
|
167
|
+
data: body['data'],
|
|
168
|
+
errors: sanitize_errors(body.fetch('errors', [])),
|
|
169
|
+
metadata: safe_metadata(response)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def valid_envelope?(body)
|
|
174
|
+
return false unless body.is_a?(Hash)
|
|
175
|
+
return false unless body.key?('data') || body.key?('errors')
|
|
176
|
+
return false unless body['data'].nil? || body['data'].is_a?(Hash)
|
|
177
|
+
|
|
178
|
+
errors = body.fetch('errors', [])
|
|
179
|
+
errors.is_a?(Array) && errors.all?(Hash)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def malformed_envelope(response)
|
|
183
|
+
ResponseEnvelope.new(
|
|
184
|
+
data: nil,
|
|
185
|
+
errors: [
|
|
186
|
+
{
|
|
187
|
+
'message' => 'Malformed GraphQL response',
|
|
188
|
+
'extensions' => { 'code' => 'MALFORMED_RESPONSE' }
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
metadata: safe_metadata(response)
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def sanitize_errors(errors)
|
|
196
|
+
errors.map do |error|
|
|
197
|
+
{
|
|
198
|
+
'message' => redact(error.fetch('message', 'GraphQL request failed').to_s)[0, 500],
|
|
199
|
+
'path' => sanitize_path(error['path']),
|
|
200
|
+
'locations' => sanitize_locations(error['locations']),
|
|
201
|
+
'extensions' => sanitize_extensions(error['extensions'])
|
|
202
|
+
}.compact
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def sanitize_path(path)
|
|
207
|
+
return unless path.is_a?(Array)
|
|
208
|
+
|
|
209
|
+
path.select { |part| part.is_a?(String) || part.is_a?(Integer) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def sanitize_locations(locations)
|
|
213
|
+
return unless locations.is_a?(Array)
|
|
214
|
+
|
|
215
|
+
locations.filter_map do |location|
|
|
216
|
+
next unless location.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
line = location['line']
|
|
219
|
+
column = location['column']
|
|
220
|
+
{ 'line' => line, 'column' => column } if line.is_a?(Integer) && column.is_a?(Integer)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def sanitize_extensions(extensions)
|
|
225
|
+
return unless extensions.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
extensions.slice(*SAFE_ERROR_EXTENSION_KEYS).transform_values { |value| redact(value.to_s)[0, 100] }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def safe_metadata(response)
|
|
231
|
+
headers = response.headers.to_h.transform_keys(&:downcase)
|
|
232
|
+
SAFE_METADATA_HEADERS.each_with_object({}) do |header, metadata|
|
|
233
|
+
value = headers[header]
|
|
234
|
+
metadata[header] = redact(value.to_s)[0, 100] unless value.nil?
|
|
128
235
|
end
|
|
236
|
+
end
|
|
129
237
|
|
|
130
|
-
|
|
238
|
+
def redact(value)
|
|
239
|
+
@sensitive_values.reduce(value.dup) { |result, secret| result.gsub(secret, '[FILTERED]') }
|
|
131
240
|
end
|
|
132
241
|
|
|
133
242
|
# Resolves a dot-separated path against a nested hash.
|
data/lib/printavo/page.rb
CHANGED
|
@@ -14,9 +14,11 @@ module Printavo
|
|
|
14
14
|
# client.customers.each_page(first: 10) do |records|
|
|
15
15
|
# records.each { |c| puts c.full_name }
|
|
16
16
|
# end
|
|
17
|
-
Page = Struct.new(:records, :has_next_page, :end_cursor, keyword_init: true) do
|
|
17
|
+
Page = Struct.new(:records, :has_next_page, :end_cursor, :errors, :metadata, keyword_init: true) do
|
|
18
18
|
def to_a = records
|
|
19
19
|
def size = records.size
|
|
20
20
|
def empty? = records.empty?
|
|
21
|
+
def success? = Array(errors).empty?
|
|
22
|
+
def partial? = records.any? && !success?
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -4,11 +4,32 @@
|
|
|
4
4
|
module Printavo
|
|
5
5
|
module Resources
|
|
6
6
|
class Contacts < Base
|
|
7
|
+
ALL_QUERY = File.read(File.join(__dir__, '../graphql/contacts/all.graphql')).freeze
|
|
7
8
|
FIND_QUERY = File.read(File.join(__dir__, '../graphql/contacts/find.graphql')).freeze
|
|
8
9
|
CREATE_MUTATION = File.read(File.join(__dir__, '../graphql/contacts/create.graphql')).freeze
|
|
9
10
|
DELETE_MUTATION = File.read(File.join(__dir__, '../graphql/contacts/delete.graphql')).freeze
|
|
10
11
|
UPDATE_MUTATION = File.read(File.join(__dir__, '../graphql/contacts/update.graphql')).freeze
|
|
11
12
|
|
|
13
|
+
MAX_PAGE_SIZE = 100
|
|
14
|
+
PAGE_FILTERS = %i[primary_only query sort_descending sort_on].freeze
|
|
15
|
+
|
|
16
|
+
def all(first: 25, after: nil, **filters)
|
|
17
|
+
page(first: first, after: after, **filters).records
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetches one bounded contact page while preserving sanitized partial
|
|
21
|
+
# GraphQL errors. This method never follows the next cursor.
|
|
22
|
+
#
|
|
23
|
+
# @return [Printavo::Page]
|
|
24
|
+
def page(first: 25, after: nil, **filters)
|
|
25
|
+
validate_page!(first: first, filters: filters)
|
|
26
|
+
envelope = @graphql.query_envelope(
|
|
27
|
+
ALL_QUERY,
|
|
28
|
+
variables: contact_variables(first: first, after: after, filters: filters)
|
|
29
|
+
)
|
|
30
|
+
build_page(envelope)
|
|
31
|
+
end
|
|
32
|
+
|
|
12
33
|
# Finds a contact by ID.
|
|
13
34
|
#
|
|
14
35
|
# @param id [String, Integer]
|
|
@@ -57,6 +78,46 @@ module Printavo
|
|
|
57
78
|
@graphql.mutate(DELETE_MUTATION, variables: { id: id.to_s })
|
|
58
79
|
nil
|
|
59
80
|
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def validate_page!(first:, filters:)
|
|
85
|
+
unless first.is_a?(Integer) && first.between?(1, MAX_PAGE_SIZE)
|
|
86
|
+
raise ArgumentError, "first must be between 1 and #{MAX_PAGE_SIZE}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unknown_filters = filters.keys - PAGE_FILTERS
|
|
90
|
+
raise ArgumentError, "unknown contact filters: #{unknown_filters.join(', ')}" if unknown_filters.any?
|
|
91
|
+
|
|
92
|
+
sort_on = filters[:sort_on]
|
|
93
|
+
return if sort_on.nil? || Enums::ContactSortField::ALL.include?(sort_on)
|
|
94
|
+
|
|
95
|
+
raise ArgumentError, 'sort_on must be a Printavo::Enums::ContactSortField value'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def contact_variables(first:, after:, filters:)
|
|
99
|
+
{
|
|
100
|
+
first: first,
|
|
101
|
+
after: after,
|
|
102
|
+
primaryOnly: filters[:primary_only],
|
|
103
|
+
query: filters[:query],
|
|
104
|
+
sortOn: filters[:sort_on],
|
|
105
|
+
sortDescending: filters[:sort_descending]
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_page(envelope)
|
|
110
|
+
connection = envelope.data&.fetch('contacts', nil) || {}
|
|
111
|
+
records = Array(connection['nodes']).map { |attrs| Printavo::Contact.new(attrs) }
|
|
112
|
+
page_info = connection.fetch('pageInfo', {})
|
|
113
|
+
Printavo::Page.new(
|
|
114
|
+
records: records,
|
|
115
|
+
has_next_page: page_info.fetch('hasNextPage', false),
|
|
116
|
+
end_cursor: page_info['endCursor'],
|
|
117
|
+
errors: envelope.errors,
|
|
118
|
+
metadata: envelope.metadata
|
|
119
|
+
)
|
|
120
|
+
end
|
|
60
121
|
end
|
|
61
122
|
end
|
|
62
123
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# lib/printavo/response_envelope.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Printavo
|
|
5
|
+
class ResponseEnvelope
|
|
6
|
+
attr_reader :data, :errors, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(data:, errors: [], metadata: {})
|
|
9
|
+
@data = immutable_copy(data)
|
|
10
|
+
@errors = immutable_copy(errors)
|
|
11
|
+
@metadata = immutable_copy(metadata)
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def success?
|
|
16
|
+
errors.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def partial?
|
|
20
|
+
!data.nil? && errors.any?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{ 'data' => data, 'errors' => errors, 'metadata' => metadata }.freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def immutable_copy(value)
|
|
30
|
+
case value
|
|
31
|
+
when Hash
|
|
32
|
+
value.each_with_object({}) do |(key, item), copy|
|
|
33
|
+
copy[key.to_s.freeze] = immutable_copy(item)
|
|
34
|
+
end.freeze
|
|
35
|
+
when Array
|
|
36
|
+
value.map { |item| immutable_copy(item) }.freeze
|
|
37
|
+
when String
|
|
38
|
+
value.dup.freeze
|
|
39
|
+
else
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/printavo/version.rb
CHANGED
data/lib/printavo.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative 'printavo/config'
|
|
|
10
10
|
require_relative 'printavo/connection'
|
|
11
11
|
require_relative 'printavo/enums'
|
|
12
12
|
require_relative 'printavo/errors'
|
|
13
|
+
require_relative 'printavo/response_envelope'
|
|
13
14
|
require_relative 'printavo/graphql_client'
|
|
14
15
|
require_relative 'printavo/memory_store'
|
|
15
16
|
require_relative 'printavo/page'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: printavo-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stan Carver II
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -320,6 +320,7 @@ files:
|
|
|
320
320
|
- lib/printavo/graphql/approval_requests/unapprove.graphql
|
|
321
321
|
- lib/printavo/graphql/categories/all.graphql
|
|
322
322
|
- lib/printavo/graphql/categories/find.graphql
|
|
323
|
+
- lib/printavo/graphql/contacts/all.graphql
|
|
323
324
|
- lib/printavo/graphql/contacts/create.graphql
|
|
324
325
|
- lib/printavo/graphql/contacts/delete.graphql
|
|
325
326
|
- lib/printavo/graphql/contacts/find.graphql
|
|
@@ -544,6 +545,7 @@ files:
|
|
|
544
545
|
- lib/printavo/resources/types_of_work.rb
|
|
545
546
|
- lib/printavo/resources/users.rb
|
|
546
547
|
- lib/printavo/resources/vendors.rb
|
|
548
|
+
- lib/printavo/response_envelope.rb
|
|
547
549
|
- lib/printavo/version.rb
|
|
548
550
|
- lib/printavo/webhooks.rb
|
|
549
551
|
homepage: https://github.com/scarver2/printavo-ruby
|