lighthouse-graphql 0.4.0 → 0.5.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -1
  3. data/README.md +8 -5
  4. data/docs/configuration.md +26 -2
  5. data/docs/directives.md +287 -4
  6. data/docs/generators.md +82 -0
  7. data/docs/getting-started.md +26 -30
  8. data/docs/relationships.md +42 -0
  9. data/docs/relay.md +137 -0
  10. data/docs/resolvers.md +261 -22
  11. data/docs/security.md +17 -0
  12. data/docs/types.md +219 -0
  13. data/docs/uploads.md +80 -0
  14. data/lib/generators/lighthouse/base_classes.rb +32 -49
  15. data/lib/generators/lighthouse/install/install_generator.rb +83 -24
  16. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
  17. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
  18. data/lib/lighthouse/async_mutation_job.rb +17 -0
  19. data/lib/lighthouse/complexity.rb +5 -4
  20. data/lib/lighthouse/config.rb +13 -2
  21. data/lib/lighthouse/controller.rb +27 -9
  22. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  23. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  24. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  25. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  26. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  27. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  28. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  29. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  30. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  31. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  32. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  33. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  34. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  35. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  36. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  38. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  39. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  40. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  41. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  43. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  44. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  45. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  46. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  47. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  48. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  49. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  50. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  51. data/lib/lighthouse/directives/definitions.rb +52 -0
  52. data/lib/lighthouse/directives/model_directive.rb +131 -0
  53. data/lib/lighthouse/directives/node_directive.rb +134 -0
  54. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  55. data/lib/lighthouse/directives/registry.rb +21 -21
  56. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  57. data/lib/lighthouse/enum_directive.rb +41 -0
  58. data/lib/lighthouse/field_resolver_stack.rb +53 -6
  59. data/lib/lighthouse/generated_resolvers.rb +13 -8
  60. data/lib/lighthouse/global_id.rb +31 -0
  61. data/lib/lighthouse/graphql/version.rb +1 -1
  62. data/lib/lighthouse/multipart_request.rb +43 -0
  63. data/lib/lighthouse/node_registry.rb +68 -0
  64. data/lib/lighthouse/pagination.rb +3 -0
  65. data/lib/lighthouse/rb_lighthouse.rb +33 -0
  66. data/lib/lighthouse/relation_counts.rb +29 -0
  67. data/lib/lighthouse/resolve_info.rb +80 -0
  68. data/lib/lighthouse/resolver_bases.rb +9 -2
  69. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  70. data/lib/lighthouse/scalar_coercion.rb +72 -0
  71. data/lib/lighthouse/schema_cache.rb +2 -1
  72. data/lib/lighthouse/schema_generator.rb +11 -10
  73. data/lib/lighthouse/schema_implementation.rb +4 -8
  74. data/lib/lighthouse/support/environment.rb +13 -0
  75. data/lib/lighthouse/support/model_resolver.rb +7 -5
  76. data/lib/lighthouse/support/preload.rb +49 -0
  77. data/lib/lighthouse/type.rb +103 -0
  78. data/lib/lighthouse/type_resolution.rb +150 -0
  79. data/lib/lighthouse/upload.rb +26 -0
  80. data/lib/lighthouse/upload_store.rb +32 -0
  81. data/sig/lighthouse/directives.rbs +3 -2
  82. data/sig/lighthouse.rbs +18 -0
  83. metadata +38 -1
