lighthouse-graphql 0.5.0 → 0.7.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -1
  3. data/README.md +38 -22
  4. data/docs/best-practices.md +6 -4
  5. data/docs/caching.md +4 -3
  6. data/docs/configuration.md +20 -26
  7. data/docs/custom-directives.md +2 -1
  8. data/docs/directives.md +79 -14
  9. data/docs/federation.md +1 -1
  10. data/docs/filtering-and-ordering.md +330 -30
  11. data/docs/generators.md +42 -40
  12. data/docs/getting-started.md +128 -58
  13. data/docs/motivation.md +3 -2
  14. data/docs/performance.md +5 -4
  15. data/docs/relationships.md +7 -5
  16. data/docs/relay.md +63 -0
  17. data/docs/resolvers.md +92 -39
  18. data/docs/schema-organisation.md +177 -0
  19. data/docs/security.md +64 -16
  20. data/docs/subscriptions.md +16 -9
  21. data/docs/types.md +16 -17
  22. data/docs/uploads.md +41 -14
  23. data/lib/generators/lighthouse/base_classes.rb +9 -14
  24. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  25. data/lib/generators/lighthouse/install/install_generator.rb +125 -99
  26. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  27. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
  28. data/lib/lighthouse/ast_builder.rb +124 -0
  29. data/lib/lighthouse/caching.rb +9 -3
  30. data/lib/lighthouse/config.rb +27 -25
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  32. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  33. data/lib/lighthouse/controller.rb +28 -5
  34. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  36. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
  37. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  38. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  39. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  40. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  41. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  42. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  43. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
  44. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  45. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
  46. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
  47. data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
  48. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  49. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  50. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  51. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  52. data/lib/lighthouse/directives/definitions.rb +0 -1
  53. data/lib/lighthouse/directives/generated_types.rb +103 -0
  54. data/lib/lighthouse/directives/registry.rb +39 -10
  55. data/lib/lighthouse/document_ast.rb +111 -0
  56. data/lib/lighthouse/field_resolver_stack.rb +9 -6
  57. data/lib/lighthouse/global_id.rb +29 -3
  58. data/lib/lighthouse/graphql/version.rb +1 -1
  59. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  60. data/lib/lighthouse/ide_helper.rb +80 -23
  61. data/lib/lighthouse/namespaces.rb +1 -0
  62. data/lib/lighthouse/order_by/types.rb +137 -0
  63. data/lib/lighthouse/railtie.rb +9 -2
  64. data/lib/lighthouse/rb_lighthouse.rb +16 -5
  65. data/lib/lighthouse/resolve_info.rb +5 -5
  66. data/lib/lighthouse/resolver_bases.rb +18 -4
  67. data/lib/lighthouse/resolver_dispatch.rb +16 -1
  68. data/lib/lighthouse/root_types.rb +117 -0
  69. data/lib/lighthouse/schema_cache.rb +8 -4
  70. data/lib/lighthouse/schema_factory.rb +6 -9
  71. data/lib/lighthouse/schema_generator.rb +8 -18
  72. data/lib/lighthouse/schema_stitcher.rb +64 -0
  73. data/lib/lighthouse/support/naming.rb +0 -12
  74. data/lib/lighthouse/upload.rb +4 -2
  75. data/lib/lighthouse/upload_store.rb +39 -14
  76. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  77. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  78. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  79. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  80. data/lib/lighthouse/where_conditions/types.rb +110 -0
  81. data/sig/lighthouse/directives.rbs +16 -3
  82. data/sig/lighthouse/graphql.rbs +48 -4
  83. data/sig/lighthouse.rbs +8 -12
  84. metadata +17 -7
  85. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
  86. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  87. data/lib/generators/lighthouse/query_generator.rb +0 -30
  88. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  89. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  90. data/lib/lighthouse/sdl_loader.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dd6b5839f6520df6489135c1a37382ce4f7889d136140f652443ad5a3cd1a81
4
- data.tar.gz: 60fdb10b55abe72a1dc3df89705b904b424344e5bffa611e695a46fa8767ddc7
3
+ metadata.gz: eec6f789caae61cc196024238623ebc042783884cf8ebb53b19bb0e3f6d2cf3f
4
+ data.tar.gz: 61bdd5cef3df0d2c4ac9701501576c96a9c207d81b5f2f976d22e5fee14df9ba
5
5
  SHA512:
