printavo-ruby 0.19.0 → 0.20.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 +16 -0
- data/docs/CHANGELOG.md +15 -0
- data/lib/printavo/connection.rb +1 -1
- data/lib/printavo/graphql_client.rb +44 -12
- data/lib/printavo/page.rb +22 -1
- data/lib/printavo/resources/contacts.rb +10 -1
- data/lib/printavo/response_envelope.rb +14 -2
- data/lib/printavo/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ad593920d96da45a59747ebfaab7fce5d5a1b411e0f6afa781bca1fefcca158
|
|
4
|
+
data.tar.gz: 6eb415c85c7566986565daddcd6ebd5065d87af95680599ba0b4717892de20e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d95dc0294fe872e6db56d48ab7309c668f24edc5caadf22a5f1333407cac2f31d6845fef845c0cd126ba05cc5033e0ea776ec74768da609fd6878fb1d65fe8d3
|
|
7
|
+
data.tar.gz: c6b9e54ecd0491b4ffafcf9761473a967d180e1a0bac454ea22c511f1227ba5ec6198361e28303d7db58a4b2cb6fb5f24522820b95ba6add7ada251fe240e235
|
data/README.md
CHANGED
|
@@ -109,6 +109,22 @@ warn(page.errors.inspect) unless page.success?
|
|
|
109
109
|
`contacts.page` performs exactly one request and caps `first` at 100. It never
|
|
110
110
|
follows the next cursor automatically.
|
|
111
111
|
|
|
112
|
+
`page.response_payload` contains the exact frozen binary HTTP response body
|
|
113
|
+
captured before JSON parsing. It is intentionally omitted from ordinary
|
|
114
|
+
inspection and serialization because provider responses may contain private
|
|
115
|
+
customer data. Persist or log it only through an explicit encrypted evidence
|
|
116
|
+
boundary.
|
|
117
|
+
|
|
118
|
+
Captured contact responses can be replayed through the same SDK parser without
|
|
119
|
+
provider access:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
replayed_page = client.contacts.page_from_response_payload(
|
|
123
|
+
page.response_payload,
|
|
124
|
+
metadata: page.metadata
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
112
128
|
### GraphQL Response Envelopes
|
|
113
129
|
|
|
114
130
|
```ruby
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ 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.20.0] - 2026-08-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `ResponseEnvelope#response_payload` exposes the exact immutable binary HTTP
|
|
13
|
+
response bytes captured before JSON parsing.
|
|
14
|
+
- `Page#response_payload` carries the exact payload for bounded contact pages.
|
|
15
|
+
- `GraphqlClient#envelope_from_response_payload` and
|
|
16
|
+
`Contacts#page_from_response_payload` rehydrate captured responses through
|
|
17
|
+
the same validation, sanitization, and page-building path without network I/O.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- Raw response payloads remain excluded from `ResponseEnvelope#to_h` and from
|
|
21
|
+
ordinary `ResponseEnvelope` and `Page` inspection. They may contain private
|
|
22
|
+
provider data and must be handled through an explicit evidence boundary.
|
|
23
|
+
|
|
9
24
|
## [0.19.0] - 2026-07-29
|
|
10
25
|
|
|
11
26
|
### Added
|
data/lib/printavo/connection.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Printavo
|
|
|
29
29
|
f.headers['email'] = @email
|
|
30
30
|
f.headers['token'] = @token
|
|
31
31
|
f.request :retry, **retry_options
|
|
32
|
-
f.response :json
|
|
32
|
+
f.response :json, preserve_raw: true
|
|
33
33
|
f.options.timeout = @timeout
|
|
34
34
|
f.options.open_timeout = @timeout
|
|
35
35
|
f.adapter Faraday.default_adapter
|
|
@@ -92,6 +92,23 @@ module Printavo
|
|
|
92
92
|
execute_envelope(mutation_string, variables: variables)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Rehydrates one captured GraphQL response without performing network I/O.
|
|
96
|
+
# The exact payload remains attached to the immutable envelope while parsed
|
|
97
|
+
# data and errors follow the same validation and sanitization as live calls.
|
|
98
|
+
def envelope_from_response_payload(response_payload, metadata: {})
|
|
99
|
+
raise ArgumentError, 'response_payload must be a String' unless response_payload.is_a?(String)
|
|
100
|
+
|
|
101
|
+
payload = response_payload.b.dup.freeze
|
|
102
|
+
body = JSON.parse(payload)
|
|
103
|
+
build_envelope(
|
|
104
|
+
body: body,
|
|
105
|
+
metadata: safe_metadata_hash(metadata),
|
|
106
|
+
response_payload: payload
|
|
107
|
+
)
|
|
108
|
+
rescue JSON::ParserError
|
|
109
|
+
malformed_envelope(metadata: safe_metadata_hash(metadata), response_payload: payload)
|
|
110
|
+
end
|
|
111
|
+
|
|
95
112
|
# Iterates all pages of a paginated GraphQL query, yielding each page's
|
|
96
113
|
# nodes array. The query must accept `$first: Int` and `$after: String`
|
|
97
114
|
# variables, and the target connection must expose `nodes` and `pageInfo`.
|
|
@@ -145,10 +162,10 @@ module Printavo
|
|
|
145
162
|
end
|
|
146
163
|
|
|
147
164
|
def execute_envelope(document, variables: {})
|
|
148
|
-
response = @connection.post('')
|
|
149
|
-
req.body = JSON.generate(query: document, variables: variables)
|
|
150
|
-
end
|
|
165
|
+
response = @connection.post('') { |request| request.body = JSON.generate(query: document, variables: variables) }
|
|
151
166
|
handle_envelope(response)
|
|
167
|
+
rescue Faraday::ParsingError => e
|
|
168
|
+
handle_envelope(e.response)
|
|
152
169
|
rescue Faraday::Error
|
|
153
170
|
raise TransportError, 'Printavo transport request failed', cause: nil
|
|
154
171
|
end
|
|
@@ -160,26 +177,33 @@ module Printavo
|
|
|
160
177
|
when 404 then raise NotFoundError, 'Resource not found'
|
|
161
178
|
end
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
build_envelope(
|
|
181
|
+
body: response.body,
|
|
182
|
+
metadata: safe_metadata_hash(response.headers.to_h),
|
|
183
|
+
response_payload: raw_response_payload(response)
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def build_envelope(body:, metadata:, response_payload:)
|
|
188
|
+
return malformed_envelope(metadata: metadata, response_payload: response_payload) unless valid_envelope?(body)
|
|
165
189
|
|
|
166
190
|
ResponseEnvelope.new(
|
|
167
191
|
data: body['data'],
|
|
168
192
|
errors: sanitize_errors(body.fetch('errors', [])),
|
|
169
|
-
metadata:
|
|
193
|
+
metadata: metadata,
|
|
194
|
+
response_payload: response_payload
|
|
170
195
|
)
|
|
171
196
|
end
|
|
172
197
|
|
|
173
198
|
def valid_envelope?(body)
|
|
174
|
-
return false unless body.is_a?(Hash)
|
|
175
|
-
return false unless body.key?('data') || body.key?('errors')
|
|
199
|
+
return false unless body.is_a?(Hash) && (body.key?('data') || body.key?('errors'))
|
|
176
200
|
return false unless body['data'].nil? || body['data'].is_a?(Hash)
|
|
177
201
|
|
|
178
202
|
errors = body.fetch('errors', [])
|
|
179
203
|
errors.is_a?(Array) && errors.all?(Hash)
|
|
180
204
|
end
|
|
181
205
|
|
|
182
|
-
def malformed_envelope(
|
|
206
|
+
def malformed_envelope(metadata:, response_payload:)
|
|
183
207
|
ResponseEnvelope.new(
|
|
184
208
|
data: nil,
|
|
185
209
|
errors: [
|
|
@@ -188,10 +212,16 @@ module Printavo
|
|
|
188
212
|
'extensions' => { 'code' => 'MALFORMED_RESPONSE' }
|
|
189
213
|
}
|
|
190
214
|
],
|
|
191
|
-
metadata:
|
|
215
|
+
metadata: metadata,
|
|
216
|
+
response_payload: response_payload
|
|
192
217
|
)
|
|
193
218
|
end
|
|
194
219
|
|
|
220
|
+
def raw_response_payload(response)
|
|
221
|
+
payload = response.env[:raw_body] || response.body
|
|
222
|
+
payload if payload.is_a?(String)
|
|
223
|
+
end
|
|
224
|
+
|
|
195
225
|
def sanitize_errors(errors)
|
|
196
226
|
errors.map do |error|
|
|
197
227
|
{
|
|
@@ -227,8 +257,10 @@ module Printavo
|
|
|
227
257
|
extensions.slice(*SAFE_ERROR_EXTENSION_KEYS).transform_values { |value| redact(value.to_s)[0, 100] }
|
|
228
258
|
end
|
|
229
259
|
|
|
230
|
-
def
|
|
231
|
-
|
|
260
|
+
def safe_metadata_hash(value)
|
|
261
|
+
raise ArgumentError, 'metadata must be a Hash' unless value.is_a?(Hash)
|
|
262
|
+
|
|
263
|
+
headers = value.transform_keys { |key| key.to_s.downcase }
|
|
232
264
|
SAFE_METADATA_HEADERS.each_with_object({}) do |header, metadata|
|
|
233
265
|
value = headers[header]
|
|
234
266
|
metadata[header] = redact(value.to_s)[0, 100] unless value.nil?
|
data/lib/printavo/page.rb
CHANGED
|
@@ -14,11 +14,32 @@ 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(
|
|
17
|
+
Page = Struct.new(
|
|
18
|
+
:records,
|
|
19
|
+
:has_next_page,
|
|
20
|
+
:end_cursor,
|
|
21
|
+
:errors,
|
|
22
|
+
:metadata,
|
|
23
|
+
:response_payload,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
) do
|
|
26
|
+
def initialize(**attributes)
|
|
27
|
+
payload = attributes[:response_payload]
|
|
28
|
+
raise ArgumentError, 'response_payload must be a String' if payload && !payload.is_a?(String)
|
|
29
|
+
|
|
30
|
+
payload = payload.b.dup.freeze if payload
|
|
31
|
+
super(**attributes.merge(response_payload: payload))
|
|
32
|
+
end
|
|
33
|
+
|
|
18
34
|
def to_a = records
|
|
19
35
|
def size = records.size
|
|
20
36
|
def empty? = records.empty?
|
|
21
37
|
def success? = Array(errors).empty?
|
|
22
38
|
def partial? = records.any? && !success?
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class} records=#{records.inspect} has_next_page=#{has_next_page.inspect} " \
|
|
42
|
+
"end_cursor=#{end_cursor.inspect} errors=#{errors.inspect} metadata=#{metadata.inspect}>"
|
|
43
|
+
end
|
|
23
44
|
end
|
|
24
45
|
end
|
|
@@ -30,6 +30,14 @@ module Printavo
|
|
|
30
30
|
build_page(envelope)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Rebuilds a captured contacts page through the SDK parser without
|
|
34
|
+
# performing provider network I/O.
|
|
35
|
+
def page_from_response_payload(response_payload, metadata: {})
|
|
36
|
+
build_page(
|
|
37
|
+
@graphql.envelope_from_response_payload(response_payload, metadata: metadata)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
# Finds a contact by ID.
|
|
34
42
|
#
|
|
35
43
|
# @param id [String, Integer]
|
|
@@ -115,7 +123,8 @@ module Printavo
|
|
|
115
123
|
has_next_page: page_info.fetch('hasNextPage', false),
|
|
116
124
|
end_cursor: page_info['endCursor'],
|
|
117
125
|
errors: envelope.errors,
|
|
118
|
-
metadata: envelope.metadata
|
|
126
|
+
metadata: envelope.metadata,
|
|
127
|
+
response_payload: envelope.response_payload
|
|
119
128
|
)
|
|
120
129
|
end
|
|
121
130
|
end
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
module Printavo
|
|
5
5
|
class ResponseEnvelope
|
|
6
|
-
attr_reader :data, :errors, :metadata
|
|
6
|
+
attr_reader :data, :errors, :metadata, :response_payload
|
|
7
7
|
|
|
8
|
-
def initialize(data:, errors: [], metadata: {})
|
|
8
|
+
def initialize(data:, errors: [], metadata: {}, response_payload: nil)
|
|
9
9
|
@data = immutable_copy(data)
|
|
10
10
|
@errors = immutable_copy(errors)
|
|
11
11
|
@metadata = immutable_copy(metadata)
|
|
12
|
+
@response_payload = immutable_payload(response_payload)
|
|
12
13
|
freeze
|
|
13
14
|
end
|
|
14
15
|
|
|
@@ -24,8 +25,19 @@ module Printavo
|
|
|
24
25
|
{ 'data' => data, 'errors' => errors, 'metadata' => metadata }.freeze
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
def inspect
|
|
29
|
+
"#<#{self.class} data=#{data.inspect} errors=#{errors.inspect} metadata=#{metadata.inspect}>"
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
private
|
|
28
33
|
|
|
34
|
+
def immutable_payload(value)
|
|
35
|
+
return if value.nil?
|
|
36
|
+
raise ArgumentError, 'response_payload must be a String' unless value.is_a?(String)
|
|
37
|
+
|
|
38
|
+
value.b.dup.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
29
41
|
def immutable_copy(value)
|
|
30
42
|
case value
|
|
31
43
|
when Hash
|
data/lib/printavo/version.rb
CHANGED
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.20.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-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.
|
|
19
|
+
version: '2.14'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.
|
|
26
|
+
version: '2.14'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: faraday-retry
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|