@@ -0,0 +1,82 @@
1
+ # Generators
2
+
3
+ Lighthouse builds on graphql-ruby, and so do its generators: it writes only what
4
+ graphql-ruby has no generator for, or what an SDL-first schema needs differently.
5
+ Every graphql-ruby app's base classes (`Types::BaseObject`, `Resolvers::BaseResolver`,
6
+ `Mutations::BaseMutation`, ...) come from graphql-ruby's own `graphql:install`.
7
+
8
+ ## `lighthouse:install`
9
+
10
+ ```shell
11
+ bin/rails generate lighthouse:install
12
+ ```
13
+
14
+ It runs graphql-ruby's install first
15
+ (`graphql:install --no-relay --skip-graphiql --skip-query-logs`), which writes:
16
+
17
+ - `app/graphql/types/base_*.rb`: `BaseObject`, `BaseField`, `BaseArgument`,
18
+ `BaseInterface`, `BaseUnion`, `BaseScalar`, `BaseEnum`, `BaseInputObject`;
19
+ - `app/graphql/resolvers/base_resolver.rb` and `app/graphql/mutations/base_mutation.rb`;
20
+ - its schema class, `Types::QueryType`, `Types::MutationType` and controller, and the
21
+ `POST /graphql` route.
22
+
23
+ Then Lighthouse puts its own on top of what graphql-ruby wrote in that run:
24
+
25
+ | File | What Lighthouse does |
26
+ | --- | --- |
27
+ | `app/graphql/app_schema.rb` | Replaced by the schema built from the SDL, naming every base class in `base_types` |
28
+ | `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` |
29
+ | `app/graphql/types/query_type.rb`, `mutation_type.rb` | Removed: the SDL declares the root types |
30
+ | `app/graphql/mutations/base_mutation.rb` | Made a `GraphQL::Schema::Mutation`: the SDL declares a mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes a single `input:` |
31
+ | `app/graphql/subscriptions/base_subscription.rb` | Written: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
32
+ | `config/initializers/lighthouse.rb` | Written: every [configuration](configuration.md) option, commented out |
33
+ | `graphql/schema.graphql` | A starter schema, unless the folder holds `.graphql` files already |
34
+ | `graphql.config.yml` | IDE configuration, and `.gitignore` entries for the [IDE helper](getting-started.md#ide-support) files |
35
+
36
+ | Option | Effect |
37
+ | --- | --- |
38
+ | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
39
+ | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
40
+ | `--skip-controller` | No controller or route (graphql-ruby's are removed too) — for apps that serve GraphQL themselves |
41
+ | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
42
+ | `--force` | Replace a schema class or controller the app had before |
43
+
44
+ Rerunning it is safe: a file the app has is kept, and the route and `.gitignore`
45
+ entries are added once. In an app that already had a graphql-ruby schema class or
46
+ controller, those are kept (Lighthouse replaces only what graphql-ruby wrote in the
47
+ same run), and graphql-ruby's install points that schema at the root types it
48
+ wrote: have the controller call `AppSchema.execute(...)` to serve Lighthouse's
49
+ schema, or pass `--force`.
50
+
51
+ ## Lighthouse's generators
52
+
53
+ For the classes graphql-ruby has no generator for, or none that fits an SDL-first
54
+ schema. Each writes into the first namespace configured for its kind (or
55
+ `--namespace=…`); namespaced names nest modules (`Blog::Publish`).
56
+
57
+ | Generator | Writes |
58
+ | --- | --- |
59
+ | `lighthouse:query LatestPosts` | `Resolvers::LatestPosts < BaseResolver`, which `Query.latestPosts` resolves to by convention |
60
+ | `lighthouse:mutation PublishPost` | `Mutations::PublishPost < BaseMutation`, for `Mutation.publishPost` |
61
+ | `lighthouse:subscription PostUpdated` | `Subscriptions::PostUpdated < BaseSubscription`, with its hooks (and the base, when missing) |
62
+ | `lighthouse:builder Published` | `Builders::Published`, for `@builder(class: "Published")` |
63
+ | `lighthouse:validator CreatePostInput` | `Validators::CreatePostInputValidator`, a `Dry::Validation::Contract` |
64
+
65
+ The class's arguments and return type are the SDL's, so it declares neither. When
66
+ the base class is missing (an app that never ran an install, or a namespace of its
67
+ own such as `Api::Resolvers`), the generator says so: `lighthouse:install` writes it
68
+ through graphql-ruby, or write `Api::Resolvers::BaseResolver` yourself.
69
+
70
+ ## graphql-ruby's generators in a Lighthouse app
71
+
72
+ | Generator | In a Lighthouse app |
73
+ | --- | --- |
74
+ | `graphql:install` | Run by `lighthouse:install`. On its own, it writes a code-first schema, root types and a `RelayClassicMutation` base: run `lighthouse:install` instead |
75
+ | `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): add `include Lighthouse::GraphQL::Type`, then methods. The `field` lines it writes (from the model's columns, when there is one) are ignored, as the SDL declares the fields: delete them |
76
+ | `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): add `include Lighthouse::GraphQL::Type`; its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
77
+ | `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add `include Lighthouse::GraphQL::Type` and a `resolve_type`. Its `possible_types` and `field` lines are ignored |
78
+ | `graphql:enum`, `graphql:input` | Not needed: the SDL declares enums and inputs ([`@enum`](directives.md#enum) gives a value an internal one) |
79
+ | `graphql:mutation`, `graphql:mutation_create` / `_update` / `_delete` | **Not ready**: they add a field to `Types::MutationType`, which a Lighthouse app has none of (they write it again), and write a `RelayClassicMutation` taking `input:`. Use `lighthouse:mutation`, or [`@create`, `@update`, `@delete`](mutations.md) |
80
+ | `graphql:relay` | Not needed: [`@node`, `@globalId`](relay.md) and `@paginate(type: CONNECTION)` |
81
+ | `graphql:loader` | A GraphQL::Batch loader, which your own resolver classes may use; Lighthouse's directives batch through graphql-ruby's Dataloader instead |
82
+ | `graphql:detailed_trace` | **Not ready**: it edits a `class AppSchema < GraphQL::Schema`, which Lighthouse's `AppSchema = get_schema(...)` is not. Add its migration and `AppSchema.use(GraphQL::Tracing::DetailedTrace)` by hand |
@@ -24,32 +24,24 @@ a hard dependency of Lighthouse so non-federated apps stay lean.
24
24
  bin/rails generate lighthouse:install
25
25
  ```
26
26
 
27
- does steps 1–5 below for you:
27
+ does steps 1–5 below for you. It runs graphql-ruby's `graphql:install` first, for
28
+ the base classes every graphql-ruby app has (`Types::Base*`, `Resolvers::BaseResolver`,
29
+ `Mutations::BaseMutation`), then puts Lighthouse's own on top:
28
30
 
29
31
  | File | What it is |
30
32
  | --- | --- |
31
33
  | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out at its default |
32
34
  | `graphql/schema.graphql` | A starter schema (`user` via `@find`, `users` via `@paginate`) that assumes a `User` model — skipped when the folder already holds `.graphql` files |
33
- | `app/graphql/app_schema.rb` | The executable schema, with `GraphQL::Dataloader` enabled |
35
+ | `app/graphql/app_schema.rb` | The executable schema, built from the SDL, with `GraphQL::Dataloader` enabled and the base classes in `base_types` |
36
+ | `app/graphql/{types,resolvers,mutations,subscriptions}/base_*.rb` | graphql-ruby's base classes, and Lighthouse's `Subscriptions::BaseSubscription` |
34
37
  | `app/controllers/graphql_controller.rb` | The endpoint, plus a `post '/graphql'` route |
35
38
  | `graphql.config.yml` | IDE configuration — see [IDE support](#ide-support) |
36
39
 
37
- | Option | Effect |
38
- | --- | --- |
39
- | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
40
- | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
41
- | `--skip-controller` | No controller or route — for apps that already serve GraphQL |
42
- | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
43
-
44
- Rerunning it is safe: the route and `.gitignore` entries are only added once, and
45
- an existing schema class or controller is kept (pass `--force` to replace it).
46
-
47
- It coexists with graphql-ruby's own tooling. The generators live under
48
- `lighthouse:` and the rake task is `lighthouse:ide_helper`, next to graphql-ruby's
49
- `graphql:*` generators and `graphql:schema:*` tasks. `graphql:install` writes the
50
- same controller and route; whichever runs second finds the route already in place,
51
- and Lighthouse keeps a controller graphql-ruby generated. In that case, have the
52
- controller call `AppSchema.execute(...)` to serve the Lighthouse schema.
40
+ `--schema=ApiSchema`, `--sdl-folder=app/schema`, `--skip-controller` and
41
+ `--skip-ide` change what it writes. Rerunning it is safe, and a schema class or
42
+ controller the app had before is kept (pass `--force` to replace it).
43
+ [generators.md](generators.md) has the details, every generator Lighthouse has, and
44
+ which of graphql-ruby's `graphql:*` generators fit a Lighthouse app.
53
45
 
54
46
  ## 1. Write your schema (SDL)
55
47
 
@@ -79,19 +71,22 @@ field's return type name (`User`), or you can be explicit with
79
71
 
80
72
  Lighthouse builds on graphql-ruby, and follows its convention: each kind of class
81
73
  inherits from a base class in its own namespace, where what they all share goes
82
- once. `rails g lighthouse:install` writes the bases of the classes that resolve
83
- root fields:
74
+ once. graphql-ruby's `graphql:install` writes them, and `lighthouse:install` runs it;
75
+ by hand, run it yourself, then add the subscription base, which graphql-ruby has
76
+ none of, and make the mutation base a `GraphQL::Schema::Mutation` (the SDL declares a
77
+ mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes one `input:`):
84
78
 
85
- ```ruby
86
- # app/graphql/resolvers/base_resolver.rb — Query fields (Resolvers::LatestPosts)
87
- module Resolvers
88
- class BaseResolver < GraphQL::Schema::Resolver
89
- end
90
- end
79
+ ```shell
80
+ bin/rails generate graphql:install --no-relay --skip-graphiql
81
+ ```
91
82
 
83
+ ```ruby
92
84
  # app/graphql/mutations/base_mutation.rb — Mutation fields (Mutations::PublishPost)
93
85
  module Mutations
94
- class BaseMutation < GraphQL::Schema::Mutation
86
+ class BaseMutation < GraphQL::Schema::Mutation # graphql-ruby writes RelayClassicMutation
87
+ argument_class Types::BaseArgument
88
+ field_class Types::BaseField
89
+ object_class Types::BaseObject
95
90
  end
96
91
  end
97
92
 
@@ -103,14 +98,15 @@ module Subscriptions
103
98
  end
104
99
  ```
105
100
 
106
- The SDL's types can have graphql-ruby base classes too, as `graphql:install`
107
- writes them: the place to mix in Apollo Federation or custom field and argument
108
- behavior.
101
+ Delete the `Types::QueryType` and `Types::MutationType` it writes: the SDL declares
102
+ the root types. The `Types::Base*` classes are the SDL's types' bases: the place to
103
+ mix in Apollo Federation or custom field and argument behavior.
109
104
 
110
105
  ```ruby
111
106
  # app/graphql/types/base_object.rb
112
107
  module Types
113
108
  class BaseObject < GraphQL::Schema::Object
109
+ field_class Types::BaseField
114
110
  # include ApolloFederation::Object # if you use federation
115
111
  end
116
112
  end
@@ -31,6 +31,10 @@ The `morph*` names are PHP Lighthouse's. ActiveRecord resolves polymorphic
31
31
  associations the same way as any other, so they behave exactly like their
32
32
  non-polymorphic counterparts. Use whichever reads better.
33
33
 
34
+ `@hasMany` and `@hasOne` resolve `has_many` / `has_one :through` associations as
35
+ they are. PHP Lighthouse's `@hasManyThrough` and `@hasOneThrough` work too, as
36
+ aliases, so an SDL written for it builds unchanged.
37
+
34
38
  ## Avoiding the N+1 problem
35
39
 
36
40
  The relationship directives load through graphql-ruby's **Dataloader** and
@@ -50,6 +54,44 @@ the Dataloader, the directives still work, but they load one parent at a time.
50
54
  A field with `scopes:` or a filter argument the client supplied runs its own query
51
55
  per parent, because each parent's query differs.
52
56
 
57
+ ## Eager loading for other fields
58
+
59
+ The relationship directives load their own association. Three directives load for
60
+ the fields that only read one: a method, a type class method, a field read off
61
+ the model. Each one loads across every parent in the response at once, through the
62
+ Dataloader:
63
+
64
+ - `@with(relation: "tasks")` loads a relation of the parent before the field
65
+ resolves. `user.tasks` is then in memory for the method that reads it.
66
+ - `@withCount(relation: "posts")` counts a relation for every parent in one
67
+ `GROUP BY` query, and keeps each count on its parent as `<relation>_count`, as
68
+ Laravel's `withCount` does. It is not the field's value: the field's resolver,
69
+ or the default reader of a `postsCount` field, reads `user.posts_count`.
70
+ - `@lazyLoad(relations: ["tags"])` loads relations of the records the field
71
+ returns: a list, a relation, a `@paginate` page. On a `@hasMany`, the records of
72
+ every parent load their relations together.
73
+
74
+ ```graphql
75
+ type User {
76
+ latestPostTitle: String @with(relation: "posts") @method
77
+ usage: String! @withCount(relation: "posts", scopes: ["published"]) @method
78
+ posts: [Post!]! @hasMany @lazyLoad(relations: ["tags"])
79
+ }
80
+ ```
81
+
82
+ ```ruby
83
+ class User < ApplicationRecord
84
+ def latest_post_title = posts.max_by(&:created_at)&.title
85
+ def usage = "#{posts_count} published posts"
86
+ end
87
+ ```
88
+
89
+ Relations may be nested with dots (`"posts.comments"`), and `scopes` narrow what is
90
+ loaded or counted, as model scopes. A count loads in one query when the database has
91
+ window functions and the relation links its parent through one column, as a
92
+ [paginated relation](#paginating-many-parents-at-once) does; otherwise once per
93
+ parent.
94
+
53
95
  ## Lists, and paginating them
54
96
 
55
97
  Collection directives return a **plain list**, as in PHP Lighthouse:
data/docs/relay.md ADDED
@@ -0,0 +1,137 @@
1
+ # Relay: Global Object Identification
2
+
3
+ Relay clients refetch any object by one opaque id, through a root `node` field.
4
+ Lighthouse gives a schema that, as [PHP Lighthouse](https://lighthouse-php.com/master/digging-deeper/relay.html)
5
+ does, with two directives: `@globalId` encodes and decodes the ids, and `@node`
6
+ makes a type findable by them. For Relay's cursor pagination, see
7
+ [`@paginate(type: CONNECTION)`](directives.md#paginate).
8
+
9
+ ## Global ids
10
+
11
+ `@globalId` on a field encodes its value with the type's name:
12
+
13
+ ```graphql
14
+ type User {
15
+ id: ID! @globalId
16
+ name: String
17
+ }
18
+ ```
19
+
20
+ `{ user(id: 1) { id } }` answers `"VXNlcjox"`, the base64 of `User:1`.
21
+
22
+ On an argument, or an input field, `@globalId` decodes what the client sent before
23
+ the field resolves, so every resolver (a directive, `@field`, a `Resolvers::` class)
24
+ gets the decoded value. `decode:` says which part:
25
+
26
+ | `decode:` | The resolver gets |
27
+ | --- | --- |
28
+ | `ARRAY` (default) | `["User", "1"]` |
29
+ | `TYPE` | `"User"` |
30
+ | `ID` | `"1"` |
31
+
32
+ ```graphql
33
+ type Query {
34
+ user(id: ID! @globalId(decode: ID) @eq): User @find
35
+ }
36
+
37
+ type Mutation {
38
+ deleteNode(id: ID! @globalId): Node
39
+ }
40
+ ```
41
+
42
+ Lists of ids decode item by item. A value that is not a global id makes the field
43
+ fail with `"…" is not a valid global id.`
44
+
45
+ ## Nodes
46
+
47
+ `@node` registers a type for refetching:
48
+
49
+ ```graphql
50
+ type User @node {
51
+ id: ID! @globalId
52
+ name: String
53
+ }
54
+ ```
55
+
56
+ and adds what Relay expects, leaving alone whatever the SDL declares itself:
57
+
58
+ ```graphql
59
+ "Any object implementing this type can be found by ID through `Query.node`."
60
+ interface Node {
61
+ "Global identifier that can be used to resolve any Node implementation."
62
+ id: ID!
63
+ }
64
+
65
+ type User implements Node @node { ... }
66
+
67
+ type Query {
68
+ "Fetches an object given its globally unique `ID`."
69
+ node(id: ID! @globalId): Node
70
+ }
71
+ ```
72
+
73
+ ```graphql
74
+ {
75
+ node(id: "VXNlcjox") {
76
+ id
77
+ ... on User { name }
78
+ }
79
+ }
80
+ ```
81
+
82
+ The type keeps its own `id: ID! @globalId`, which encodes the ids `node` decodes.
83
+ `node` answers `null` for an id with no object, and an error for the id of a type
84
+ without `@node`.
85
+
86
+ ### Where the object comes from
87
+
88
+ 1. `@node(resolver: "Countries@by_code")` names a class, and a method after the `@`
89
+ (`call` without one). It gets the decoded id and the context, and returns any
90
+ object, a Hash included:
91
+
92
+ ```graphql
93
+ type Country @node(resolver: "Countries@by_code") {
94
+ id: ID! @globalId
95
+ name: String!
96
+ }
97
+ ```
98
+
99
+ ```ruby
100
+ class Resolvers::Countries
101
+ COUNTRIES = { 'DE' => { id: 'DE', name: 'Germany' } }.freeze
102
+
103
+ def self.by_code(id, _context) = COUNTRIES[id]
104
+ end
105
+ ```
106
+
107
+ The class is looked up as `@field` looks one up on a type: in the resolvers
108
+ namespace, then in [`resolver_namespaces`](configuration.md#resolver_namespaces).
109
+ 2. `@node(model: "Account")` names the model.
110
+ 3. Otherwise, the type's [`@model`](directives.md#model), or the model named
111
+ after the type.
112
+
113
+ A model's record is found by its primary key, through graphql-ruby's Dataloader, so
114
+ several `node` fields in one query load together.
115
+
116
+ To give `Query.node` your own description or middleware, declare it, with the
117
+ registry Lighthouse uses:
118
+
119
+ ```graphql
120
+ type Query {
121
+ "This description is up to you."
122
+ node(id: ID! @globalId): Node
123
+ @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
124
+ @guard
125
+ }
126
+ ```
127
+
128
+ ## Your own encoding
129
+
130
+ [`global_id`](configuration.md#global_id) takes any object with
131
+ `encode(type, id)` and `decode(global_id)`, which returns `[type, id]`:
132
+
133
+ ```ruby
134
+ Lighthouse.configure do |config|
135
+ config.global_id = MyGlobalIds # e.g. signed, or a UUID lookup
136
+ end
137
+ ```