graph_weaver 0.6.1 → 0.7.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +153 -4
  4. data/README.md +45 -79
  5. data/docs/alternatives.md +195 -0
  6. data/docs/cassettes.md +61 -50
  7. data/docs/editors.md +32 -47
  8. data/docs/errors.md +360 -103
  9. data/docs/federation.md +692 -473
  10. data/docs/generated_modules.md +441 -314
  11. data/docs/getting_started.md +370 -194
  12. data/docs/i18n.md +171 -0
  13. data/docs/logging.md +197 -50
  14. data/docs/real_world.md +42 -27
  15. data/docs/scalars.md +307 -176
  16. data/docs/testing.md +473 -220
  17. data/docs/transports.md +224 -151
  18. data/docs/upgrading.md +258 -305
  19. data/examples/README.md +38 -0
  20. data/examples/countries.rb +39 -0
  21. data/examples/federation.rb +62 -0
  22. data/examples/github/generate.rb +20 -0
  23. data/examples/github/generated/star_mutation.rb +126 -0
  24. data/examples/github/generated/stargazers_query.rb +232 -0
  25. data/examples/github/generated/starred_query.rb +151 -0
  26. data/examples/github/queries/star.graphql +8 -0
  27. data/examples/github/queries/stargazers.graphql +22 -0
  28. data/examples/github/queries/starred.graphql +11 -0
  29. data/examples/github/run.rb +43 -0
  30. data/examples/github/setup.rb +18 -0
  31. data/examples/rick_and_morty.rb +57 -0
  32. data/graph_weaver.gemspec +19 -3
  33. data/lib/generators/graph_weaver/install_generator.rb +138 -4
  34. data/lib/graph_weaver/client.rb +69 -11
  35. data/lib/graph_weaver/codegen/aliases.rb +7 -5
  36. data/lib/graph_weaver/codegen/emit.rb +98 -29
  37. data/lib/graph_weaver/codegen/enum_type.rb +2 -1
  38. data/lib/graph_weaver/codegen/nodes.rb +39 -6
  39. data/lib/graph_weaver/codegen/registry.rb +175 -0
  40. data/lib/graph_weaver/codegen/scalar_type.rb +123 -30
  41. data/lib/graph_weaver/codegen/type_helpers.rb +56 -11
  42. data/lib/graph_weaver/codegen.rb +406 -195
  43. data/lib/graph_weaver/coerce.rb +155 -26
  44. data/lib/graph_weaver/context_seam.rb +54 -0
  45. data/lib/graph_weaver/errors.rb +284 -46
  46. data/lib/graph_weaver/federation.rb +129 -27
  47. data/lib/graph_weaver/graph.rb +315 -0
  48. data/lib/graph_weaver/hints.rb +100 -24
  49. data/lib/graph_weaver/in_process.rb +27 -15
  50. data/lib/graph_weaver/input_struct.rb +119 -32
  51. data/lib/graph_weaver/internal/endpoint.rb +80 -0
  52. data/lib/graph_weaver/internal/headers.rb +70 -0
  53. data/lib/graph_weaver/internal/overrides.rb +67 -5
  54. data/lib/graph_weaver/internal/planner.rb +45 -15
  55. data/lib/graph_weaver/internal/refusal.rb +49 -0
  56. data/lib/graph_weaver/internal/schemas.rb +23 -9
  57. data/lib/graph_weaver/internal/selection.rb +34 -0
  58. data/lib/graph_weaver/internal/server_input.rb +251 -0
  59. data/lib/graph_weaver/internal/test_clients.rb +276 -0
  60. data/lib/graph_weaver/internal/unused.rb +287 -0
  61. data/lib/graph_weaver/internal/values.rb +40 -4
  62. data/lib/graph_weaver/internal.rb +249 -14
  63. data/lib/graph_weaver/log_subscriber.rb +74 -0
  64. data/lib/graph_weaver/logging.rb +163 -19
  65. data/lib/graph_weaver/query_module.rb +44 -3
  66. data/lib/graph_weaver/railtie.rb +237 -17
  67. data/lib/graph_weaver/representation.rb +55 -17
  68. data/lib/graph_weaver/result_struct.rb +90 -0
  69. data/lib/graph_weaver/retry.rb +45 -13
  70. data/lib/graph_weaver/rspec.rb +404 -93
  71. data/lib/graph_weaver/schema_loader.rb +266 -56
  72. data/lib/graph_weaver/tasks.rb +380 -89
  73. data/lib/graph_weaver/testing/cassette.rb +34 -10
  74. data/lib/graph_weaver/testing/endpoint.rb +107 -0
  75. data/lib/graph_weaver/testing/failure.rb +69 -12
  76. data/lib/graph_weaver/testing/fake_client.rb +164 -45
  77. data/lib/graph_weaver/testing/router.rb +64 -13
  78. data/lib/graph_weaver/testing.rb +200 -58
  79. data/lib/graph_weaver/transport/faraday.rb +41 -8
  80. data/lib/graph_weaver/transport/http.rb +48 -6
  81. data/lib/graph_weaver/transport.rb +134 -27
  82. data/lib/graph_weaver/version.rb +1 -1
  83. data/lib/graph_weaver.rb +495 -106
  84. metadata +71 -3
  85. data/CHANGELOG.md +0 -2355
data/docs/upgrading.md CHANGED
@@ -1,333 +1,286 @@
1
1
  # Upgrading
2
2
 
