printavo-ruby 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7786dadca364f38068636a9a6e30efac78ac79107c8dfb4ade22b39db5f0bc1
4
- data.tar.gz: f0464978791f12c8e3f5d37d2de09ecfc4b97e57aca4999beda995b68957d2e1
3
+ metadata.gz: 4bdc16c358a3dafbdae1842e0ddb8eebbe8775053b1b95efb2308cd33056e2ea
4
+ data.tar.gz: 7399f57922f9244a02900c458bf9d2b9830aa0d63a5b4ff07952ca3ade6254fd
5
5
  SHA512:
6
- metadata.gz: a6aae8c15d5f5c348d8e717c09eaf85092a9421d473541ac1ef3c38042534ed1aa14c6e0c5283d6878325cdf32a7325fed52e980913e7b15e8c443ed64a695bb
7
- data.tar.gz: bac95ec4243aa408c495f88d7c4841a308f7c49f509d8b7a0f3a4a161cb9db30418d2ba2c79be67f353c89b68d24cec6f70c1487b64496ea5ef74489fe44acc8
6
+ metadata.gz: fa26e26063d2ed94011d20f22c4ae6c9fa964c62d2fa07b31343b97f2d1e9dbe623114960dab489cca029fe0d1f036964c7748b27a96e1c2eb7d63d33d3d285f
7
+ data.tar.gz: 0427ed5bc1601858080cb1f057547cb4143cc51d32cbd7189fa4fc8546260045c11603204814e28656b26a97f00e286634b24f8b536c879d14e5f8dd44f8313c
data/README.md CHANGED
@@ -196,6 +196,42 @@ result = client.graphql.query(
196
196
  )
197
197
  ```
198
198
 
199
+ ### Mutations
200
+
201
+ Use `mutate` for GraphQL write operations:
202
+
203
+ ```ruby
204
+ result = client.graphql.mutate(
205
+ <<~GQL,
206
+ mutation UpdateOrder($id: ID!, $nickname: String!) {
207
+ updateOrder(id: $id, input: { nickname: $nickname }) {
208
+ order { id nickname }
209
+ errors
210
+ }
211
+ }
212
+ GQL
213
+ variables: { id: "99", nickname: "Rush Job" }
214
+ )
215
+ result["updateOrder"]["order"]["nickname"] # => "Rush Job"
216
+ ```
217
+
218
+ ### Raw GraphQL Pagination
219
+
220
+ Paginate any custom query without a resource wrapper using `paginate`.
221
+ The `path:` option is dot-separated and maps to the connection in the response:
222
+
223
+ ```ruby
224
+ client.graphql.paginate(MY_QUERY, path: "orders", first: 50) do |nodes|
225
+ nodes.each { |n| puts n["nickname"] }
226
+ end
227
+
228
+ # Nested connection (e.g. order.lineItems)
229
+ client.graphql.paginate(JOBS_QUERY, path: "order.lineItems",
230
+ variables: { orderId: "99" }) do |nodes|
231
+ nodes.each { |j| puts j["name"] }
232
+ end
233
+ ```
234
+
199
235
  ## Webhooks
200
236
 
201
237
  `Printavo::Webhooks.verify` provides Rack-compatible HMAC-SHA256 signature verification.
@@ -257,12 +293,11 @@ end
257
293
  | 0.1.0 | Auth + Customers + Orders + Jobs + Webhooks (Rack-compatible) ✅ |
258
294
  | 0.2.0 | Status registry + Inquiries ✅ |
259
295
  | 0.3.0 | Pagination helpers (`each_page`, `all_pages`) + `bin/lint` ✅ |
260
- | 0.4.0 | Expanded GraphQL DSL |
296
+ | 0.4.0 | Expanded GraphQL DSL (`mutate`, `paginate`) ✅ |
261
297
  | 0.5.0 | Mutations (create/update) |
262
298
  | 0.6.0 | Analytics / Reporting queries |
263
299
  | 0.7.0 | Community burn-in / API stabilization |
264
- | 0.8.0 | Pagination abstraction helpers |
265
- | 0.9.0 | Retry/backoff + rate limit awareness |
300
+ | 0.8.0 | Retry/backoff + rate limit awareness |
266
301
  | 1.0.0 | Stable public SDK |
267
302
 
268
303
  **Rules**: `PATCH` = bug fix · `MINOR` = new backward-compatible feature · `MAJOR` = breaking change
data/docs/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.4.0] - 2026-03-30
12
+
13
+ ### Added
14
+ - `GraphqlClient#mutate` — semantic method for GraphQL mutations; identical transport to `query` but signals write intent at the call site; lays the foundation for 0.5.0 resource-level mutations
15
+ - `GraphqlClient#paginate` — cursor-following pagination for raw GraphQL queries; accepts a dot-separated `path:` to resolve nested connections (e.g. `"order.lineItems"`); yields each page's `nodes` array
16
+ - `GraphqlClient#execute` (private) — extracted shared POST logic; `query` and `mutate` both delegate to it
17
+ - `GraphqlClient#dig_path` (private) — resolves dot-separated key paths against nested response hashes
18
+ - Roadmap: removed stale Pagination entry from 0.8.0 (shipped in 0.3.0); Retry/backoff renumbered to 0.8.0
19
+
11
20
  ## [0.3.0] - 2026-03-29
12
21
 
13
22
  ### Added
data/docs/FUTURE.md CHANGED
@@ -20,21 +20,6 @@ printavo sync orders --to crm
20
20
 
