lighthouse-graphql 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3be850a4eb57c4d4fc6bd41103d5bb783482530f3fc0f3584aa00314de4e936
4
- data.tar.gz: 75ae5604d85d1549f159dc17a98932d7c4274074ea26b9a9ade38b4f8d8d6af6
3
+ metadata.gz: 844e3fc4942a637b4353427932e031c210ad52d4d425161a58f45e2e36f1d931
4
+ data.tar.gz: 9b0b05109d1fcac2f330b7266fe91eb502d521dabce399adfd98828137119468
5
5
  SHA512:
6
- metadata.gz: 62cafecf9c4deae7b974747a9de82c422ea1d58d50fa752d6c583fba20f6ff16b99db56496a9963d0740826803b89e730c3ede19fed937baf22fac7f954bd826
7
- data.tar.gz: fcd638c332e4f055afb9b28e7ac211162582878e578e5cb45c02a7d24dcf2ae70f7dd5342cdd477bc1f8a16b3cc1e6dd78e487791ee396044fefbe7aa95bea49
6
+ metadata.gz: ef2d22dd33355e36e3b40725cea55ebf5923a06ea2c94660228d8cc6b3a6e222bd7da6f42f796d52275a2130c1ebd9e54844f85febf31d66a7a4c759d310723e
7
+ data.tar.gz: 7334b3e2b4ecb745fea9a4f9c08860d8e5c6245654323b0161a0b97c367d8ea022a3b23f7bd7e5bbd8c4e8614939b16e82fa737e9e133e4c5cba555199088c1d
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-10-17
10
+
11
+ ### Added
12
+
13
+ - RBS signatures in `sig/`, shipped with the gem, for the parts an app implements or calls:
14
+ `Lighthouse::_Builder` (what a `@builder` class answers) and `_Validator`, every
15
+ `Lighthouse::Config` setting with the callables it takes (`_ErrorHandler`, `_UserReader`,
16
+ `_Authorizer`), the directive extension points (`DirectiveRegistry`, the contracts,
17
+ `BaseDirectiveResolver`, `Directives::Arguments::Base`), the entrypoints (`get_schema`,
18
+ `SchemaFactory`, `PersistedQueries`, `CacheControl`, `SchemaCache`, `IdeHelper`) and the
19
+ errors. `rbs collection` (and so Steep) loads them from the installed gem, as it does for any
20
+ gem that ships `sig/`; nothing changes at runtime.
21
+ CI validates them and runs the suite with every Lighthouse method checked against its
22
+ signature, so they cannot drift from the code.
23
+ - A documentation site, built from `docs/` with VuePress as PHP Lighthouse builds its own,
24
+ and published to GitHub Pages on every push to `main`:
25
+ <https://vaz-innovation.github.io/lighthouse-graphql/>.
26
+ - `docs/motivation.md`: where Lighthouse comes from, why the schema comes first, and why
27
+ directives work so well on Active Record — and when something else fits better.
28
+
9
29
  ## [0.2.0] - 2026-10-17
10
30
 
11
31
  ### Added
@@ -208,7 +228,8 @@ had been running inside a private monorepo as a path gem before being extracted.
208
228
  - Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
209
229
  `on_error`, and `DirectiveRegistry.register` for app-defined directives.
210
230
 