3
+ [Regenerate](#regenerate-on-every-upgrade) whichever version you're on, then read
4
+ the one section that is yours: from [0.7.0](#upgrading-from-070) or from
5
+ [0.6.1](#upgrading-from-061). Coming from 0.6.0 or older, the path is that
6
+ version's own upgrade notes — read them at the tag they shipped under
7
+ (`git show v0.7.1:docs/upgrading.md`), then this page from 0.6.1 down.
8
+
3
9
  ## Regenerate on every upgrade
4
10
 
5
- **Any release can change what codegen emits.** Patch releases included — most of
11
+ **Any release can change what codegen emits**, patch releases included — most of
6
12
  them are fixes to a generated type, and a fix to a type is a change to the bytes.
7
- 0.5.1 was a patch and moved three of them.
8
-
9
13
  So `rake graph_weaver:generate` is part of upgrading the gem, every time, and
10
14
  `rake graph_weaver:verify` is the detector: it fails when the checked-in Ruby
11
- isn't what this version would write. Nothing beyond that is promised — there is
12
- no "generated output is stable within a minor" rule to lean on. What each release
13
- changed, and whether it needs a regenerate, is in the changelog.
15
+ isn't what this version would write. Nothing beyond that is promised — there is no
16
+ "generated output is stable within a minor" rule to lean on, and what each release
17
+ changed is in the [changelog](../CHANGELOG.md).
14
18
 
15
19
  A generated file's header names the release that wrote it, so the first `verify`
16
- after an upgrade reports the tree as stale whether or not codegen actually
17
- moved. That's the reminder working, not a false alarm.
20
+ after an upgrade reports the tree as stale whether or not codegen actually moved.
21
+ That's the reminder working, not a false alarm. Generation is deterministic, so
22
+ the diff is exactly what the new version emits differently and nothing else —
23
+ worth reading rather than rubber-stamping.
18
24
 
19
- Generation is deterministic, so the diff is exactly what the new version emits
20
- differently and nothing else — worth reading rather than rubber-stamping.
25
+ ## Upgrading from 0.7.0
21
26
 
22
- ## Upgrading from 0.5.1
27
+ A patch release of fixes, and a typical app ticks none of these. Read the left
28
+ column and skip what isn't yours; the [changelog](../CHANGELOG.md) says why each
29
+ one moved.
23
30
 
24
- Much smaller than 0.5.0, and mostly mechanical. Three commands find most of it:
31
+ | applies if you… | what changed |
32
+ |---|---|
33
+ | read `payload[:code]` in an alert or a dashboard expecting an HTTP status | it is a GraphQL error code or nothing now — the number is on `:http_status`, where it already was |
34
+ | assert a fabricated `userErrors` is non-empty under `graphql: :fake` | a list field whose name ends in `errors` fabricates `[]` — pin it (`{ "userErrors" => [{ "message" => "…" }] }`) to fabricate failures |
35
+ | grep or parse the debug log for `[req 3 …]` | the tag names the process: `[req 4123-3 …]` |
36
+ | run more than one graph with `cache: true` | the second graph caches to `schema-<url digest>.json` of its own instead of sharing the first's dump — `cache: "<path>"` names it yourself |
37
+ | commit your cassette directory | recording takes a `<cassette>.yml.lock` sidecar — gitignore `*.yml.lock` |
38
+ | `rescue ArgumentError` around the union-dispatch refusal | it is a `GraphWeaver::Error` naming the query file now |
39
+ | wrote a client of your own with a bare `attr_accessor :context` and mount it behind `Testing::Endpoint` | include `GraphWeaver::ContextSeam` — the lock lives on whoever owns the field |
40
+ | read `CHANGELOG.md` out of the installed gem | it isn't packaged any more; `changelog_uri` points at `blob/v<version>` |
41
+ | send a `File`, `IO`, `Pathname` or plain object as a variable under `graphql: :in_process` or `:fake` | refused there too now, as it already was over the wire — **a test that "proved" an upload works starts failing** |
42
+ | retry a 429/503 that arrives **with** a GraphQL errors body | a `Retry-After` header wins over the configured backoff now, as it already did for a raised `ServerError` — **nothing raises** |
43
+
44
+ ## Upgrading from 0.6.1
45
+
46
+ Mostly mechanical. Everything that wants your hands, or changes under you, is one
47
+ row below; read the left column and skip what isn't yours. A typical app ticks two
48
+ or three.
49
+
50
+ | applies if you… | what changed |
51
+ |---|---|
52
+ | run a Rails app that configures no logger or instrumenter | **you start logging one info line per GraphQL call** — a production log-volume change, [first bullet below](#behavior-that-changed-under-you) |
53
+ | wrap a gateway in `Retry` | **a 5xx/429 that arrives with an errors body retries now** — real traffic, [below](#behavior-that-changed-under-you); `retries: 0` opts out |
54
+ | commit a schema dump introspected through a url carrying a token | **refresh it, and rotate the token if that file was pushed** — the dump recorded the url verbatim |
55
+ | keep a schema dump deliberately behind your own schema class | `verify` fails on it now |
56
+ | check in a composed supergraph as your dump | `schema:refresh` refuses it rather than overwriting it with the API schema — recompose instead |
57
+ | use `@oneOf` input types and commit a `.json` dump | `@oneOf` starts being enforced client-side once you regenerate |
58
+ | call `result.to_json`, or `render json: result` | it is the wire shape now, not `#inspect` or your prop names |
59
+ | send a `File`, `IO` or `Pathname` as a variable | refused at the wire, where `JSON.generate` used to ship its `#to_s` |
60
+ | pin a lowercase type name in `graphql_fake` | it works now; a near-miss *keyword* raises `ArgumentError` |
61
+ | tag specs `graphql: false`, or set `config.default_mode = nil` | both refused — [renames](#renames) |
62
+ | `rescue GraphWeaver::TypeError` or `GraphWeaver::ValidationError` | both constants are gone, with no alias — [renames](#renames) |
63
+ | subscribe to `"graph_weaver.execute"` | the event is `"execute.graph_weaver"` — [renames](#renames) |
64
+ | index a hash by an `InputError`'s `#field` | it names the input field now, not the variable — **nothing raises** |
65
+ | read an `InputError`'s `#details[:type]` | it is the GraphQL type now, never a Ruby class — **nothing raises** |
66
+ | read `payload[:status]` in an instrumentation subscriber | it is a Symbol; the HTTP status moved to `:http_status` — **nothing raises** |
67
+ | call `respond_to?` on a result struct | it stopped answering true for props that don't exist — **nothing raises** |
68
+ | generate a module with a baked `client:` | a `graphql:` tag now reaches it |
69
+ | set `config.context`, `config.schema` or `config.router` from a `before` hook | all three refused — they are suite setup |
70
+ | pass a `DateTime` where the schema says `Date` | refused — pass `.to_date` |
71
+ | register a scalar with your own `cast:`/`serialize:` | the same guard as the built-ins, and a proc that returns a value is refused |
72
+ | have a field named `class`, `hash`, `display`, `to_json`, `each` or `supplied` | the prop takes a trailing underscore |
73
+ | have an entity `@key` that selects through a list | the kwarg is a list now, not one hash — regenerate |
74
+ | build a type helper with `extend_type("Widget") { … }` | its constant is named for its graph and its type — regenerate |
75
+ | adopt `GraphWeaver.graph` | every queries directory then needs one |
76
+ | write `config.graph_weaver.<anything but watch>` | refused at boot |
77
+ | pass `SUPERGRAPH=` to any task but `federation:*` | refused, where it was ignored — a CI step that did it goes red |
78
+ | compare a `Testing::Router` error hash whole in a spec | a subgraph error carries `extensions: {"service" => …}` now |
79
+ | pass `seed:` to `graphql_router(fake: …)` | refused |
80
+ | require `graph_weaver/rspec` from `spec/support/` | check the glob is uncommented — rspec-rails ships it commented out |
81
+ | adopt `graphql: :wire` | it needs `require "webmock/rspec"`, not just the gem |
82
+
83
+ Then five commands, in order:
25
84
 
26
85
  ```sh
27
- rake graph_weaver:generate # 1. what codegen emits moved in several places
28
- srb tc # 2. kwargs that got narrower are call-site errors
29
- bundle exec rspec # 3. every deleted knob raises where it's still set
30
- ```
86
+ # 1. the two renames your own code holds
87
+ grep -rn "GraphWeaver::TypeError\|GraphWeaver::ValidationError" app lib spec
88
+ grep -rn "graph_weaver.execute" app lib config spec # the old event name
31
89
 
32
- The rest of this section is what those three don't catch.
90
+ # 2. rewrite the dump: it drops a credential the url carried, and picks up
91
+ # isOneOf. Skip only if your dump is SDL and records no source url — and
92
+ # a composed supergraph refuses, since introspection can't rebuild one:
93
+ # `rover supergraph compose` is what rewrites that.
94
+ rake graph_weaver:schema:refresh
33
95
 
34
- ### Loose input coerces, so `coerce:` and `auto_coerce` are gone
96
+ # 3. regenerate also the graph name in every module, the underscored
97
+ # reserved props, as_json, and the client: and cast:/serialize: refusals
98
+ rake graph_weaver:generate
35
99
 
36
- `execute(first: params[:first])` converts the String to an `Integer` for every
37
- variable and every input-object field, with nothing to switch on. The old way of
38
- buying that was `GraphWeaver.auto_coerce` or `register_scalar(…, coerce: true)`,
39
- and both paid for it by **widening the emitted kwarg**, which switched off the
40
- static check at every call site. Delete them:
100
+ # 4. the renamed tag, the deleted nil, the seed: refusal
101
+ bundle exec rspec
41
102
 
42
- ```ruby
43
- GraphWeaver.auto_coerce = true # gone
44
- GraphWeaver.register_scalar("Money", Money, coerce: true) # drop the coerce:
103
+ # 5. the gate: red while any checked-in file is still what 0.6.1 wrote
104
+ rake graph_weaver:verify
45
105
  ```
46
106
 
47
- Behavior is unchanged; the kwarg is not. It is now typed exactly as the schema
48
- types it, so a call site passing a **literal** of the wrong type is a new
49
- `srb tc` errorwhich is the point, since a literal is one you can just spell
50
- right:
51
-
52
- ```ruby
53
- StargazersQuery.execute(first: "10") # srb tc error now
54
- StargazersQuery.execute(first: params[:first]) # fine, and "10" becomes 10
55
- ```
56
-
57
- `cast:` is what a loose value converts through, so a custom scalar needs nothing
58
- beyond the registration it already has. Bad input raises
59
- `GraphWeaver::InputError` naming the variable, the operation and the value.
60
-
61
- Two conversions got **stricter** at the same time, and either can bite an app
62
- that was passing. A numeric string is now read as a wire format rather than as
63
- Ruby source, so `"010"` is ten rather than eight and `"0x1f"` and `"1_0"` are
64
- refused. And a `Boolean` refuses a String outright — every rule for `"0"` and
65
- `"off"` is somebody's convention, so convert at the call site.
66
-
67
- ### `nil` sends `null`
68
-
69
- A variable passed `nil` now sends an explicit `null`; one left out is still left
70
- out. That's what lets a mutation clear a field — and it changes what a kwarg fed
71
- a possibly-missing value means:
72
-
73
- ```ruby
74
- UpdateProfile.execute!(bio: params[:bio]) # a missing param used to omit; now it clears the bio
75
- ```
76
-
77
- **Grep for kwargs fed straight from `params` or an optional attribute**, and
78
- pass the keyword only when you mean it:
79
-
80
- ```ruby
81
- UpdateProfile.execute!(**(params[:bio] ? { bio: params[:bio] } : {}))
82
- ```
83
-
84
- Non-null variables are unaffected: they can't carry `null`, so `nil` there still
85
- omits and the schema default applies. Input objects get the distinction only
86
- where a Hash can express it — `coerce({nickname: nil})` sends null, `coerce({})`
87
- omits, and a struct built with `.new` can't tell the two apart, so `nil` there
88
- still means omit.
107
+ **Two kinds of file answer that first grep, and only one needs your hands.** Hits
108
+ under your generated directory (`app/graphql/generated/` by default) are the old
109
+ names in machine-written code step 3 rewrites them. Hits anywhere else are
110
+ yours: `CastError` and `QueryValidationError`, renamed by hand.
89
111
 
90
112
  ### Renames
91
113
 
92
114
  | before | after |
93
115
  |---|---|
94
- | `Retry.new(tries: n)`, `retries: { tries: n }` | `retries: n - 1` one word everywhere, counting the attempts *after* the first, so `retries: 0` is one attempt and `GraphWeaver.new(url, retries: 3)` is four |
95
- | `GraphWeaver.new(url, retries: { retries: 5, retry_codes: […] })` | `GraphWeaver.new(url, retries: 5, retry_codes: […])` the other retry options sit beside the count; the Hash form read as a key nested in itself |
96
- | `Retry.new(t, on: […])` | `Retry.new(t, retry_on: […])` |
97
- | `Retry.new(t, base: 0.5, max: 30)` | `Retry.new(t, base_delay: 0.5, max_delay: 30)` beside a count, `max: 30` read as a second, larger attempt count |
98
- | `Codegen.generate(module_name:)` | `name:` — the spelling `GraphWeaver.parse` already used; `module_name:` now raises, naming its replacement |
99
- | `Testing.config.null_chance = 0.3` | `graphql_fake(null_chance: 0.3)`, on the example that wants it |
100
- | `Testing.config.mode = :literal` | `graphql_fake(values: :literal)`, likewise |
101
- | `Testing::MODES` | `Testing::VALUE_STYLES` |
102
- | `SchemaLoader.stale?(path)` | `SchemaLoader.diff(path).empty?` — and `diff` also names what moved |
103
-
104
- The two `Testing.config` deletions are the ones worth a sentence. A suite-wide
105
- `null_chance` answers a per-example question, so it nils an unrelated field one
106
- run in ten, on a seed the failure doesn't name; move it onto the examples that
107
- are *about* an empty state. (`config.default_mode` and the `graphql: :fake` tag
108
- are untouched — the per-fake `mode:` became `values:` so the two can't be
109
- confused for each other.) Every retry misspelling raises rather than being
110
- ignored: the Hash form names its flat replacement, and a retry option passed
111
- without `retries:` says so.
112
-
113
- ### The internals moved behind `Internal`
114
-
115
- The public surface is now what the docs name, what generated code calls, and the
116
- `execute` slot; everything else sits under `GraphWeaver::Internal` or went
117
- `private`, and a spec diffs the two so the next accidental promotion fails CI.
118
- Nothing documented moved — skip this section unless `srb tc` or a
119
- `NoMethodError` says otherwise.
120
-
121
- What a suite might plausibly have reached for: the federation query planner and
122
- its IR (`Internal::Planner`), the fake-value engine (`Internal::Values`), the
123
- selection walk (`Internal::Selection` — so `FakeClient` no longer answers
124
- `each_field` or `gather`), the cassette matching rules (`Internal::RequestKey`),
125
- subgraph detection (was `Testing::Subgraphs`), `GraphWeaver.log` /
126
- `.instrument` / `.filter_variables` (`Internal::Log` — `logger=`,
127
- `instrumenter=` and `filter_parameters=` are unchanged), and
128
- `Transport.operation_name` / `.mutation?` / `.log_tag`, which left the class you
129
- subclass for `Internal::Wire`.
130
-
131
- Two smaller edges. `SchemaDiff::Change`, `Cassette::Check`, `Coverage::Result`
132
- and `InputStruct::Field` are `Data` now rather than `Struct`, so they hand out
133
- no writers — read one, build a new one to change a field. And generated modules
134
- keep their own plumbing to themselves: `DEFAULT_CLIENT`, `FIELDS` and `ONE_OF`
135
- are emitted `private_constant`, so **regenerate**.
116
+ | `graphql: false` (rspec tag) | `graphql: :live` the opt-out is your own client, which is a mode like the other four; `false` is refused, naming it |
117
+ | `config.default_mode = nil` | `config.default_mode = :live`, which is now the **default** every example has exactly one mode, and `nil` is no longer a value it reads back |
118
+ | `GraphWeaver::TypeError` | `GraphWeaver::CastError` the response wouldn't cast into the generated structs; the old name shadowed a core class it doesn't descend from. No alias: the old constant is gone, so a stale `rescue` is a `NameError` |
119
+ | `GraphWeaver::ValidationError` | `GraphWeaver::QueryValidationError` build time, the *query* against the schema. Your input's validation is `InputError`. No alias here either |
120
+ | `"graph_weaver.execute"` | `"execute.graph_weaver"``<event>.<namespace>`, the way every notification in this ecosystem is spelled, and what `LogSubscriber.attach_to` and an APM's namespace routing key on. Subscribe through `GraphWeaver::EXECUTE_EVENT` and there is nothing to rename; a hardcoded string silently stops matching |
136
121
 
137
122
  ### Behavior that changed under you
138
123
 
139
- - **A mutation is no longer retried.** A timeout doesn't say whether the server
140
- applied it, and a second `charge` is worse than a failed one. Pass
141
- `retry_mutations: true` for an API whose mutations are idempotent.
142
- - **A registration this schema can't match warns instead of failing
143
- generation.** One registry serves a whole federated graph, so a name the
144
- schema in hand doesn't declare may belong to the subgraph next door — see
145
- [federation](federation.md#generating-for-a-federated-graph). Your typo is now
146
- in the list `rake graph_weaver:generate` prints after the files, so read it.
147
- - **`verify_generated!` fails when it finds no query documents.** A mistyped
148
- `queries_paths` used to leave a CI gate green forever.
149
- - **The local router refuses a `@fromContext` argument** rather than fetching
150
- the field with it unset. Federation 2.8's `@context` machinery was on the
151
- routing table's known list, so the argument was read and dropped. Per query,
152
- like `@interfaceObject`: a subtree one subgraph answers whole still runs.
153
- - **A `#trace` assertion may see one entry fewer.** Two `@requires` field sets
154
- crossing into the same subgraph on the same `@key` now ride one entity fetch,
155
- the way Apollo's do.
156
- - **Fabricating a custom scalar registered as a class of your own needs a pin
157
- for the type** — `Testing.config.overrides = { "Money" => "12.00" }`, or the
158
- same key on one example's `graphql_fake`. Without one, `FakeClient` and
159
- cassette anonymization refuse rather than feeding your cast a `"Money-1"`
160
- placeholder. Scalars registered as `Time`, `Date`, `Integer`, `Float`,
161
- `String` or `T::Boolean` need nothing.
162
- - **Re-run `rake graph_weaver:cassettes:anonymize`** on any committed cassette
163
- holding a registered custom scalar: the anonymizer used to write a value the
164
- generated codec couldn't read back.
165
- - **Generation refuses four more things**, each naming its fixa
166
- `register_scalar` whose Ruby type nothing can build out of JSON (a value
167
- object of your own: give it a `cast:`), a result key that would shadow a constant the
168
- file uses, an enum value that camelizes to nothing, and a narrowed fragment
169
- whose `__typename` sits behind `@skip`/`@include`.
170
-
171
- ## Upgrading to 0.5.0
172
-
173
- 0.5.0 is one large breaking release. Almost all of it is caught mechanically,
174
- in this order:
175
-
176
- ```sh
177
- # 1. rename the path settings first generate won't load without them
178
- # (queries_path -> queries_paths, generated_path -> generated_paths,
179
- # fragments_path -> fragments_paths; see "Path settings are lists" below)
180
-
181
- bundle exec tapioca gem graph_weaver # 2. regenerate the RBI
182
- rake graph_weaver:generate # 3. the emitted call shape changed
183
- srb tc # 4. every call site that moved is an error
184
- rake graph_weaver:verify # 5. fails until the tree is regenerated
185
- ```
186
-
187
- **Step 2 is not optional.** Against the 0.4.6 RBI, `srb tc` reports errors
188
- pointing into your `generated/` directory — `QueryModule`, `client_for`,
189
- `check_envelope!` which read as though codegen emitted broken Ruby. It
190
- didn't; sorbet is checking new generated code against the old gem's types.
191
- Regenerate the RBI and what remains is only your own call sites.
192
-
193
- Generated code is `# typed: strict`, so step 4 finds those for you. The rest of
194
- this page is what a typechecker can't see.
195
-
196
- ### `execute` means one thing now
197
-
198
- Every client answers the same call `execute(query, variables:, operation_name:)`,
199
- returning the raw response hash. `Client` used to spell something else under
200
- that name, which is why `Retry.new(client)` and `Sequence.new(client, fake)`
201
- raised `ArgumentError`. They work now.
202
-
203
- The one-shot sugar moved to `run`:
204
-
205
- ```ruby
206
- client.execute!("query { … }", id: "1") # before
207
- client.run!("query { }", id: "1") # after (and #run for the envelope)
208
-
209
- GraphWeaver.execute(source, query, **vars) # before
210
- GraphWeaver.run(source, query, **vars) # after
211
- ```
212
-
213
- **This one is worth grepping for.** `Client#execute` still exists, so a stale
214
- call fails at runtime rather than at typecheck as do `GraphWeaver.execute`
215
- and `GraphWeaver.reset_scalars!`, which are simply gone and will not be flagged
216
- until the RBI is regenerated (step 2): `rg '\.execute!?\(' --type ruby`
217
- and check each hit is passing `variables:` rather than loose kwargs.
218
-
219
- A generated module takes its per-call client as a **keyword**:
220
-
221
- ```ruby
222
- PersonQuery.execute(some_client, id: "1") # before
223
- PersonQuery.execute(client: some_client, id: "1") # after
224
- ```
225
-
226
- `GraphWeaver.resolve_transport` is gone; nothing needs unwrapping any more.
227
-
228
- ### Path settings are lists
229
-
230
- `queries_paths`, `generated_paths`, `fragments_paths` — every entry is read.
231
- Assigning a String still works, so the change is the name:
232
-
233
- ```ruby
234
- GraphWeaver.queries_path = "app/graphql/queries" # before
235
- GraphWeaver.queries_paths = "app/graphql/queries" # after
236
- ```
237
-
238
- `schema_path` stays singular: one run reads one schema.
239
-
240
- ### One reset
241
-
242
- `GraphWeaver.reset_registrations!` is the clean slate between tests, or between
243
- generations for different schemas. The four
244
- narrow ones moved to where they live:
245
-
246
- ```ruby
247
- GraphWeaver.reset_scalars! # before
248
- GraphWeaver::Codegen.reset_scalars! # after (also reset_enums!, clear_scalars!,
249
- # reset_type_helpers!)
250
- ```
251
-
252
- ### Generated names come from the response key, not the type
253
-
254
- Nested structs used to be named for the GraphQL *type* they were cast from;
255
- they are now named for the **response key that selects them**, camelized, and
256
- the constant path reads like the query. The typechecker finds the call sites in
257
- a `# typed: true` file (an unresolved constant is an `srb tc` error); in a
258
- `# typed: false` file it is `uninitialized constant` at runtime, so grep for
259
- `::Result::` there.
260
-
261
- | selection | before (type) | after (key) |
262
- |---|---|---|
263
- | `person { pets { name } }` | `PersonQuery::Result::Person::Pet` | `PersonQuery::Result::Person::Pets` |
264
- | `payrollRisk { score }` | `…::Result::RiskAssessment` | `…::Result::PayrollRisk` |
265
- | `_entities(…) { ... on Product { … } }` | `…::Result::Product` | `…::Result::Entities::Product` |
266
-
267
- The key is used verbatim no pluralization, so a list field `pets` is `Pets`.
268
- To pick the name yourself, alias the field: `pet: pets { name }` generates
269
- `Pet`. Union and interface members keep their type-condition names, nested in
270
- the container the field names. The payoff is that adding, removing or
271
- reordering an unrelated selection can never rename a struct you reference.
272
-
273
- **Enums moved out of the result tree.** Every schema enum a query touches is one
274
- Ruby type in the shared module, `GraphQLTypes::Species`, so a value read from
275
- one query hands straight into another's variable. A query module aliases the
276
- enums its *variables* use (`AddPetMutation::Species` still works); an enum
277
- reached only through a result is no longer nested under the struct that
278
- carries it `SearchQuery::Result::Search::Species` is `GraphQLTypes::Species`.
279
-
280
- ### Smaller renames
281
-
282
- | before | after |
283
- |---|---|
284
- | `Testing.config.auto_fake = true` | `Testing.config.default_mode = :fake` |
285
- | a mutation's `…Query` module | `…Mutation` |
286
- | `graphql: :none` (rspec tag) | `graphql: false` |
287
-
288
- **The shared types module was three, and is now one.** `GraphQLInputs`,
289
- `GraphQLEnums` and `GraphQLUnions` are all `GraphQLTypes`, and the files move
290
- with them `generated/inputs/` becomes `generated/types/`. The three settings
291
- that named them (`inputs_module=`, `enums_module=`, `unions_module=`) are one
292
- `types_module=`. Regenerating writes the new tree; delete the old directory,
293
- which pruning leaves behind empty.
294
-
295
- If your specs run one schema class in-process while your client points at a
296
- different API, name it — per example, since a federated suite runs more than
297
- one:
298
-
299
- ```ruby
300
- graphql_in_process(MySchema) # in the example
301
- GraphWeaver::Testing.config.schema = MySchema # or once, for the whole suite
302
- ```
303
-
304
- ### Registering from Rails
305
-
306
- A registration naming one of your own constants belongs in a `to_prepare` block
307
- — the same place the in-process client goes, and for the same reason:
308
- autoloading is set up after `config/initializers` run.
309
-
310
- ```ruby
311
- Rails.application.config.to_prepare do
312
- GraphWeaver.register_enum("Species", PetKind, fallback: PetKind::Unknown)
313
- GraphWeaver.extend_type("Pet", PetHelpers)
314
- end
315
- ```
316
-
317
- Generation depends on `:environment`, which runs `to_prepare` too, so the
318
- registration is in place before it emits — and at boot the generated files
319
- load from a `to_prepare` block of their own, after yours.
320
-
321
- ### If you use the federation router
322
-
323
- Detection only sees *loaded* schema classes, and Rails does not eager load for
324
- rake or in the default test environment. Both are one line:
325
-
326
- ```ruby
327
- config.eager_load = true # config/environments/test.rb
328
- config.rake_eager_load = true # config/application.rb
329
- ```
330
-
331
- Without them the `federation:*` tasks silently see nothing — and
332
- `federation:diff` now **fails** rather than reporting a green "matches" over
333
- zero subgraphs.
124
+ The first one reaches every Rails app that never configured logging, and it is the
125
+ only one here that shows up in production rather than in your code.
126
+
127
+ - **A Rails app logs one line per GraphQL call, and emits one notification.** The
128
+ railtie sets `GraphWeaver.instrumenter` to the `ActiveSupport::Notifications`
129
+ adapter and attaches `GraphWeaver::LogSubscriber`, so an app that configured
130
+ neither gets `GraphWeaver billing/InvoicesQuery (12.3ms) ok` at **info** — the
131
+ query and variables stay at debug. **To opt out, set `GraphWeaver.logger = nil`
132
+ or `GraphWeaver.instrumenter = nil` in `config/initializers`**, which now takes
133
+ effect (an app that worked around that with `config.after_initialize` can drop
134
+ it). In-process calls are in scope too: a bare schema class in a client slot goes
135
+ through the same wrapper, so it produces events and log lines where it produced
136
+ none. See [logging](logging.md).
137
+ - **A `Retry` in front of a gateway starts actually retrying.** It read only the
138
+ failures that *raised*, and Apollo Router answers everything it decides itself
139
+ with a GraphQL errors body, so `retries: 3` made one attempt. A response retries
140
+ now when its status is one a `ServerError` retries on (5xx, 408, 429), or when
141
+ its error codes are named in `retry_codes:`. **This is real traffic you weren't
142
+ sending** — if the inert policy was what you wanted, `retries: 0`.
143
+ - **A task that can't honour `SUPERGRAPH=` refuses instead of ignoring it.** The
144
+ flag reaches the `federation:*` tasks and nothing else, so
145
+ `SUPERGRAPH=public.graphql rake graph_weaver:queries:check` used to report every
146
+ query valid against a supergraph missing fields they select. **A CI step that
147
+ passes it to `generate`, `verify` or `queries:check` goes red**; drop the flag,
148
+ or declare the supergraph on a graph.
149
+ - **`InputError#field` names the input field, not the variable.** It is `#path`'s
150
+ last *named* segment the slot that actually held the bad value where it used
151
+ to be re-branded with the *variable* name. Nothing raises; the value just differs
152
+ once a refusal happens inside an input object. **Read `error.path.first` wherever
153
+ you wanted the variable.** An index is a position rather than a field, so
154
+ `execute(ids: [1, 2, "x"])` reports `#path` `["ids", 2]` and `#field` `"ids"`.
155
+ - **The instrumentation payload's `:status` is a Symbol, and the HTTP status moved
156
+ to `:http_status`.** `:status` is `:ok`, `:errors` or `:failed` — a 200 carrying
157
+ errors is not a success, and only a symbol says that on both sides of the seam.
158
+ Nothing raises: a subscriber comparing it to an Integer just stops matching. **A
159
+ subscriber that branched on a 4xx/5xx reads `:http_status` now**, which is nil
160
+ in-process. The whole payload is a documented contract —
161
+ [logging](logging.md#the-payload).
162
+ - **`respond_to?` on a result struct no longer answers true for a name that doesn't
163
+ exist.** It used to say true for any near miss, which broke the standard
164
+ duck-typing guard. **A branch that read the old answer now takes the other path**,
165
+ and `struct.method(:nmae)` raises Ruby's bare `NameError`; `struct.nmae` still
166
+ hints.
167
+ - **A `graphql:` tag reaches a module generated with `client:`.** The baked client
168
+ used to sit above the slot a tag swaps, so a bound module ran against its real
169
+ endpoint under `graphql: :fake`. **If a spec relied on that**, pass `client:` on
170
+ the call, set `MyQuery.client =`, or tag the example `graphql: :live`.
171
+ - **`config.context`, `config.schema` and `config.router` are suite setup.**
172
+ Setting any of the three once an example is running refuses, naming the
173
+ per-example helper (`graphql_context`, `graphql_fake(schema:)`,
174
+ `graphql_router(fake:)`). The tag builds an example's clients in a `before` hook
175
+ of its own, which rspec runs ahead of any group `before`, so a set there was read
176
+ too late and silently changed nothing. **Move it to an `around`, or to
177
+ `GraphWeaver::Testing.configure` in the spec helper.**
178
+ - **`result.to_json` is real JSON, and it is the wire shape.** It used to be Ruby's
179
+ `Object#to_json` the `#inspect` string, quoted while under Rails
180
+ `render json: result` shipped the *Ruby* prop names. Both now produce the response
181
+ keys, each leaf back through its scalar registration's `serialize:`, so
182
+ `Result.from_h(JSON.parse(result.to_json)) == result`. `#to_h` is unchanged and
183
+ still the Ruby view. **Anything that parsed the old output is reading something
184
+ different now** and `as_json` is emitted code, so a struct generated by 0.6.1
185
+ raises `GraphWeaver::Error` naming this until you regenerate.
186
+ - **A schema dump introspected through a credentialed url still holds the token.**
187
+ The provenance stamp wrote the transport's url verbatim. It records the endpoint
188
+ bare now userinfo and any query parameter `filter_parameters` filters are
189
+ dropped — and re-introspection still authenticates from the dump's `auth_env`.
190
+ **Run `rake graph_weaver:schema:refresh` once, and rotate the token if that file
191
+ was ever pushed.**
192
+ - **`verify` fails when the dump has fallen behind the schema class it was built
193
+ from.** For an app that serves its own schema the dump is an artifact derived
194
+ from code in the same repo, so `generate` and `verify` both called a tree up to
195
+ date while the live resolvers had already moved. **A dump you deliberately keep
196
+ behind your own schema is a red gate now** — `rake graph_weaver:schema:refresh`,
197
+ or ask about no dump at all with `verify_generated!(schema:)`. It costs one
198
+ in-process introspection per graph and never a network call.
199
+ - **`@oneOf` starts being enforced if your dump is `.json`.** graphql-ruby's
200
+ introspection query omits `isOneOf` unless asked, so every dump this gem had
201
+ written said "not @oneOf" for every input object. **Regenerate and the emitted
202
+ `ONE_OF` starts refusing calls that set two fields** which your server was
203
+ refusing all along, so the failure moves from the wire into `execute`. SDL dumps,
204
+ inline SDL and a live class were always correct.
205
+ - **A fake pin is told from an option by a schema lookup, not by casing.** A
206
+ lowercase type could not be pinned at all (`graphql_fake("pokemon_v2_pokemon" =>
207
+ …)` against a Hasura API); those pins work now. The other side of it: **a keyword
208
+ that is a near-miss for a pin (`Persn: "Ada"`) raises `ArgumentError` from the
209
+ fake** rather than `GraphWeaver::Error` from the override check — the same key
210
+ written in the leading positional hash is unchanged, and is the spelling for a
211
+ schema whose vocabulary collides with an option name.
212
+ - **A `DateTime` given for a `Date` variable is refused.** `DateTime` is a `Date`
213
+ to Ruby, so it used to pass the cast untouched and go on the wire as a full
214
+ timestamp where the schema said `ISO8601Date`. **Pass `.to_date`.** The pairings
215
+ that already raised — a `Time` for a date, a `Date` for a timestamp now raise a
216
+ branded `InputError` rather than Ruby's *"no implicit conversion of Time into
217
+ String"*, and a `DateTime` or `Time.zone.now` for a *timestamp* converts
218
+ losslessly where it used to raise.
219
+ - **A `cast:` of your own gets the same guard and the same verdict.** A registration
220
+ like `register_scalar("Date", Date, cast: :iso8601, serialize: :iso8601)` emitted
221
+ a bare `value.is_a?(Date)` pass-through, so a `DateTime` went by untouched and
222
+ your `serialize:` wrote a full timestamp into a date field — **pass `.to_date`
223
+ there too**. Anything else wrong used to arrive as Ruby's own sentence under
224
+ `kind: :unparseable`; the verdict is the library's now and splits the way Ruby
225
+ does a `TypeError` from a codec reads `expected a Date, got 5` under
226
+ `:type_mismatch`, an `ArgumentError` keeps the parser's words under
227
+ `:unparseable`. **`#details[:type]` is the GraphQL type now, never a Ruby class.**
228
+ A spec matching the old message, or branching on `:unparseable` for a wrong class,
229
+ needs updating and the guard is emitted into your generated files, so a
230
+ checked-in one keeps the old behavior until you regenerate.
231
+ - **A field whose name a struct already answers to now generates as `name_`.**
232
+ `class` becomes `class_`, and so on for `hash`, `display`, `to_json`, `each` and
233
+ (on an input) `supplied`. Only the Ruby name moves: the wire keeps the schema's
234
+ spelling in both directions, so a refusal on that field still reports `#path`
235
+ `["class"]`, and an input struct's `#to_h` is still the wire hash. A key you
236
+ aliased in the query to get past the old refusal still generates from that alias
237
+ **drop the alias and regenerate** if you want the field's own name back. The
238
+ names that take an underscore are a list the gem owns (`T::Struct` and `Object`'s
239
+ public instance methods, the hooks Ruby and Rails call on an object, and the
240
+ gem's own mixins) rather than whatever the generating process happened to have
241
+ loaded, so a few more names move than 0.6.1 touched; Kernel's *private* methods
242
+ are not on it, so `format`, `select`, `open` and `load` stay ordinary props. A
243
+ federation `@key` on such a field follows the same rule instead of being refused
244
+ the kwarg takes the underscore and `"class"` still goes on the wire, so
245
+ **regenerate if a `@key` of yours names one**. Generated source marks each rename
246
+ on the line above the prop, so **read the regenerate diff**.
247
+ - **If you adopt `GraphWeaver.graph`, every queries directory needs a graph.**
248
+ Declaring one replaces the implicit graph your top-level settings describe, so a
249
+ graph declared beside an existing `app/graphql/queries` used to leave that
250
+ directory unread. `generate!`, `verify_generated!` and `check_queries` refuse
251
+ now, naming the stray files. **Name the directory in a graph, declare a graph for
252
+ it, or delete it.** An app that declares no graph is unaffected.
253
+ - **A `client` that isn't a constant is refused at generation.** Its value is
254
+ spelled into every module the graph generates, so `client` given an endpoint url
255
+ emitted a file that doesn't parse, from a run that reported success. Declare the
256
+ constant and name it `CLIENT = GraphWeaver.new(url)`, then `client "CLIENT"`.
257
+ - **A `cast:` or `serialize:` proc that returns a value is refused at
258
+ registration.** A proc there builds *source* for the generated file, so
259
+ `cast: ->(v) { v.to_sym }` interpolated to nothing and every response failed far
260
+ from the registration. It is probed once when registered now: return the source
261
+ (`cast: ->(v) { "Money.parse(#{v})" }`) or name a method instead.
262
+ - **`config.graph_weaver` refuses a key the railtie doesn't read**, at boot. It
263
+ takes `watch`; `config.graph_weaver.queries_paths = …` was taken silently and did
264
+ nothing. **Move it to `GraphWeaver.queries_paths =`.**
265
+ - **A router's `fake:` refuses `seed:`**, as `graphql_fake` already did. A router is
266
+ built once for the suite, so a seed there would pin every example to one run —
267
+ `rspec --seed 1234` reproduces the fabricated data along with the test order, and
268
+ `GraphWeaver::Testing.config.seed` is the override for a harness that isn't rspec.
269
+ - **Check that your `require "graph_weaver/rspec"` actually runs.** The old setup
270
+ put it in `spec/support/graph_weaver.rb`, and rspec-rails ships the `spec/support`
271
+ glob **commented out** so if you never uncommented it, the tag did nothing and
272
+ every `graphql: :fake` example has been hitting the real client.
273
+ `rails g graph_weaver:install` writes the line into `spec/rails_helper.rb`
274
+ instead; **move yours there** if the glob isn't live.
275
+ - **`graphql: :wire`, if you adopt it, needs webmock *enabled*** — `require
276
+ "webmock/rspec"` in the spec helper. Having it in the Gemfile is not enough:
277
+ `Bundler.require` loads webmock without installing its adapters, and the tag
278
+ refuses before the first request.
279
+ - **Regenerate**, as ever. Generated modules carry a private `GRAPH` naming the
280
+ graph they were generated from, and a
281
+ [multi-schema](getting_started.md#more-than-one-schema) app whose modules predate
282
+ it refuses rather than guessing which schema a module belongs to. A generated
283
+ `execute` also makes its request through the gem now, which is what lets an event
284
+ name the graph; 0.6.1's modules keep working, but `verify` reports the tree out of
285
+ date until you regenerate. Result structs also gained `==`/`eql?`/`hash`,
286
+ `deconstruct_keys`, `#to_h` and `#as_json`.