21
21
  Planned version: `0.7.0`
22
22
 
23
- ### Pagination Abstraction
24
-
25
- A lazy-enumerator-style helper to automatically page through all results:
26
-
27
- ```ruby
28
- client.customers.each_page do |page|
29
- page.each { |c| process(c) }
30
- end
31
-
32
- # Or collect all:
33
- all_orders = client.orders.all_pages
34
- ```
35
-
36
- Planned version: `0.8.0`
37
-
38
23
  ### Retry/Backoff
39
24
 
40
25
  Intelligent rate limit handling with exponential backoff:
@@ -48,7 +33,7 @@ client = Printavo::Client.new(
48
33
  )
49
34
  ```
50
35
 
51
- Planned version: `0.9.0`
36
+ Planned version: `0.8.0`
52
37
 
53
38
  ### Analytics / Reporting Expansion
54
39
 
@@ -7,16 +7,90 @@ module Printavo
7
7
  @connection = connection
8
8
  end
9
9
 
10
+ # Executes a GraphQL query and returns the parsed `data` hash.
11
+ #
12
+ # @param query_string [String] the GraphQL query document
13
+ # @param variables [Hash] optional input variables
14
+ # @return [Hash]
15
+ #
16
+ # @example
17
+ # client.graphql.query("{ customers { nodes { id } } }")
18
+ # client.graphql.query(
19
+ # "query Customer($id: ID!) { customer(id: $id) { id email } }",
20
+ # variables: { id: "42" }
21
+ # )
10
22
  def query(query_string, variables: {})
11
- response = @connection.post('') do |req|
12
- req.body = JSON.generate(query: query_string, variables: variables)
13
- end
23
+ execute(query_string, variables: variables)
24
+ end
14
25
 
15
- handle_response(response)
26
+ # Executes a GraphQL mutation and returns the parsed `data` hash.
27
+ # Semantically equivalent to `query` — both POST to the same endpoint —
28
+ # but distinguishes write intent at the call site.
29
+ #
30
+ # @param mutation_string [String] the GraphQL mutation document
31
+ # @param variables [Hash] optional input variables
32
+ # @return [Hash]
33
+ #
34
+ # @example
35
+ # client.graphql.mutate(
36
+ # <<~GQL,
37
+ # mutation UpdateOrder($id: ID!, $input: OrderInput!) {
38
+ # updateOrder(id: $id, input: $input) {
39
+ # order { id nickname }
40
+ # errors
41
+ # }
42
+ # }
43
+ # GQL
44
+ # variables: { id: "99", input: { nickname: "Rush Job" } }
45
+ # )
46
+ def mutate(mutation_string, variables: {})
47
+ execute(mutation_string, variables: variables)
48
+ end
49
+
50
+ # Iterates all pages of a paginated GraphQL query, yielding each page's
51
+ # nodes array. The query must accept `$first: Int` and `$after: String`
52
+ # variables, and the target connection must expose `nodes` and `pageInfo`.
53
+ #
54
+ # @param query_string [String] the GraphQL query document
55
+ # @param path [String] dot-separated key path to the connection in the response
56
+ # e.g. "orders" or "customer.orders"
57
+ # @param variables [Hash] additional variables merged with `first` and `after`
58
+ # @param first [Integer] page size (default 25)
59
+ # @yieldparam nodes [Array<Hash>] one page of raw node hashes
60
+ #
61
+ # @example
62
+ # client.graphql.paginate(ORDERS_QUERY, path: "orders") do |nodes|
63
+ # nodes.each { |n| puts n["nickname"] }
64
+ # end
65
+ #
66
+ # @example With extra variables
67
+ # client.graphql.paginate(JOBS_QUERY, path: "order.lineItems",
68
+ # variables: { orderId: "99" }, first: 50) do |nodes|
69
+ # nodes.each { |j| puts j["name"] }
70
+ # end
71
+ def paginate(query_string, path:, variables: {}, first: 25)
72
+ after = nil
73
+ loop do
74
+ data = execute(query_string, variables: variables.merge(first: first, after: after))
75
+ conn = dig_path(data, path)
76
+ nodes = conn&.fetch('nodes', []) || []
77
+ yield nodes
78
+ page_info = conn&.fetch('pageInfo', {}) || {}
79
+ break unless page_info['hasNextPage']
80
+
81
+ after = page_info['endCursor']
82
+ end
16
83
  end
17
84
 
18
85
  private
19
86
 
87
+ def execute(document, variables: {})
88
+ response = @connection.post('') do |req|
89
+ req.body = JSON.generate(query: document, variables: variables)
90
+ end
91
+ handle_response(response)
92
+ end
93
+
20
94
  def handle_response(response)
21
95
  body = response.body
22
96
 
@@ -34,5 +108,11 @@ module Printavo
34
108
 
35
109
  body.is_a?(Hash) ? body['data'] : body
36
110
  end
111
+
112
+ # Resolves a dot-separated path against a nested hash.
113
+ # e.g. dig_path(data, "customer.orders") => data["customer"]["orders"]
114
+ def dig_path(data, path)
115
+ path.split('.').reduce(data) { |obj, key| obj.is_a?(Hash) ? obj[key] : nil }
116
+ end
37
117
  end
38
118
  end
@@ -1,4 +1,4 @@
1
1
  # lib/printavo/version.rb
2
2
  module Printavo
3
- VERSION = '0.3.0'.freeze
3
+ VERSION = '0.4.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: printavo-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Carver II