6
- metadata.gz: 0f2aff4008f41238a6ebca9c598add10069e7f0c4c8c5578bc1041f3da50f36b859b9ce1c91a29576442562a42afca20845b8fb3704b19d374570381a3588262
7
- data.tar.gz: b985fc24dfb2d0196c96f84cb0423d0b21e38659a0bc1cd4db13dc3818a8939fd2340006d85605cb5a50fde37f638d177ba90bf5af26e2cc3b9a3a0c2043986a
6
+ metadata.gz: 11c76fea28a691e29eea4910674585891907cefbeef72e9efc7519b8f812cd26f39d6895c90a46c91259b53c0dc64e3a0437add651d9eed07e5cd74807f245f2
7
+ data.tar.gz: 530507331c8736355448b67e1cf6ac354a0745351038fc9e7f8535df47f30299e2cf4ea203bee15482bc3fbcf3b8f9d0e5d7e72c61f263f0d9759016ae210a10
data/CHANGELOG.md CHANGED
@@ -6,6 +6,215 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.7.0] - 2026-10-18
10
+
11
+ The schema is written by PHP Lighthouse's rules now: one entrypoint that imports
12
+ the other files, every type defined once, and `extend type` to add to it.
13
+ `@orderBy`, `@whereConditions` and `@whereHasConditions` generate and apply what
14
+ PHP Lighthouse's do. In development, the IDE helper files are rewritten
15
+ automatically.
16
+
17
+ **Upgrading from 0.6:**
18
+
19
+ 1. Replace `config.sdl_folder = ...` with `config.schema_path = Rails.root.join('graphql/schema.graphql').to_s`
20
+ (that is the default), and `get_schema(sdl_folder:)` with `get_schema(schema_path:)`.
21
+ 2. Import every other file from `graphql/schema.graphql`. `#import **/*.graphql` imports
22
+ every file below its folder, which matches what was loaded before.
23
+ 3. Turn a second `type Query { ... }` (or `type Mutation`, or any type defined in two files)
24
+ into `extend type Query { ... }`. A type defined twice now fails the build.
25
+ 4. Write `orderBy:` arguments as `orderBy: _ @orderBy(columns: [...])`, and drop the
26
+ `OrderByClause` inputs and the `SortOrder` enum you declared. `order` is required in a
27
+ clause, and `{ field:, direction: }` clauses are no longer accepted.
28
+ 5. `SQLOperator` has PHP Lighthouse's values. Replace `NIN` with `NOT_IN`, and
29
+ `CONTAINS` / `STARTS_WITH` / `ENDS_WITH` with `LIKE` and a `%` pattern
30
+ (`value: "%Rails%"`, `"Rails%"`, `"%Rails"`). `@where(operator:)` keeps its operators.
31
+ 6. A `HAS` condition is on the related records' columns (`…HasCondition`, a `String`
32
+ column), as in PHP Lighthouse, no longer on the parent's column enum.
33
+
34
+ ### Added
35
+
36
+ - **`#import`.** The schema's entrypoint, `config.schema_path`, imports other files by a
37
+ path relative to the importing file. Globs (`#import blog/*.graphql`) work too. An
38
+ imported file can import more files, and each file is read once.
39
+ - **Type extensions merged as PHP Lighthouse merges them.** `extend type`, `input`,
40
+ `interface`, `enum`, `union` and `scalar` add to their definition, and the
41
+ directives on their fields work as on the type's own. `extend type Query` alone
42
+ defines `Query`.
43
+ - **`@orderBy` generates its types.** `orderBy: _ @orderBy(columns: [...])` gets
44
+ `[<Type><Field><Arg>OrderByClause!]`, a column enum (`@enum(value:)` per column)
45
+ and `SortOrder`. `columnsEnum:` reuses an enum, and without columns the shared
46
+ `OrderByClause` takes any column.
47
+ - **`@orderBy(relations:)`.** Clients order by `COUNT` of a relation, or by
48
+ `AVG`/`MIN`/`MAX`/`SUM` of one of its columns
49
+ (`{ posts: { aggregate: COUNT }, order: DESC }`). Records without related ones
50
+ are ordered too, and this works on batched relationship pages.
51
+ - **`@orderBy(column:, direction:)` on a field.** It always orders the result, after
52
+ the client's clauses.
53
+ - **`@whereConditions` and `@whereHasConditions`, as in PHP Lighthouse.** They have
54
+ PHP Lighthouse's `SQLOperator` (`LIKE`, `NOT_LIKE`, `NOT_IN`, `BETWEEN`,
55
+ `NOT_BETWEEN`, ...), and `HAS` with `operator:` / `amount:`
56
+ (`{ relation: "posts", operator: LT, amount: 1 }`: the records with none). The
57
+ `…Relation` / `…HasCondition` inputs filter on the related table, and
58
+ `handler:` names a class that applies the conditions instead.
59
+ - **JSON columns.** `@whereConditions`, `@whereHasConditions` and `@orderBy` take a path into a
60
+ JSON column, `meta->author->name` or `meta->tags->0`, on PostgreSQL, MySQL and SQLite.
61
+ Numbers compare and sort as numbers, booleans compare as booleans, and `IS_NULL` holds
62
+ for a JSON `null` too.
63
+ - **`config.ide_helper`.** `schema-directives.graphql` and
64
+ `programmatic-types.graphql` are rewritten on boot and on every reload. By
65
+ default this happens in development when the app has a `graphql.config.yml`.
66
+ - `Contracts::FieldBuilder`, and `Contracts::ArgManipulator` as `ASTBuilder` calls
67
+ it, for custom directives.
68
+ - The guide [docs/schema-organisation.md](docs/schema-organisation.md).
69
+
70
+ ### Changed
71
+
72
+ - **Breaking:** The schema is read from `config.schema_path` and its imports, not from every
73
+ file in `config.sdl_folder`, which is removed. `lighthouse:install` takes `--schema-path`
74
+ instead of `--sdl-folder`.
75
+ - **Breaking:** A type defined twice fails the build: "Duplicate definition X found when parsing
76
+ the schema", pointing at `extend`. PHP Lighthouse keeps the last definition.
77
+ - **Breaking:** An argument typed `_` that no directive replaces fails the build. It used to
78
+ become a scalar.
79
+ - **Breaking:** `@orderBy` clauses need `order`, take only `{ column:, order: }`, and a
80
+ column outside an enum must be an identifier.
81
+ - **Breaking:** `SQLOperator`'s values are PHP Lighthouse's (see above).
82
+ - **Breaking:** `@builder` and `@whereAuth` on a field apply after the argument directives,
83
+ as PHP Lighthouse's field builder directives do. An `order` a builder adds now breaks the
84
+ ties of the client's `orderBy` instead of preceding it. They also apply on relationship
85
+ fields (`@hasMany`, ...), where they used to be ignored, and on `@aggregate`, which now
86
+ gets the argument directives too.
87
+
88
+ ### Fixed
89
+
90
+ - A schema that stops building while you edit it keeps its `programmatic-types.graphql`.
91
+ If there is none yet, the file declares the `_` placeholder alone, so the IDE never
92
+ reports `Unknown type "_"`. When an import is missing, the IDE helper reads every
93
+ `.graphql` file in the folder, as the IDE does.
94
+ - `programmatic-types.graphql` no longer repeats the types `schema-directives.graphql`
95
+ defines (`Mixed`, `CanAction`, `PaginateType`, ...). graphql-js, which the VS Code
96
+ and JetBrains GraphQL plugins use, rejected the schema.
97
+ - `@whereConditions` applied only to an argument named `where`. It and `@whereHasConditions`
98
+ are argument directives like the others now: any name, in SDL order.
99
+ - An error in a `@whereConditions` value (an unknown relation or operator) is the client's
100
+ error. It used to be swallowed and leave the query unfiltered.
101
+
102
+ ## [0.6.0] - 2026-10-18
103
+
104
+ Lighthouse now uses what the Rails app and graphql-ruby already have instead of settings and
105
+ files of its own: the app's cache, its controllers' rate limiting, its ActiveStorage, and
106
+ everything graphql-ruby's `graphql:install` writes, which `lighthouse:install` now builds on.
107
+ Several defaults change.
108
+
109
+ **Upgrading from 0.5:**
110
+
111
+ 1. Remove `config.cache_store =` and `config.upload_store =` from
112
+ `config/initializers/lighthouse.rb`. Configure the app's `config.cache_store` instead, and
113
+ `config.action_controller.cache_store` if rate limits should be counted somewhere else.
114
+ 2. `@throttle` counts per IP now. Add `by: "current_user.id"` where a field counted per user.
115
+ 3. Past a `@throttle` limit the request is answered with a 429, as Rails' `rate_limit` answers it.
116
+ To fail only the field, as before, give `with:` a controller method that raises a
117
+ `GraphQL::ExecutionError`.
118
+ 4. `@upload` checks `public:` against the ActiveStorage service. On a public service, add
119
+ `public: true`. Where `public: true` meets a private service, name a public one with `disk:`.
120
+ 5. `rails g lighthouse:query` and `lighthouse:mutation` are gone. Resolve a root field with a
121
+ method of `Types::QueryType` / `MutationType`, with `rails g graphql:mutation`, or with a
122
+ `Resolvers::` / `Mutations::` class written by hand, which convention still finds. An app
123
+ installed with 0.5 keeps working as it is: `AppSchema = get_schema(...)` is still supported.
124
+
125
+ ### Added
126
+
127
+ - `@throttle(by:)`: who the client is, as `rate_limit`'s `by:`. It is a path in the context, read
128
+ as `@inject` reads one (`by: "current_user.id"`).
129
+ - `@throttle(with:)`: a controller method to call past the limit, as `rate_limit`'s `with:`. One
130
+ that renders or redirects ends the request with its response, as a `before_action` that renders
131
+ does. One that returns lets the call through, and one that raises a `GraphQL::ExecutionError`
132
+ fails only the field.
133
+ - `@throttle` refusals are instrumented as `rate_limit.action_controller`, with `rate_limit`'s
134
+ payload (`request`, `count`, `to`, `within`, `by`, `name`, `scope`, `cache_key`).
135
+ - `base_types` takes graphql-ruby's code-first root types and Node interface. The SDL's root type
136
+ of a kind is built on `query_type:` / `mutation_type:` / `subscription_type:` (`Types::QueryType`),
137
+ so the `field`s the class declares (graphql-ruby's Relay `node` and `nodes`, what
138
+ `graphql:mutation` adds to `Types::MutationType`) are the schema's too, beside the SDL's, and
139
+ resolve as in graphql-ruby. With no such root type in the SDL, the class is the root. `node:`
140
+ (`Types::NodeType`) is the SDL's `Node` interface, which `@node` types implement, so
141
+ graphql-ruby's Relay and `@node` share one `Node`.
142
+ - A root field with no resolving directive resolves with the method of its name on the root type
143
+ class, when the class includes `Lighthouse::GraphQL::Type` (`Query.latestPosts` →
144
+ `Types::QueryType#latest_posts`), before the conventional `Resolvers::` / `Mutations::` class.
145
+ - A schema that implements graphql-ruby's `id_from_object` / `object_from_id` has one kind of
146
+ global id: `@globalId` encodes an object's own id with `id_from_object`, and decodes an
147
+ argument with `object_from_id`.
148
+ - `lighthouse:install --subscriptions` writes `Subscriptions::BaseSubscription` and names it in
149
+ `base_types`; without it, the install writes no subscription base.
150
+
151
+ ### Changed
152
+
153
+ - `@cache`, `@clearCache` and Automatic Persisted Queries use the app's `Rails.cache`
154
+ (`config.cache_store`), as PHP Lighthouse uses Laravel's cache. Outside Rails they use an
155
+ in-memory store.
156
+ - `@throttle` is Rails' `rate_limit` on one field, and counts as it does:
157
+ - It counts in the controller's `cache_store` (`config.action_controller.cache_store`,
158
+ `Rails.cache` unless set), under `rate_limit`'s key, `rate-limit:<scope>:<name>:<by>`.
159
+ Counts kept under the old `lighthouse:throttle:` keys start over.
160
+ - `name:` is `rate_limit`'s. Each field counts under its own name (`Query.x`) by default, apart
161
+ from the controller's `rate_limit`, or together with it when both use the same name.
162
+ `prefix:` is `rate_limit`'s `scope:`, the controller's path by default.
163
+ - The client is its IP, as `rate_limit`'s default `by:`, no longer the signed-in user.
164
+ - Past the limit it does what `rate_limit` does by default: it raises
165
+ `ActionController::TooManyRequests`, which Rails answers with a 429
166
+ (`head :too_many_requests` on Rails 7.2). The field used to fail on its own, with the rest of
167
+ the operation answered; that remains the behavior without Action Pack.
168
+ - `lighthouse:install` builds on graphql-ruby's `graphql:install`, run with whichever of its
169
+ options the app chose (`--relay`, `--batch`, `--skip-mutation-root-type`, `--schema`,
170
+ `--directory`, `--api`), or run first with its defaults when the app has none. It writes no
171
+ schema class of its own: graphql-ruby's `class AppSchema < GraphQL::Schema` becomes
172
+ `class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: ... })`,
173
+ naming every class graphql-ruby wrote, and keeps the rest of the class (`use
174
+ GraphQL::Dataloader`, `max_depth`, `id_from_object`, ...). It takes out what Lighthouse replaces:
175
+ the `query(...)` / `mutation(...)` root types the SDL's are built on, the `resolve_type` stub,
176
+ and the example `test_field`. Its base object, interface, union and scalar include
177
+ `Lighthouse::GraphQL::Type`, so every type class is Lighthouse's without an `include` of its
178
+ own. With `--batch`, the schema uses `GraphQL::Dataloader` too. Its `Mutations::BaseMutation`, a
179
+ `RelayClassicMutation`, is kept; graphql-ruby's controller, unchanged since its install, becomes
180
+ one with `Lighthouse::GraphQL::Controller`. `Types::QueryType` and `MutationType` are kept.
181
+ - An SDL Mutation field that declares its arguments one by one resolves by convention to a
182
+ `GraphQL::Schema::RelayClassicMutation` subclass as to a plain mutation, its arguments as
183
+ keywords. It used to fail looking for `input:`.
184
+ - A subscriptions backend the app's schema class `use`s in its body
185
+ (`use GraphQL::Subscriptions::ActionCableSubscriptions`) delivers the SDL's Subscription fields.
186
+ graphql-ruby adds its subscription extension only to a root type of the class's own.
187
+ - `Lighthouse::GraphQL::Controller` puts the controller in the context (`:lighthouse_controller`)
188
+ for `@throttle`. In development, an error Rails answers with a status of its own
189
+ (`config.action_dispatch.rescue_responses`, such as a 429 or a 404) now gets that status, as in
190
+ production, instead of being rendered as a JSON 500.
191
+ - `@upload` stores files on the app's ActiveStorage only, with PHP Lighthouse's rules. `disk:`
192
+ names a service of `config/storage.yml` (`config.active_storage.service` without it), and
193
+ `path:` starts the blob's key. ActiveStorage sets visibility per service, so `public:` must now
194
+ agree with the service's: `public: true` on a private service, or the default `false` on a
195
+ public one, fails instead of being ignored. An unknown service, or a value that is not a file,
196
+ fails naming the argument. The file is rewound before it is stored, so a `@validator` may read
197
+ it first.
198
+ - The `Upload` scalar's errors are PHP Lighthouse's: a value that is not a file names the GraphQL
199
+ multipart request specification and what was sent instead.
200
+
201
+ ### Removed
202
+
203
+ - `Config.cache_store`: configure the app's `config.cache_store` instead, and
204
+ `config.action_controller.cache_store` (or a controller's `self.cache_store =`) for `@throttle`.
205
+ `Config.persisted_queries_store` stays, defaulting to `Rails.cache`.
206
+ - `Config.upload_store`: to store files elsewhere, declare an ActiveStorage service and name it
207
+ with `@upload(disk:)`, as a Laravel app declares a disk.
208
+ - `rails g lighthouse:query` and `lighthouse:mutation`: graphql-ruby has no resolver generator,
209
+ and `graphql:mutation` works in a Lighthouse app now. See Upgrading.
210
+
211
+ ### Fixed
212
+
213
+ - On graphql-ruby 2.5, a schema using `@limit`, `@bind` or `@upload` without one of their optional
214
+ arguments did not build (`Expected value to not be null`): graphql 2.5 does not apply the default
215
+ of a non-null directive argument. Those arguments (`builder`, `column`, `with`, `optional`,
216
+ `path`, `public`) are nullable now, with the same defaults.
217
+
9
218
  ## [0.5.0] - 2026-10-18
10
219
 
11
220
  ### Added
@@ -461,7 +670,9 @@ had been running inside a private monorepo as a path gem before being extracted.
461
670
  - Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
462
671
  `on_error`, and `DirectiveRegistry.register` for app-defined directives.
463
672
 
464
- [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.5.0...HEAD
673
+ [Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.7.0...HEAD
674
+ [0.7.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.6.0...v0.7.0
675
+ [0.6.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.5.0...v0.6.0
465
676
  [0.5.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.4.0...v0.5.0
466
677
  [0.4.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.3.0...v0.4.0
467
678
  [0.3.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...v0.3.0
data/README.md CHANGED
@@ -72,34 +72,46 @@ It depends on `graphql ~> 2.5` and `activesupport`. For federation you also need
72
72
  On graphql 2.5, keep the `json` gem below 3: graphql 2.5 calls `JSON.generate`
73
73
  with an option json 3 removed (graphql 2.6 no longer does).
74
74
 
75
- Then let the generator set the app up:
75
+ Then let the generators set the app up:
76
76
 
77
77
  ```shell
78
+ bin/rails generate graphql:install # graphql-ruby's, with the options you choose (optional)
78
79
  bin/rails generate lighthouse:install
79
80
  ```
80
81
 
81
- It runs graphql-ruby's own `graphql:install` for the base classes every graphql-ruby
82
- app has, then writes Lighthouse's on top: the initializer, a starter schema in
83
- `graphql/schema.graphql`, the `AppSchema` class built from it, a `GraphqlController`
84
- with its `POST /graphql` route, and a `graphql.config.yml` for your IDE. Run
82
+ `lighthouse:install` builds on graphql-ruby's own install, and runs `graphql:install`
83
+ with its defaults when the app has none. It adapts what graphql-ruby wrote: the
84
+ schema class becomes Lighthouse's, built from the SDL on graphql-ruby's root types
85
+ and base classes, and the rest of its body stays; the base object, interface, union
86
+ and scalar include `Lighthouse::GraphQL::Type`; the controller serves the schema
87
+ through `Lighthouse::GraphQL::Controller`. It adds the initializer, a starter schema
88
+ in `graphql/schema.graphql` and a `graphql.config.yml` for your IDE. Run
85
89
  `bin/rails generate lighthouse:install --help` for its options (`--schema`,
86
- `--sdl-folder`, `--skip-controller`, `--skip-ide`), and see
90
+ `--schema-path`, `--subscriptions`, `--skip-controller`, `--skip-ide`), and see
87
91
  [docs/generators.md](docs/generators.md) for every generator, graphql-ruby's included.
88
92
 
93
+ ### Schema files
94
+
95
+ As in PHP Lighthouse, the schema is read from one entrypoint,
96
+ `graphql/schema.graphql`, which `#import`s the other files
97
+ (`#import user.graphql`, `#import blog/*.graphql`). A type is defined once, and the
98
+ other files add to it with `extend type Query`. See
99
+ [docs/schema-organisation.md](docs/schema-organisation.md).
100
+
89
101
  ### IDE support
90
102
 
91
103
  Lighthouse's directives and the types it generates (Paginator/Connection types,
92
- `whereConditions` inputs, …) are defined in Ruby, so your IDE can't see them in
93
- your `.graphql` files. Write them out for it:
104
+ `@whereConditions` and `@orderBy` inputs, the `_` placeholder, …) are defined in
105
+ Ruby, so your IDE can't see them in your `.graphql` files. Lighthouse writes them
106
+ to `schema-directives.graphql` and `programmatic-types.graphql` in the app root:
107
+ in development on every boot and reload, or with:
94
108
 
95
109
  ```shell
96
110
  bin/rails lighthouse:ide_helper
97
111
  ```
98
112
 
99
- This writes `schema-directives.graphql` and `programmatic-types.graphql` to the app
100
- root; the generated `graphql.config.yml` already points the GraphQL plugins for
101
- JetBrains IDEs and VS Code at them. Rerun it after changing your schema or
102
- registering a directive. See [docs/getting-started.md](docs/getting-started.md#ide-support).
113
+ The generated `graphql.config.yml` already points the GraphQL plugins for
114
+ JetBrains IDEs and VS Code at them. See [docs/getting-started.md](docs/getting-started.md#ide-support).
103
115
 
104
116
  ## Quick start
105
117
 
@@ -123,22 +135,26 @@ Prefer to wire it by hand? These are the steps the generator performs.
123
135
  }
124
136
  ```
125
137
 
126
- 2. **Build the schema** (typically in `app/graphql/your_schema.rb`):
138
+ 2. **Build the schema**: graphql-ruby's schema class (`app/graphql/app_schema.rb`),
139
+ inheriting from the schema Lighthouse builds instead of `GraphQL::Schema`:
127
140
 
128
141
  ```ruby
129
- require 'lighthouse-graphql'
130
-
131
- AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
132
- sdl_folder: Rails.root.join('graphql').to_s,
142
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
133
143
  options: {
134
- base_types: {
135
- resolver: Resolvers::BaseResolver, # graphql-ruby's base classes, which
136
- mutation: Mutations::BaseMutation, # rails g lighthouse:install has graphql-ruby write
137
- subscription: Subscriptions::BaseSubscription
138
- # object: Types::BaseObject, ...
144
+ base_types: { # the classes graphql-ruby's install writes
145
+ query_type: Types::QueryType,
146
+ mutation_type: Types::MutationType,
147
+ node: Types::NodeType,
148
+ resolver: Resolvers::BaseResolver,
149
+ mutation: Mutations::BaseMutation,
150
+ object: Types::BaseObject
151
+ # interface: Types::BaseInterface, union:, scalar:, enum:, input_object: ...
139
152
  }
140
153
  }
141
154
  )
155
+ use GraphQL::Dataloader # Lighthouse batches relationships through it
156
+ max_depth(15)
157
+ end
142
158
  ```
143
159
 
144
160
  3. **Execute** it from your controller exactly like any graphql-ruby schema:
@@ -92,7 +92,9 @@ app's GraphQL setup easy to find.
92
92
 
93
93
  ## Split your SDL
94
94
 
95
- Lighthouse concatenates every `*.graphql` file under your `sdl_folder`. Organize
96
- by domain (`users.graphql`, `orders.graphql`, …) and keep a small
97
- `_directives.graphql` only for directives you declare yourself (built-ins are
98
- injected automatically).
95
+ Lighthouse reads `graphql/schema.graphql` and the files it `#import`s
96
+ ([schema-organisation.md](schema-organisation.md)). Organize
97
+ by domain (`users.graphql`, `orders.graphql`, …), each with its types and the
98
+ fields it adds with `extend type Query` / `extend type Mutation`. Keep a small
99
+ `directives.graphql`, imported too, only for directives you declare yourself
100
+ (built-ins are injected automatically).
data/docs/caching.md CHANGED
@@ -41,9 +41,10 @@ such as a Relay connection object, is reported through
41
41
  [`on_error`](configuration.md#on_error) and returned uncached. So cache list
42
42
  fields, not connection fields.
43
43
 
44
- The store is [`Config.cache_store`](configuration.md#cache_store), `Rails.cache`
45
- by default. Note that Rails uses a `:null_store` in development and test unless
46
- you enable caching (`bin/rails dev:cache`).
44
+ The store is your app's cache, `Rails.cache` (`config.cache_store`), as PHP
45
+ Lighthouse uses Laravel's: Lighthouse keeps no store of its own. Note that Rails
46
+ uses a `:null_store` in development and test unless you enable caching
47
+ (`bin/rails dev:cache`).
47
48
 
48
49
  A cached value is still authorized and validated on every request: `@cache` runs
49
50
  inside `@guard`, `@can*` and `@validator`, never around them.
@@ -9,7 +9,7 @@ reusable and publishable.
9
9
  require 'lighthouse-graphql'
10
10
 
11
11
  Lighthouse.configure do |config|
12
- config.sdl_folder = Rails.root.join('graphql').to_s
12
+ config.schema_path = Rails.root.join('graphql/schema.graphql').to_s
13
13
  config.namespaces = { queries: ['Resolvers'], mutations: ['Mutations'] }
14
14
  config.resolver_namespaces = ['App::Graphql']
15
15
  config.on_error = ->(error, _context) { Rails.error.report(error) }
@@ -18,11 +18,25 @@ end
18
18
 
19
19
  ## Options
20
20
 
21
- ### `sdl_folder`
21
+ ### `schema_path`
22
22
 
23
- Where Lighthouse loads `*.graphql` files from. Defaults to `RAILS_ROOT/graphql`
24
- in a Rails app, `nil` otherwise (pass SDL explicitly via `SchemaFactory` when not
25
- on Rails).
23
+ The schema's entrypoint, as PHP Lighthouse's `schema_path`. Lighthouse reads
24
+ this file and the files it imports (`#import user.graphql`, `#import blog/*.graphql`).
25
+ A type is defined once, and other files add to it with `extend type`. See
26
+ [schema-organisation.md](schema-organisation.md). Defaults to
27
+ `RAILS_ROOT/graphql/schema.graphql` in a Rails app, and `nil` otherwise. Outside
28
+ Rails, pass the path, or the SDL itself (`SchemaFactory.build_base_schema(sdl:)`).
29
+
30
+ `get_schema(schema_path: ...)` overrides it for one schema.
31
+
32
+ ### `ide_helper`
33
+
34
+ Rewrite `schema-directives.graphql` and `programmatic-types.graphql` in the
35
+ app's root on boot and on every reload, for the IDE
36
+ ([schema-organisation.md](schema-organisation.md#ide-support)). A file whose
37
+ content is unchanged is left alone. `nil` (the default) turns it on in
38
+ development when the app has a `graphql.config.yml`, which `lighthouse:install`
39
+ writes. Set `true` or `false` to decide yourself.
26
40
 
27
41
  ### `namespaces`
28
42
 
@@ -70,15 +84,6 @@ reporter in production:
70
84
  config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
71
85
  ```
72
86
 
73
- ### `cache_store`
74
-
75
- The store behind [`@cache` / `@clearCache`](caching.md). Defaults to
76
- `Rails.cache` (an in-memory store outside Rails).
77
-
78
- ```ruby
79
- config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
80
- ```
81
-
82
87
  ### `pagination_default_count`, `pagination_max_count`
83
88
 
84
89
  Defaults for paginated fields: the `first` used when a client leaves it out (so
@@ -99,7 +104,7 @@ Options for large schemas and high traffic, all off by default. See
99
104
  config.schema_cache = Rails.env.production? # cache the built schema in tmp/cache
100
105
  config.watch_sdl = Rails.env.development? # reload when a .graphql file changes
101
106
  config.persisted_queries = true # Apollo's Automatic Persisted Queries
102
- config.persisted_queries_store = Rails.cache # where APQ keeps queries (default: cache_store)
107
+ config.persisted_queries_store = Rails.cache # where APQ keeps queries (default: Rails.cache)
103
108
  config.persisted_queries_ttl = 1.week.to_i # how long they live there
104
109
  config.persisted_queries_manifest = Rails.root.join('persisted-queries.json') # trusted operations
105
110
  config.persisted_queries_only = Rails.env.production? # run nothing else
@@ -128,17 +133,6 @@ returning `[type, id]`. Defaults to `Lighthouse::GraphQL::GlobalId`, the base64
128
133
  config.global_id = SignedGlobalIds
129
134
  ```
130
135
 
131
- ### `upload_store`
132
-
133
- Stores the file an [`@upload`](uploads.md) argument carries: any object with
134
- `call(file, disk:, path:, public:)`, whose return value the argument then holds.
135
- Defaults to `Lighthouse::GraphQL::UploadStore`, which makes an ActiveStorage blob and
136
- returns its signed id.
137
-
138
- ```ruby
139
- config.upload_store = ->(file, path: '/', **) { S3Uploader.put(file, prefix: path) }
140
- ```
141
-
142
136
  ### `queue_broadcasts`, `broadcast_queue`
143
137
 
144
138
  Whether [`@broadcast`](directives.md#broadcast) triggers its subscription in an
@@ -29,7 +29,8 @@ Include the marker module(s) your directive fulfils:
29
29
  | `Contracts::FieldResolver` | provide a field's resolver | `#define_resolver(type)` (or `#resolve_field`) |
30
30
  | `Contracts::FieldMiddleware` | wrap an existing resolver (e.g. `@auth`) | `#wrap_resolver(next)` |
31
31
  | `Contracts::FieldManipulator` | reshape the schema for a field | `manipulate_field_definition(...)` |
32
- | `Contracts::ArgManipulator` | reshape the schema for an argument | `manipulate_argument_definition(...)` |
32
+ | `Contracts::ArgManipulator` | reshape the schema for an argument, as `@orderBy` and `@whereConditions` generate their types | `#manipulate_arg_definition(document_ast, field_node, type_node)` returning the argument node |
33
+ | `Contracts::FieldBuilder` | constrain a field's query, whatever the client sends | `#handle_field_builder(relation)` |
33
34
  | `Contracts::ArgBuilder` | compose a query constraint from an argument | `#handle_builder(relation, value)` |
34
35
 
35
36
  A directive class also answers two class-level questions: its name and its SDL
data/docs/directives.md CHANGED
@@ -78,7 +78,7 @@ type Query {
78
78
  ```
79
79
 
80
80
  - **The query.** It is the model's (`model:`, else the return type), narrowed by
81
- `scopes:`, [`@builder`](#builder) and the field's argument filters. On a nested
81
+ `scopes:`, the field's argument filters and [`@builder`](#builder). On a nested
82
82
  field it pages the parent's association of the same name: `User.posts @paginate`
83
83
  pages the user's posts.
84
84
  - **`first`.** It defaults to `defaultCount`, else
@@ -283,16 +283,23 @@ type PostQueries {
283
283
 
284
284
  ### `@throttle`
285
285
 
286
- Rate-limits a field per client: at most `maxAttempts` calls every `decayMinutes`
287
- (default 60 a minute), counted in [`cache_store`](configuration.md#cache_store). A
288
- client is the signed-in user, else the context's `:remote_ip`, which the controller
289
- concern sets. Past the limit the field fails with `Rate limit for Query.x exceeded.
290
- Try again later.` before anything else runs. `name:` makes fields count together,
291
- `prefix:` separates groups.
286
+ Rails' `rate_limit` on one field: at most `maxAttempts` calls (its `to:`) every
287
+ `decayMinutes` (its `within:`, default 60 a minute), counted before anything else runs.
288
+ It takes `rate_limit`'s options, and counts as `rate_limit` does: in the controller's
289
+ `cache_store`, under the same key, and instrumented as `rate_limit.action_controller`.
290
+ See [security.md](security.md#rate-limiting).
291
+
292
+ | Argument | `rate_limit`'s | Default |
293
+ | --- | --- | --- |
294
+ | `by:` | `by:`, who the client is: a path in the context, as [`@inject`](resolvers.md#spread-and-inject)'s (`"current_user.id"`) | the client's IP |
295
+ | `with:` | `with:`, the controller's method to call past the limit | `rate_limit`'s: a 429 (it raises `ActionController::TooManyRequests`) |
296
+ | `name:` | `name:`; fields, and the controller's own `rate_limit`, of the same name count together | the field (`Query.x`) |
297
+ | `prefix:` | `scope:` | the controller's path |
292
298
 
293
299
  ```graphql
294
300
  type Mutation {
295
301
  login(email: String!, password: String!): User @throttle(maxAttempts: 5)
302
+ createTag(name: String!): Tag! @throttle(maxAttempts: 20, by: "current_user.id", with: "too_many_requests") @guard @create
296
303
  }
297
304
  ```
298
305
 
@@ -449,7 +456,12 @@ Builders are looked up in their own namespace, `Builders::` (app/graphql/builder
449
456
  `class:` with `method:` (default `call`), or PHP Lighthouse's `method: "Class@method"`.
450
457
 
451
458
  On a field, the method gets `(relation, args, context)`, or the directive's
452
- `value:` in place of the arguments. On an argument or an input field, it gets
459
+ `value:` in place of the arguments. As in PHP Lighthouse, a field's `@builder`
460
+ (with `@whereAuth` and `@orderBy(column:)`) applies after the argument
461
+ directives, in SDL order. An `order` it adds therefore breaks the ties of the
462
+ client's `orderBy`. It applies on every field that queries records: `@all`,
463
+ `@paginate`, `@find`, `@first`, `@count`, `@aggregate`, the relationship
464
+ directives and `resolve_info.enhance_builder`. On an argument or an input field, it gets
453
465
  `(relation, value, context)` whenever the client sends that argument, an explicit
454
466
  `null` included, and not when it is left out:
455
467
 
@@ -511,7 +523,7 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
511
523
  | `@whereBetween` / `@whereNotBetween` | `column BETWEEN` / `NOT BETWEEN` the input's two fields, in order |
512
524
  | `@whereJsonContains` | a JSON column contains the value (every value of a list); `key: "meta->tags"` points into it |
513
525
  | `@scope(name:)` | calls the model scope with the value (`name:` defaults to the argument's) |
514
- | `@orderBy` | sort by one or more columns |
526
+ | `@orderBy` | sort by the client's clauses (columns, relations' aggregates); on a field, a fixed order |
515
527
 
516
528
  ```graphql
517
529
  type Query {
@@ -519,7 +531,7 @@ type Query {
519
531
  status: String @eq
520
532
  nameSearch: String @like
521
533
  minAge: Int @where(operator: "GTE", key: "age")
522
- orderBy: [OrderByClause!] @orderBy
534
+ orderBy: _ @orderBy(columns: ["name", "created_at"])
523
535
  ): [User!]! @all
524
536
  }
525
537
  ```
@@ -561,18 +573,71 @@ parents it would cut them all together, as PHP Lighthouse warns. PHP Lighthouse
561
573
  also allows `@limit` on the field itself, where it only marks the field the argument
562
574
  limits; it is accepted there and changes nothing.
563
575
 
576
+ ### `@orderBy`
577
+
578
+ ```graphql
579
+ directive @orderBy(
580
+ columns: [String!] # argument: the columns the client may order by
581
+ columnsEnum: String # argument: an enum of them instead (one of the two)
582
+ relations: [OrderByRelation!] # argument: { relation:, columns:, columnsEnum: } to order by an aggregate
583
+ column: String # field: the column to order by
584
+ direction: OrderByDirection = ASC
585
+ ) on ARGUMENT_DEFINITION | FIELD_DEFINITION
586
+ ```
587
+
588
+ On an argument written as `_`, Lighthouse generates the clause input
589
+ (`<Type><Field><Arg>OrderByClause`, or `…RelationOrderByClause` with
590
+ `relations:`) and the column enum. Without `columns:`, `columnsEnum:` or
591
+ `relations:`, the argument takes the shared `OrderByClause`, which allows any
592
+ column. A column can be a JSON path, `meta->rank`, in `@orderBy` and in
593
+ `@whereConditions` ([JSON columns](filtering-and-ordering.md#json-columns)). On a field, `@orderBy(column:, direction:)` orders the result after
594
+ the client's clauses.
595
+
596
+ ```graphql
597
+ type Query {
598
+ posts(orderBy: _ @orderBy(columns: ["posted_at", "title"])): [Post!]! @all
599
+ users(orderBy: _ @orderBy(relations: [{ relation: "posts" }])): [User!]! @all
600
+ latestPosts: [Post!]! @all @orderBy(column: "posted_at", direction: DESC)
601
+ }
602
+
603
+ # { posts(orderBy: [{ column: POSTED_AT, order: DESC }]) { title } }
604
+ # { users(orderBy: [{ posts: { aggregate: COUNT }, order: DESC }]) { name } }
605
+ ```
606
+
607
+ Guide: [filtering-and-ordering.md](filtering-and-ordering.md#ordering).
608
+
564
609
  ### `@whereConditions` / `@whereHasConditions`
565
610
 
566
- Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
567
- related records). Lighthouse generates the input and enum types for you. See
568
- [filtering-and-ordering.md](filtering-and-ordering.md#complex-where-conditions).
611
+ ```graphql
612
+ directive @whereConditions(columns: [String!], columnsEnum: String, handler: String) on ARGUMENT_DEFINITION
613
+ directive @whereHasConditions(relation: String, columns: [String!], columnsEnum: String, handler: String) on ARGUMENT_DEFINITION
614
+ ```
615
+
616
+ These give clients a dynamic `WHERE` builder, with conditions joined by `AND`
617
+ and `OR`, PHP Lighthouse's `SQLOperator` operators, and `HAS` checks on
618
+ relations. On an argument written as `_`, Lighthouse generates the input
619
+ (`<Type><Field><Arg>WhereConditions` / `…WhereHasConditions`), its `…Relation`
620
+ and `…HasCondition` inputs, and the column enum. Without `columns:` or
621
+ `columnsEnum:`, the argument takes the shared `WhereConditions`, which allows
622
+ any column.
623
+
624
+ `@whereHasConditions` keeps the records with a related record matching the
625
+ conditions. The relation is `relation:`, or the argument's name without `has`.
626
+ `handler:` names a `Builders::` class whose `call(relation, conditions)` applies
627
+ the conditions instead.
569
628
 
570
629
  ```graphql
571
630
  type Query {
572
631
  people(where: _ @whereConditions(columns: ["age", "type"])): [Person!]! @all
632
+ users(hasPosts: _ @whereHasConditions(columns: ["title"])): [User!]! @all
573
633
  }
634
+
635
+ # { people(where: { AND: [{ column: AGE, operator: GT, value: 37 }, { column: TYPE, value: "Actor" }] }) { name } }
636
+ # { users(hasPosts: { column: TITLE, operator: LIKE, value: "%Rails%" }) { name } }
574
637
  ```
575
638
 
639
+ Guide: [filtering-and-ordering.md](filtering-and-ordering.md#complex-where-conditions).
640
+
576
641
  ### `@trim`, `@hash`, `@drop`, `@bind` and `@upload`
577
642
 
578
643
  Rewrite an argument's or input field's value before the field resolves, in the
@@ -584,7 +649,7 @@ order the SDL lists them:
584
649
  | `@drop` | nothing: the argument is removed |
585
650
  | `@hash` | the value hashed with BCrypt (the `bcrypt` gem, as `has_secure_password`) |
586
651
  | `@bind(class:, column: "id", with: [], optional: false)` | the model record(s) the value identifies, eager-loading `with`; or what a class's `call(value, context)` returns |
587
- | `@upload(disk:, path: "/")` | what [`upload_store`](configuration.md#upload_store) answers for the uploaded file: the signed id of an ActiveStorage blob, by default ([uploads.md](uploads.md)) |
652
+ | `@upload(disk:, path: "/", public: false)` | the signed id of the ActiveStorage blob the file was stored as, on the app's service `disk` names ([uploads.md](uploads.md)) |
588
653
 
589
654
  ```graphql
590
655
  type Mutation {
data/docs/federation.md CHANGED
@@ -29,7 +29,7 @@ class Types::BaseObject < GraphQL::Schema::Object
29
29
  end
30
30
 
31
31
  BaseAppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
32
- sdl_folder: Rails.root.join('graphql').to_s,
32
+ schema_path: Rails.root.join('graphql/schema.graphql').to_s,
33
33
  options: { federation: true, base_types: { object: Types::BaseObject, resolver: Resolvers::BaseResolver } }
34
34
  )
35
35