211
- [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...HEAD
231
+ [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...HEAD
232
+ [0.2.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...v0.2.1
212
233
  [0.2.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.1...v0.2.0
213
234
  [0.1.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.0...v0.1.1
214
235
  [0.1.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/releases/tag/v0.1.0
data/README.md CHANGED
@@ -39,7 +39,7 @@ paginate, walk a relationship, or read an attribute. Lighthouse — like its PHP
39
39
  inspiration — captures those patterns as **directives** you attach in the schema,
40
40
  and falls back to a sensible default reader for plain fields. You write Ruby only
41
41
  for the genuinely custom parts (`@field(resolver: "...")`), and your schema stays
42
- the single source of truth.
42
+ the single source of truth. [Motivation](docs/motivation.md) explains the idea in depth.
43
43
 
44
44
  ### Inspirations
45
45
 
@@ -142,8 +142,12 @@ are the library's end-to-end suite. See [example/README.md](example/README.md).
142
142
 
143
143
  ## Documentation
144
144
 
145
+ **Read it at [vaz-innovation.github.io/lighthouse-graphql](https://vaz-innovation.github.io/lighthouse-graphql/)**,
146
+ or as Markdown in [`docs/`](docs):
147
+
145
148
  | Guide | What it covers |
146
149
  | --- | --- |
150
+ | [Motivation](docs/motivation.md) | Where Lighthouse comes from, why the schema comes first, and why directives fit Active Record |
147
151
  | [Getting started](docs/getting-started.md) | Install, base types, building & serving the schema |
148
152
  | [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
149
153
  | [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne`, the `@morph*` variants, batching and `type:` pagination |
@@ -0,0 +1,184 @@
1
+ # Motivation
2
+
3
+ Lighthouse GraphQL Rails brings the ideas of [Lighthouse](https://lighthouse-php.com),
4
+ the GraphQL framework for Laravel, to Ruby on Rails. This page explains those ideas:
5
+ why the schema comes first, why behaviour lives in directives, and why that works so
6
+ well on top of an opinionated model layer like Active Record.
7
+
8
+ ## Where it comes from
9
+
10
+ Lighthouse ([nuwave/lighthouse](https://github.com/nuwave/lighthouse)) changed how many
11
+ Laravel teams build GraphQL APIs. Instead of a resolver class for every field, you write
12
+ the schema in GraphQL's own language and attach **directives** that say what a field
13
+ does: `@paginate` it, follow a `@hasMany` relationship, `@create` a model, check a policy
14
+ with `@canFind`. Lighthouse reads those directives and generates efficient queries against
15
+ Eloquent, Laravel's ORM.
16
+
17
+ The idea does not depend on PHP. It depends on two things: a schema written first, and a
18
+ model layer with strong conventions. Rails has the second in Active Record, and
19
+ [graphql-ruby](https://graphql-ruby.org) gives Ruby a mature GraphQL engine to build the
20
+ first on. Lighthouse GraphQL Rails puts them together. It keeps Lighthouse's directive
21
+ names, arguments and error formats wherever they make sense, so its documentation, its
22
+ examples and the experience of Laravel teams carry over.
23
+
24
+ ## The problem with writing every resolver
25
+
26
+ Most GraphQL fields in a Rails app do one of a handful of things: list records, find one,
27
+ paginate, follow an association, filter and sort, read an attribute, or create, update
28
+ and delete a record. Written by hand, each of these is a resolver. Each resolver is a
29
+ place where:
30
+
31
+ - **the API's shape hides in Ruby.** To know what `posts` accepts and returns, you read
32
+ a type class, its fields, their resolver methods and the scopes they call.
33
+ - **N+1 queries creep in.** Every association needs its own batch loader, and every
34
+ paginated association needs a separate count, page and cursor logic.
35
+ - **the same decisions are made again**, slightly differently each time: how pages are
36
+ numbered, what a cursor contains, how validation errors are reported, which filter
37
+ arguments are allowed.
38
+
39
+ With [graphql-ruby](https://graphql-ruby.org) alone, a list of published posts with their
40
+ author and comments, filterable by author, looks roughly like this:
41
+
42
+ ```ruby
43
+ class Types::PostType < Types::BaseObject
44
+ field :id, ID, null: false
45
+ field :title, String, null: false
46
+ field :author, Types::UserType, null: false
47
+ field :comments, Types::CommentType.connection_type, null: false
48
+
49
+ def author
50
+ dataloader.with(Sources::RecordById, User).load(object.author_id)
51
+ end
52
+
53
+ def comments
54
+ object.comments.order(:created_at) # one query per post
55
+ end
56
+ end
57
+
58
+ class Types::QueryType < Types::BaseObject
59
+ field :posts, Types::PostType.connection_type, null: false do
60
+ argument :author_id, ID, required: false
61
+ end
62
+
63
+ def posts(author_id: nil)
64
+ scope = Post.published.order(created_at: :desc)
65
+ author_id ? scope.where(author_id: author_id) : scope
66
+ end
67
+ end
68
+ ```
69
+
70
+ …plus the `Sources::RecordById` batch loader, and more code to batch the comments. With
71
+ Lighthouse, it is the schema:
72
+
73
+ ```graphql
74
+ type Query {
75
+ posts(authorId: ID @eq): [Post!]! @paginate(type: CONNECTION) @builder(class: "Published")
76
+ }
77
+
78
+ type Post {
79
+ id: ID!
80
+ title: String!
81
+ author: User! @belongsTo
82
+ comments: [Comment!]! @hasMany(type: CONNECTION, scopes: ["chronological"])
83
+ }
84
+ ```
85
+
86
+ The `author` of every post loads in one query, and so does every post's page of
87
+ comments, with one more for their totals if the client asks for `pageInfo { total }`. This isn't a
88
+ special case: it is how every relationship directive works.
89
+
90
+ ## Why the schema comes first
91
+
92
+ In a schema-first API, the `.graphql` files *are* the API's definition, not something
93
+ generated from Ruby classes:
94
+
95
+ - **It is a contract you can read.** Frontend developers, mobile developers and reviewers
96
+ read the same SDL that clients query. A pull request that changes the API shows the
97
+ change in the schema diff.
98
+ - **It is designed before it is implemented.** You can agree on the types and fields
99
+ first, as a team, and the server follows the design rather than leaking its internals.
100
+ - **Tools understand it directly.** IDE plugins, code generators and schema registries
101
+ read SDL. Lighthouse's `lighthouse:ide_helper` task adds its directives and generated
102
+ types so editors validate and autocomplete the schema.
103
+ - **It says what each field does.** `posts: [Post!]! @paginate @guard @cacheControl(maxAge: 60)`
104
+ tells a reader, without opening any Ruby, that the field is paginated, requires a
105
+ signed-in user and can be cached for a minute.
106
+
107
+ ## Why directives
108
+
109
+ Directives are a small vocabulary that composes. Each one does one job, and a field
110
+ combines them:
111
+
112
+ ```graphql
113
+ type Mutation {
114
+ updatePost(input: UpdatePostInput! @spread): Post!
115
+ @guard # require a user
116
+ @canFind(ability: "update", find: "input.id") # PostPolicy#update?
117
+ @update # save through Active Record, in a transaction
118
+ @clearCache(type: "Post", idSource: { argument: "input.id" })
119
+ }
120
+ ```
121
+
122
+ Because the behaviour is declared, Lighthouse controls the order it runs in
123
+ (authentication, then authorization, then validation, then the save, then cache
124
+ invalidation) and handles the details the same way everywhere: pagination types and
125
+ cursors, validation errors keyed by argument path, `Cache-Control` headers. That
126
+ consistency is what a hand-written resolver per field rarely achieves.
127
+
128
+ ## Why it fits Active Record
129
+
130
+ A directive can only be this short if something else knows the details. In Lighthouse,
131
+ that is the model layer. Active Record is opinionated, and every convention it imposes is
132
+ information Lighthouse can use:
133
+
134
+ | Active Record already knows | So Lighthouse can |
135
+ | --- | --- |
136
+ | Associations, their foreign keys, `:through` and polymorphic links | Resolve `@hasMany`, `@belongsTo`, `@belongsToMany` and `@morph*` from the name alone, batch them, and paginate many parents in one query |
137
+ | Scopes | Narrow any query with `scopes: ["published"]` |
138
+ | Validations | Report a failed `@create` or `@update` by argument path, with the model's own messages and locale |
139
+ | Transactions and callbacks | Run nested mutations (create a post, attach tags, add comments) atomically, with your callbacks |
140
+ | Naming conventions (`Post`, `posts`, `author_id`) | Map `Post` to its model, `authorId` to `author_id`, and a field to its association, with no configuration |
141
+
142
+ The schema says **what** the API offers; the models already say **how** the data
143
+ behaves. Nothing is described twice, and your domain logic stays where Rails puts it,
144
+ in models, scopes, validations and policies, instead of moving into GraphQL resolvers.
145
+ That is the same reason Lighthouse works so well on Eloquent: both are opinionated
146
+ Active Record ORMs, and conventions are what make a declarative layer on top possible.
147
+
148
+ ## Ruby where there is real logic
149
+
150
+ Schema-first doesn't mean schema-only. When a field does something no directive
151
+ describes, write Ruby, using graphql-ruby's own classes:
152
+
153
+ - A `Query` or `Mutation` field without a resolving directive resolves by convention to
154
+ `Queries::FieldName` (a `GraphQL::Schema::Resolver`) or `Mutations::FieldName` (a
155
+ `GraphQL::Schema::Mutation`). See [Queries, Mutations and Builders](resolvers.md).
156
+ - `@builder` classes adjust a query; `@validator` contracts check arguments.
157
+ - Your own directives plug in through the registry, with the same contracts as the
158
+ built-in ones. See [Custom Directives](custom-directives.md).
159
+
160
+ Lighthouse is built on graphql-ruby, not instead of it: its Dataloader, its execution
161
+ engine and its classes are all there when you need them.
162
+
163
+ ## From small apps to large ones
164
+
165
+ The same schema serves a side project and a busy API. When it grows, opt-in features keep
166
+ it fast without changing the schema: a cached schema build for quick boots, Apollo's
167
+ persisted queries for CDN caching, and an allowlist of trusted operations. They are all
168
+ off by default. See [Performance at scale](performance.md).
169
+
170
+ ## When something else fits better
171
+
172
+ Lighthouse is at its best when the API is close to your models. Consider plain
173
+ graphql-ruby, or using Lighthouse only for part of the schema, when:
174
+
175
+ - the API is mostly computed, aggregated or fetched from other services, rather than read
176
+ from and written to your database;
177
+ - your team prefers to define the schema in Ruby and generate the SDL from it
178
+ (code-first);
179
+ - types differ so much from your tables that most fields would need their own resolver.
180
+
181
+ Both approaches run on graphql-ruby, and a Lighthouse schema can call graphql-ruby
182
+ classes anywhere, so you can start with directives and write Ruby where it pays off.
183
+
184
+ Ready to try it? Continue with [Getting Started](getting-started.md).
data/docs/resolvers.md CHANGED
@@ -152,6 +152,16 @@ are looked up in the builders namespace only. Raising a `GraphQL::ExecutionError
152
152
  from a builder (e.g. a tenancy check) always reaches the client; it never falls
153
153
  back to the unscoped relation.
154
154
 
155
+ The gem ships RBS signatures in `sig/`, so with Steep (or anything that reads
156
+ `rbs collection`) a builder can be checked against `Lighthouse::_Builder`:
157
+
158
+ ```rbs
159
+ # sig/builders/published.rbs
160
+ class Builders::Published
161
+ extend Lighthouse::_Builder
162
+ end
163
+ ```
164
+
155
165
  ## Generators
156
166
 
157
167
  ```shell
@@ -15,7 +15,7 @@ class Lighthouse::Generators::BuilderGenerator < Lighthouse::Generators::ClassGe
15
15
  <<~RUBY
16
16
  # Applied by @builder(class: "#{class_name}") before the field resolves:
17
17
  # receives the relation, the field's arguments and the GraphQL context, and
18
- # returns the relation to use.
18
+ # returns the relation to use. Its type is Lighthouse::_Builder.
19
19
  class #{class_basename}
20
20
  def self.call(relation, args, context)
21
21
  relation
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lighthouse
4
4
  module GraphQL
5
- VERSION = '0.2.0'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end
@@ -0,0 +1,158 @@
1
+ # The extension points for custom directives: the contracts a directive class
2
+ # includes, the registry it is registered in, and the base classes it can
3
+ # subclass. See docs/custom-directives.md.
4
+ module Lighthouse
5
+ # What the registry asks a self-describing directive class: its SDL name
6
+ # (without the @) and its `directive @name(...) on ...` definition.
7
+ interface _DirectiveClass
8
+ def directive_name: () -> String
9
+ def definition: () -> String?
10
+ end
11
+
12
+ # Marker modules: the registry detects a class's capabilities by which of
13
+ # them it includes. Only FieldResolver and ArgBuilder change how a class is
14
+ # registered; the others document intent.
15
+ module Contracts
16
+ module Directive
17
+ end
18
+
19
+ # Its instances, built with (field_def, type_def, directive_node, opts),
20
+ # install the field's resolver in `define_resolver(target_type)`.
21
+ module FieldResolver
22
+ include Directive
23
+ end
24
+
25
+ # Its instances, built with (arg_node, directive_node), answer
26
+ # `handle_builder(relation, value)` with the constrained relation.
27
+ module ArgBuilder
28
+ include Directive
29
+ end
30
+
31
+ module FieldMiddleware
32
+ include Directive
33
+ end
34
+
35
+ module FieldManipulator
36
+ include Directive
37
+ end
38
+
39
+ module ArgManipulator
40
+ include Directive
41
+ end
42
+ end
43
+
44
+ class DirectiveRegistry
45
+ RESOLUTION_ORDER: Hash[String, Integer]
46
+ NAMED_RESOLVERS: Hash[String, Symbol]
47
+ SELF_DESCRIBING_RESOLVERS: Array[Symbol]
48
+ ARG_BUILDERS: Array[Symbol]
49
+ PASSIVE_DEFINITIONS: Hash[String, String]
50
+
51
+ def self.default: () -> DirectiveRegistry
52
+
53
+ # register(MyDirective) when the class answers `directive_name`, else
54
+ # register('name', MyDirective).
55
+ def self.register: (Class | String | Symbol name_or_klass, ?Class? klass) -> DirectiveRegistry
56
+
57
+ def initialize: () -> void
58
+ def register: (Class | String | Symbol name_or_klass, ?Class? klass) -> self
59
+ def register_defaults: () -> self
60
+ def register_definition: (String | Symbol name, String definition) -> self
61
+ def register_type_definition: (String | Symbol name, String definition) -> self
62
+ def register_manipulator: (_SdlManipulator manipulator) -> self
63
+
64
+ def resolution_class: (String | Symbol name) -> Class?
65
+ def resolution_order: (String | Symbol name) -> Integer
66
+ def wrapper?: (String | Symbol name) -> bool
67
+ def resolution_names: () -> Array[String]
68
+ def arg_builder_class: (String | Symbol name) -> Class?
69
+ def arg_builder_names: () -> Array[String]
70
+
71
+ # { "name" => "directive @name(...) on ..." }
72
+ def directive_definitions: () -> Hash[String, String]
73
+ # { "Name" => "enum Name { ... }" }
74
+ def type_definitions: () -> Hash[String, String]
75
+
76
+ attr_reader sdl_manipulators: Array[_SdlManipulator]
77
+ def manipulate_sdl: (String sdl) -> String
78
+ end
79
+
80
+ # Writes the SDL an IDE needs next to the app's own: `bin/rails lighthouse:ide_helper`.
81
+ module IdeHelper
82
+ DIRECTIVES_FILE: String
83
+ PROGRAMMATIC_TYPES_FILE: String
84
+
85
+ # Writes both files into `output_dir` and returns their paths.
86
+ def self?.write: (output_dir: path, ?sdl_folder: path?, ?registry: DirectiveRegistry) -> Array[String]
87
+ def self?.directives_sdl: (?String sdl, ?registry: DirectiveRegistry) -> String
88
+ def self?.programmatic_types_sdl: (String sdl, ?registry: DirectiveRegistry) -> String
89
+ end
90
+
91
+ module GraphQL
92
+ module DirectiveResolvers
93
+ # The base of the built-in field directives, and of a custom one:
94
+ # subclass it and implement `define_resolver`.
95
+ class BaseDirectiveResolver
96
+ # graphql-ruby's field and type definitions, and the directive's AST node.
97
+ @field_def: untyped
98
+ @type_def: untyped
99
+ @directive_node: untyped
100
+ @opts: Hash[Symbol, untyped]
101
+
102
+ def initialize: (untyped field_def, untyped type_def, untyped directive_node, ?Hash[Symbol, untyped] opts) -> void
103
+
104
+ # Install the field's resolver on `target_type_class` (a graphql-ruby type class).
105
+ def define_resolver: (untyped target_type_class) -> untyped
106
+
107
+ def apply_where_filters: (relation relation, untyped args) -> relation
108
+ def apply_arg_builders: (relation relation, arguments args_h) -> relation
109
+ def extract_relation_name_from_directive: (untyped arg_node) -> String?
110
+ def constantize_model: (String? name) -> Class?
111
+ def apply_scopes: (relation relation) -> relation
112
+
113
+ # The literal value of an argument of this directive.
114
+ def directive_arg: (String name) -> untyped
115
+ def resolver_method_name: () -> String
116
+ # The field's resolver so far, for a directive that wraps it.
117
+ def capture_inner: (untyped target_type_class) -> untyped
118
+ def install: (untyped target_type_class, untyped callable) -> untyped
119
+ # The model the directive acts on; raises DefinitionError when there is none.
120
+ def model_class: () -> Class
121
+ end
122
+
123
+ # Applies the field's @builder directives, in SDL order, to its relation.
124
+ class BuilderApplier
125
+ DEFINITION: String
126
+
127
+ def self.apply: (relation relation, untyped field_def_ast_node, arguments args, context ctx, untyped resolver_instance) -> relation
128
+ def self.apply_directive: (relation relation, untyped directive_node, arguments args, context ctx, untyped resolver_instance) -> relation
129
+ def self.extract_directive_arg: (untyped directive_node, String name) -> untyped
130
+ end
131
+ end
132
+
133
+ module Directives
134
+ module Arguments
135
+ # The base of the argument filters (@eq, @in, @orderBy, ...), and of a
136
+ # custom one: subclass it and implement `handle_builder`.
137
+ class Base
138
+ include Contracts::ArgBuilder
139
+
140
+ # graphql-ruby's AST nodes for the argument and the directive on it.
141
+ @arg_node: untyped
142
+ @directive_node: untyped
143
+
144
+ def initialize: (untyped arg_node, untyped directive_node) -> void
145
+
146
+ # Constrain the relation by the value the client gave the argument.
147
+ def handle_builder: (relation relation, untyped value) -> relation
148
+
149
+ private
150
+
151
+ # The directive's `key:`, else the argument's name in snake_case.
152
+ def column: () -> String
153
+ def arel_column: (relation relation) -> untyped
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,78 @@
1
+ # The entrypoints: building the schema from SDL and running operations.
2
+ module Lighthouse
3
+ module GraphQL
4
+ VERSION: String
5
+
6
+ Config: singleton(Lighthouse::Config)
7
+
8
+ # A GraphQL::Schema subclass.
9
+ type schema = untyped
10
+
11
+ module RbLightHouse
12
+ # The schema built from the SDL in `sdl_folder` (default: Config.sdl_folder).
13
+ # `options` takes :base_types, :base_schema and the schema's own options.
14
+ def self.get_schema: (?sdl_folder: path?, ?options: Hash[Symbol, untyped]) -> schema
15
+ def self.configure: () ?{ (singleton(Lighthouse::Config) config) -> void } -> void
16
+ end
17
+
18
+ module SchemaFactory
19
+ def self?.build_base_schema: (?sdl_folder: path?, ?sdl: String?, ?base_types: Hash[untyped, untyped],
20
+ ?schema_options: Hash[Symbol, untyped], **untyped extra_options) -> schema
21
+ def self?.load_sdl: (path? sdl_folder) -> String
22
+ end
23
+
24
+ # Apollo's Automatic Persisted Queries, and a manifest of trusted operations.
25
+ module PersistedQueries
26
+ HASH: Regexp
27
+ NOT_FOUND: Array[String]
28
+ NOT_SUPPORTED: Array[String]
29
+ HASH_MISMATCH: Array[String]
30
+ NOT_IN_LIST: Array[String]
31
+
32
+ # A request the protocol rejects, rendered with Apollo's error codes.
33
+ class Error < StandardError
34
+ attr_reader code: String
35
+
36
+ def initialize: (String message, String code) -> void
37
+ def to_h: () -> Hash[String, untyped]
38
+ end
39
+
40
+ # `extensions` as the request carries it: a Hash, a JSON String (GET) or
41
+ # ActionController::Parameters.
42
+ #
43
+ # `schema.execute` with the query the request names: its result, or a
44
+ # protocol error as a ready-to-render { "errors" => [...] } Hash.
45
+ def self?.execute: (schema schema, query: String?, ?extensions: untyped, **untyped options) -> untyped
46
+ # The query text to run for a request's `query` and `extensions`.
47
+ def self?.resolve: (String? query, untyped extensions) -> String?
48
+ def self?.enabled?: () -> bool
49
+ def self?.digest: (String query) -> String
50
+ # Store a query ahead of time (e.g. from a deploy task); returns its hash.
51
+ def self?.register: (String query) -> String
52
+ end
53
+
54
+ # The Cache-Control header the selected fields' @cacheControl hints allow.
55
+ module CacheControl
56
+ DEFINITION: String
57
+ TYPE_DEFINITIONS: Hash[String, String]
58
+ CONTEXT_KEY: Symbol
59
+
60
+ # For an executed query (a GraphQL::Query::Result or GraphQL::Query);
61
+ # nil when the schema does not use @cacheControl.
62
+ def self?.header: (untyped result) -> String?
63
+ # Set the header on a Rails (or Rack) response, and return it.
64
+ def self?.apply: (untyped response, untyped result) -> String?
65
+ end
66
+
67
+ # The opt-in cache of the manipulated schema document, in tmp/cache.
68
+ module SchemaCache
69
+ PREFIX: String
70
+
71
+ def self?.enabled?: () -> bool
72
+ def self?.directory: () -> path
73
+ # Build the schema document now and return the cache file's path.
74
+ def self?.warm!: (?String? sdl) -> String
75
+ def self?.clear!: () -> void
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,142 @@
1
+ # Signatures for the parts of Lighthouse an application implements or calls:
2
+ # the classes a schema names (builders, validators), the configuration, the
3
+ # directive extension points and the entrypoints. Internals are left out.
4
+ #
5
+ # graphql-ruby and Active Record ship no RBS every app has, so the values that
6
+ # come from them are aliases of untyped, named for what they hold.
7
+ module Lighthouse
8
+ # An ActiveRecord::Relation, or anything with its query interface.
9
+ type relation = untyped
10
+
11
+ # A field's arguments, with snake_case Symbol keys as graphql-ruby passes them.
12
+ type arguments = Hash[Symbol, untyped]
13
+
14
+ # The request's GraphQL::Query::Context.
15
+ type context = untyped
16
+
17
+ # A path on disk: a String or a Pathname.
18
+ type path = String | _ToPath
19
+
20
+ # A class `@builder(class: "Name")` names, looked up in Config.namespaces[:builders].
21
+ # It refines the relation of a field resolved by @all, @paginate, @first,
22
+ # @count or @aggregate and returns the relation to use; nil keeps the one it
23
+ # was given. `@builder(method:)` names a method other than `call`, with the
24
+ # same signature. The class answers it, or else an instance built with no
25
+ # arguments does. A GraphQL::ExecutionError it raises reaches the client.
26
+ interface _Builder
27
+ def call: (relation relation, arguments args, context context) -> relation?
28
+ end
29
+
30
+ # A @validator contract other than a Dry::Validation::Contract: it gets the
31
+ # arguments (snake_case Symbol keys) and answers the errors by key,
32
+ # { title: ["is missing"] }, nested for input objects; empty when valid.
33
+ interface _Validator
34
+ def call: (Hash[Symbol, untyped] values) -> Hash[Symbol | String, untyped]
35
+ end
36
+
37
+ # Config.on_error: called with an error a directive or resolver swallowed.
38
+ interface _ErrorHandler
39
+ def call: (StandardError error, context? context) -> void
40
+ end
41
+
42
+ # Config.authenticated_user and Config.policy_user: the user of a request.
43
+ interface _UserReader
44
+ def call: (context? context) -> untyped
45
+ end
46
+
47
+ # Config.authorizer: may `user` perform `ability` on `record` (a model
48
+ # instance, or a model class for @canModel)? `args` are the @can* `args:`,
49
+ # after the field's arguments with `injectArgs: true`.
50
+ interface _Authorizer
51
+ def call: (untyped user, untyped record, String | Symbol ability, ?Array[untyped] args) -> boolish
52
+ end
53
+
54
+ # What DirectiveRegistry#register_manipulator takes: it transforms the SDL
55
+ # before the schema is built.
56
+ interface _SdlManipulator
57
+ def manipulate: (String sdl) -> String
58
+ end
59
+
60
+ def self.configure: () ?{ (singleton(Config) config) -> void } -> void
61
+
62
+ module Config
63
+ DEFAULT_NAMESPACES: Hash[Symbol, Array[String]]
64
+ SETTINGS: Array[Symbol]
65
+
66
+ def self.configure: () ?{ (singleton(Config) config) -> void } -> void
67
+ def self.[]: (Symbol | String key) -> untyped
68
+ def self.reset!: () -> void
69
+
70
+ def self.sdl_folder: () -> path?
71
+ def self.sdl_folder=: (path? folder) -> path?
72
+
73
+ # { queries:, mutations:, builders:, validators: } => [namespace, ...].
74
+ # Assigning a partial hash overrides only the kinds it names.
75
+ def self.namespaces: () -> Hash[Symbol, Array[String]]
76
+ def self.namespaces=: (Hash[Symbol | String, String | Array[String]] overrides) -> Hash[Symbol | String, String | Array[String]]
77
+
78
+ def self.resolver_namespaces: () -> Array[String]
79
+ def self.resolver_namespaces=: (Array[String] namespaces) -> Array[String]
80
+
81
+ def self.directive_map: () -> Hash[untyped, untyped]
82
+
83
+ # An ActiveSupport::Cache::Store; Rails.cache by default.
84
+ def self.cache_store: () -> untyped
85
+ def self.cache_store=: (untyped store) -> untyped
86
+
87
+ def self.transactional_mutations: () -> bool
88
+ def self.transactional_mutations=: (bool? enabled) -> bool?
89
+
90
+ def self.pagination_default_count: () -> Integer?
91
+ def self.pagination_default_count=: (Integer? count) -> Integer?
92
+ def self.pagination_max_count: () -> Integer?
93
+ def self.pagination_max_count=: (Integer? count) -> Integer?
94
+
95
+ def self.schema_cache: () -> bool
96
+ def self.schema_cache=: (bool? enabled) -> bool?
97
+ def self.schema_cache_path: () -> path?
98
+ def self.schema_cache_path=: (path? path) -> path?
99
+ def self.watch_sdl: () -> bool
100
+ def self.watch_sdl=: (bool? enabled) -> bool?
101
+
102
+ def self.persisted_queries: () -> bool
103
+ def self.persisted_queries=: (bool? enabled) -> bool?
104
+ def self.persisted_queries_store: () -> untyped
105
+ def self.persisted_queries_store=: (untyped store) -> untyped
106
+ def self.persisted_queries_ttl: () -> Numeric?
107
+ def self.persisted_queries_ttl=: (Numeric? seconds) -> Numeric?
108
+ def self.persisted_queries_manifest: () -> path?
109
+ def self.persisted_queries_manifest=: (path? path) -> path?
110
+ def self.persisted_queries_only: () -> bool
111
+ def self.persisted_queries_only=: (bool? enabled) -> bool?
112
+
113
+ def self.on_error: () -> _ErrorHandler
114
+ def self.on_error=: (_ErrorHandler? handler) -> _ErrorHandler?
115
+ def self.handle_error: (StandardError error, ?context? context) -> void
116
+
117
+ def self.authenticated_user: () -> _UserReader
118
+ def self.authenticated_user=: (_UserReader? reader) -> _UserReader?
119
+ def self.policy_user: () -> _UserReader
120
+ def self.policy_user=: (_UserReader? reader) -> _UserReader?
121
+ def self.authorizer: () -> _Authorizer
122
+ def self.authorizer=: (_Authorizer? authorizer) -> _Authorizer?
123
+ end
124
+
125
+ # The schema refers to something that does not exist or breaks a contract.
126
+ class DefinitionError < StandardError
127
+ end
128
+
129
+ # A GraphQL::ExecutionError (declared from StandardError, which it descends
130
+ # from, as graphql-ruby ships no RBS): the @can* family denied the ability.
131
+ class AuthorizationError < StandardError
132
+ def initialize: (?String message, **untyped options) -> void
133
+ end
134
+
135
+ # A GraphQL::ExecutionError: invalid arguments, keyed by argument path
136
+ # ({ "input.title" => ["Title can't be blank"] }) under extensions.validation.
137
+ class ValidationError < StandardError
138
+ attr_reader messages: Hash[String, Array[String]]
139
+
140
+ def initialize: (String | Symbol field_name, Hash[String, Array[String]] messages) -> void
141
+ end
142
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lighthouse-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vaz Innovation
@@ -62,6 +62,7 @@ files:
62
62
  - docs/federation.md
63
63
  - docs/filtering-and-ordering.md
64
64
  - docs/getting-started.md
65
+ - docs/motivation.md
65
66
  - docs/mutations.md
66
67
  - docs/performance.md
67
68
  - docs/relationships.md
@@ -155,13 +156,16 @@ files:
155
156
  - lib/lighthouse/support/naming.rb
156
157
  - lib/lighthouse/tasks/lighthouse.rake
157
158
  - lib/lighthouse/where_conditions/operator_map.rb
159
+ - sig/lighthouse.rbs
160
+ - sig/lighthouse/directives.rbs
161
+ - sig/lighthouse/graphql.rbs
158
162
  homepage: https://github.com/Vaz-Innovation/lighthouse-graphql
159
163
  licenses:
160
164
  - MIT
161
165
  metadata:
162
166
  source_code_uri: https://github.com/Vaz-Innovation/lighthouse-graphql
163
167
  changelog_uri: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/CHANGELOG.md
164
- documentation_uri: https://github.com/Vaz-Innovation/lighthouse-graphql/tree/main/docs
168
+ documentation_uri: https://vaz-innovation.github.io/lighthouse-graphql/
165
169
  rubygems_mfa_required: 'true'
166
170
  rdoc_options: []
167
171
  require_paths: