activeadmin-graphql 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +31 -0
- data/CONTRIBUTING.md +27 -0
- data/LICENSE.md +21 -0
- data/README.md +49 -0
- data/activeadmin-graphql.gemspec +66 -0
- data/app/controllers/active_admin/graphql_controller.rb +168 -0
- data/docs/graphql-api.md +486 -0
- data/lib/active_admin/graphql/auth_context.rb +35 -0
- data/lib/active_admin/graphql/engine.rb +9 -0
- data/lib/active_admin/graphql/integration.rb +135 -0
- data/lib/active_admin/graphql/key_value_pair_input.rb +48 -0
- data/lib/active_admin/graphql/railtie.rb +10 -0
- data/lib/active_admin/graphql/record_source.rb +30 -0
- data/lib/active_admin/graphql/resource_config.rb +68 -0
- data/lib/active_admin/graphql/resource_definition_dsl.rb +117 -0
- data/lib/active_admin/graphql/resource_interface.rb +25 -0
- data/lib/active_admin/graphql/resource_query_proxy/controller.rb +149 -0
- data/lib/active_admin/graphql/resource_query_proxy.rb +112 -0
- data/lib/active_admin/graphql/run_action_mutation_config.rb +23 -0
- data/lib/active_admin/graphql/run_action_mutation_dsl.rb +32 -0
- data/lib/active_admin/graphql/run_action_payload.rb +27 -0
- data/lib/active_admin/graphql/schema_builder/build.rb +84 -0
- data/lib/active_admin/graphql/schema_builder/graph_params.rb +75 -0
- data/lib/active_admin/graphql/schema_builder/mutation_action_types.rb +52 -0
- data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +61 -0
- data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +118 -0
- data/lib/active_admin/graphql/schema_builder/mutation_create.rb +65 -0
- data/lib/active_admin/graphql/schema_builder/mutation_member.rb +122 -0
- data/lib/active_admin/graphql/schema_builder/mutation_type_builder.rb +52 -0
- data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +120 -0
- data/lib/active_admin/graphql/schema_builder/query_type.rb +53 -0
- data/lib/active_admin/graphql/schema_builder/query_type_collection.rb +84 -0
- data/lib/active_admin/graphql/schema_builder/query_type_member.rb +91 -0
- data/lib/active_admin/graphql/schema_builder/query_type_pages.rb +44 -0
- data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +57 -0
- data/lib/active_admin/graphql/schema_builder/resolvers.rb +116 -0
- data/lib/active_admin/graphql/schema_builder/resources.rb +48 -0
- data/lib/active_admin/graphql/schema_builder/types_inputs.rb +119 -0
- data/lib/active_admin/graphql/schema_builder/types_object.rb +96 -0
- data/lib/active_admin/graphql/schema_builder/visibility.rb +58 -0
- data/lib/active_admin/graphql/schema_builder/wire.rb +36 -0
- data/lib/active_admin/graphql/schema_builder.rb +62 -0
- data/lib/active_admin/graphql/schema_field.rb +29 -0
- data/lib/active_admin/graphql/version.rb +7 -0
- data/lib/active_admin/graphql.rb +68 -0
- data/lib/active_admin/primary_key.rb +117 -0
- data/lib/activeadmin/graphql.rb +5 -0
- metadata +389 -0
data/docs/graphql-api.md
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# GraphQL API
|
|
2
|
+
|
|
3
|
+
This guide documents the [activeadmin-graphql](https://github.com/amkisko/activeadmin-graphql) gem: an optional GraphQL HTTP API for each ActiveAdmin namespace, built with [graphql-ruby](https://graphql-ruby.org/). The API mirrors the same resources, scoping, and controller behavior as the HTML and JSON admin endpoints: Ransack `q`, menu `scope`, sort `order`, nested `belongs_to` parents, CRUD, batch actions, and custom member and collection actions.
|
|
4
|
+
|
|
5
|
+
The HTTP feature is off until you set `graphql = true` on a namespace. That adds a `POST` endpoint only (for example `POST /admin/graphql` for the default `admin` namespace); it does not replace the existing admin UI.
|
|
6
|
+
|
|
7
|
+
## Gem and loading
|
|
8
|
+
|
|
9
|
+
Add the extension to your application (it pulls in `activeadmin` and `graphql` as runtime dependencies):
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "activeadmin"
|
|
14
|
+
gem "activeadmin-graphql"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Bundling loads `activeadmin/graphql`, which installs the Router/Resource integration and calls `ActiveAdmin::GraphQL.load!`, so graphql-ruby is loaded at boot whenever the gem is in your bundle. The schema class for a namespace is built on demand (first request or introspection) via `ActiveAdmin::GraphQL.schema_for(namespace)`.
|
|
18
|
+
|
|
19
|
+
If `graphql = true` is set for a namespace but Bundler cannot satisfy the `graphql` gem, boot fails with an ActiveAdmin dependency error until you run `bundle install`.
|
|
20
|
+
|
|
21
|
+
Custom pages that declare `graphql_field` with types such as `GraphQL::Types::Int` need graphql-ruby loaded before `app/admin` is evaluated; with `activeadmin-graphql` in the Gemfile that is already true.
|
|
22
|
+
|
|
23
|
+
## Enabling the endpoint
|
|
24
|
+
|
|
25
|
+
In `config/initializers/active_admin.rb`, turn on GraphQL for the namespace you
|
|
26
|
+
want:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
ActiveAdmin.setup do |config|
|
|
30
|
+
config.namespace :admin do |admin|
|
|
31
|
+
admin.graphql = true
|
|
32
|
+
# Optional — default is "graphql", i.e. POST /admin/graphql
|
|
33
|
+
# admin.graphql_path = "graphql"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Run `bin/rails active_admin:install` or reload routes if needed so
|
|
39
|
+
`ActiveAdmin.routes(self)` is applied.
|
|
40
|
+
|
|
41
|
+
### Namespace settings
|
|
42
|
+
|
|
43
|
+
Each namespace inherits these options (defaults in parentheses):
|
|
44
|
+
|
|
45
|
+
* `graphql` — set to `true` to mount the endpoint (`false` by default).
|
|
46
|
+
* `graphql_path` — path segment under the namespace mount (`"graphql"`).
|
|
47
|
+
* `graphql_multiplex_max` — maximum operations per multiplexed JSON array
|
|
48
|
+
request (`20`).
|
|
49
|
+
* `graphql_max_complexity` — passed to graphql-ruby `max_complexity` when set.
|
|
50
|
+
* `graphql_max_depth` — passed to `max_depth` when set.
|
|
51
|
+
* `graphql_default_page_size` / `graphql_default_max_page_size` — connection
|
|
52
|
+
defaults when set.
|
|
53
|
+
* `graphql_dataloader` — graphql-ruby dataloader plugin (`nil` = use
|
|
54
|
+
`GraphQL::Dataloader`). Set to `GraphQL::Dataloader::AsyncDataloader` when
|
|
55
|
+
the [`async` gem](https://github.com/socketry/async) is loaded if you want
|
|
56
|
+
parallel I/O from custom `GraphQL::Dataloader::Source` `#fetch` methods.
|
|
57
|
+
* `graphql_visibility` — `nil` by default (no graphql-ruby visibility plugin).
|
|
58
|
+
Set to `true` to use [`GraphQL::Schema::Visibility`](https://graphql-ruby.org/schema/visibility.html)
|
|
59
|
+
with default options, or pass a `Hash` of visibility options (`profiles:`,
|
|
60
|
+
`dynamic:`, `preload:`, `migration_errors:`, …).
|
|
61
|
+
* `graphql_visibility_profile` — when using named visibility profiles, optional
|
|
62
|
+
default `context[:visibility_profile]` for each HTTP request (symbols accepted).
|
|
63
|
+
* `graphql_schema_visible` — optional `Proc` `(context, meta) -> Boolean` when
|
|
64
|
+
`graphql_visibility` is enabled; see Schema visibility below.
|
|
65
|
+
* `graphql_configure_schema` — `Proc` receiving the schema class after build
|
|
66
|
+
(extensions, extra types, and so on).
|
|
67
|
+
|
|
68
|
+
### GraphQL::Dataloader
|
|
69
|
+
|
|
70
|
+
The namespace schema calls `schema.use` with the configured dataloader (see
|
|
71
|
+
`graphql_dataloader` above). [graphql-ruby’s
|
|
72
|
+
dataloader](https://graphql-ruby.org/dataloader/overview.html) batches external
|
|
73
|
+
work across sibling fields using Fibers; each HTTP request gets its own
|
|
74
|
+
dataloader instance (available as `context.dataloader` inside resolvers).
|
|
75
|
+
|
|
76
|
+
Active Admin registers `ActiveAdmin::GraphQL::RecordSource` for
|
|
77
|
+
non-polymorphic `belongs_to` columns on resource types: resolving `author { id }` on
|
|
78
|
+
many `Post` nodes issues one batched `WHERE id IN (...)` for the related model
|
|
79
|
+
instead of one query per node. Custom `graphql` `configure` fields and
|
|
80
|
+
`register_page` `graphql_field` blocks run as normal GraphQL field methods, so
|
|
81
|
+
you can use `dataloader` (or `context.dataloader`) with your own
|
|
82
|
+
`GraphQL::Dataloader::Source` subclasses the same way as in standalone
|
|
83
|
+
graphql-ruby apps.
|
|
84
|
+
|
|
85
|
+
To implement `Schema.object_from_id` with batching (for example for `loads:` on
|
|
86
|
+
mutation arguments), use `context.dataloader.with(ActiveAdmin::GraphQL::RecordSource, model_class).load(database_id)` inside your `graphql_configure_schema`
|
|
87
|
+
hook after assigning `schema.define_singleton_method(:object_from_id, ...)`.
|
|
88
|
+
Global / Relay-style IDs are not generated by Active Admin itself.
|
|
89
|
+
|
|
90
|
+
### Schema visibility (`GraphQL::Schema::Visibility`)
|
|
91
|
+
|
|
92
|
+
When `graphql_visibility` is set, the namespace schema calls
|
|
93
|
+
`schema.use(GraphQL::Schema::Visibility, …)` so introspection and validation
|
|
94
|
+
respect [visibility](https://graphql-ruby.org/schema/visibility.html).
|
|
95
|
+
|
|
96
|
+
Set `graphql_schema_visible` to a proc that receives the GraphQL `context`
|
|
97
|
+
and a `meta` hash and returns truthy to keep the member visible. The hash
|
|
98
|
+
always includes `:kind`, and often `:graphql_type_name` (the resource’s GraphQL
|
|
99
|
+
basename), `:resource` (`ActiveAdmin::Resource`), and other keys:
|
|
100
|
+
|
|
101
|
+
| `kind` | Meaning |
|
|
102
|
+
|--------|---------|
|
|
103
|
+
| `:resource_interface` | `ActiveAdminResource` interface |
|
|
104
|
+
| `:resource_object` | Resource object type |
|
|
105
|
+
| `:resource_enum` | Rails enum type; `meta` includes `:column` |
|
|
106
|
+
| `:input_object` | Create/update/where/list-filter input; `meta` includes `:role` (`:create_input`, …) |
|
|
107
|
+
| `:registered_resource_union` | `ActiveAdminRegisteredResource` union |
|
|
108
|
+
| `:run_action_payload` | Batch/member/collection action payload type |
|
|
109
|
+
| `:query_registered_resource` | `registered_resource` query field |
|
|
110
|
+
| `:query_collection_field` / `:query_member_field` | Resource list/detail fields; `meta` includes `:field_name` |
|
|
111
|
+
| `:query_page_field` | Custom `register_page` field; `meta` includes `:page` |
|
|
112
|
+
| `:mutation_create`, `:mutation_update`, `:mutation_delete`, … | CRUD and action mutations |
|
|
113
|
+
|
|
114
|
+
Root `Query` / `Mutation` fields are defined with
|
|
115
|
+
`ActiveAdmin::GraphQL::SchemaField`, which supports a `visibility:` keyword:
|
|
116
|
+
a metadata `Hash` (same shape as the `meta` argument above) for
|
|
117
|
+
`graphql_schema_visible`, not a graphql-ruby core option. Resource `graphql`
|
|
118
|
+
`configure` blocks use that field class too, so custom fields may pass
|
|
119
|
+
`visibility: { … }` when you want the hook to run.
|
|
120
|
+
|
|
121
|
+
You can also subclass your own `GraphQL::Schema::Object` / `Field` types in
|
|
122
|
+
`graphql_configure_schema` and implement `visible?` there; Active Admin’s
|
|
123
|
+
helpers call `super` first so those implementations compose.
|
|
124
|
+
|
|
125
|
+
Interfaces and unions: graphql-ruby may keep an interface or union visible
|
|
126
|
+
when all members are hidden unless you return `false` from
|
|
127
|
+
`graphql_schema_visible` for `:resource_interface` or `:registered_resource_union`
|
|
128
|
+
(or implement `.visible?` on custom types). See graphql-ruby’s visibility migration
|
|
129
|
+
notes for edge cases.
|
|
130
|
+
|
|
131
|
+
## Making requests
|
|
132
|
+
|
|
133
|
+
The endpoint accepts `POST` at `{namespace}/{graphql_path}` (for example
|
|
134
|
+
`/admin/graphql`).
|
|
135
|
+
|
|
136
|
+
Send JSON with `query` and optional `variables`. For
|
|
137
|
+
[multiplexing](https://graphql-ruby.org/queries/multiplex.html), the body may
|
|
138
|
+
be a JSON array of such objects; the size is limited by `graphql_multiplex_max`.
|
|
139
|
+
|
|
140
|
+
Requests are handled by `ActiveAdmin::GraphqlController` (an engine controller,
|
|
141
|
+
not a generated `Admin::…` subclass).
|
|
142
|
+
|
|
143
|
+
### CSRF and cookie sessions
|
|
144
|
+
|
|
145
|
+
`GraphqlController` uses Rails’ default CSRF protection
|
|
146
|
+
(`protect_from_forgery with: :exception`). Clients that rely on session cookies
|
|
147
|
+
(for example a typical Devise session) must submit a valid CSRF token with each
|
|
148
|
+
`POST`, such as the `authenticity_token` used by the HTML admin. API-style
|
|
149
|
+
clients often use header-based authentication without cookie sessions, or a
|
|
150
|
+
separate route with CSRF disabled. Treat this endpoint like any other
|
|
151
|
+
`ActionController::Base` `POST` from the browser.
|
|
152
|
+
|
|
153
|
+
### Authentication and introspection
|
|
154
|
+
|
|
155
|
+
The namespace `authentication_method` runs before any GraphQL work, including
|
|
156
|
+
schema introspection. Unauthenticated clients should not receive a schema.
|
|
157
|
+
|
|
158
|
+
The GraphQL context exposes the same user as `current_active_admin_user` (from
|
|
159
|
+
the namespace `current_user_method`), wrapped for authorization checks.
|
|
160
|
+
|
|
161
|
+
## Authorization
|
|
162
|
+
|
|
163
|
+
Field resolvers call the namespace `authorization_adapter` (`authorized?` and
|
|
164
|
+
`scope_collection`) for standard read and CRUD operations. Data loading reuses
|
|
165
|
+
`ResourceController` (`find_collection`, `find_resource`,
|
|
166
|
+
`apply_authorization_scope`, and related methods), so collection scoping matches
|
|
167
|
+
REST.
|
|
168
|
+
|
|
169
|
+
Batch, member, and collection mutations first require read access to the
|
|
170
|
+
resource model (`authorized?` with the model class, in the same way as
|
|
171
|
+
collection queries). They then run the same controller actions and blocks as the
|
|
172
|
+
UI, including any `authorize!` or `authorize_resource!` calls.
|
|
173
|
+
|
|
174
|
+
See the ActiveAdmin guide [Authorization adapter](https://activeadmin.info/13-authorization-adapter) for adapter setup; the same rules apply to GraphQL.
|
|
175
|
+
|
|
176
|
+
## Schema overview
|
|
177
|
+
|
|
178
|
+
The `Query` type includes one Relay-style connection field per resource route
|
|
179
|
+
(for example `posts`), plus a singular field for a single record (for example
|
|
180
|
+
`post`). Optional `graphql_field` entries on `register_page` appear here too,
|
|
181
|
+
plus `registered_resource` (returning the `ActiveAdminRegisteredResource` union)
|
|
182
|
+
when at least one resource is exposed.
|
|
183
|
+
|
|
184
|
+
Every resource object type implements the `ActiveAdminResource` interface
|
|
185
|
+
(`id: ID!`) so clients can share fragments or rely on a common shape.
|
|
186
|
+
|
|
187
|
+
The `Mutation` type is present when at least one operation exists: per-resource
|
|
188
|
+
`create_*`, `update_*`, and `delete_*` when those controller actions exist, plus
|
|
189
|
+
`*_batch_action`, `*_member_action`, and `*_collection_action` fields when the
|
|
190
|
+
resource defines the corresponding batch, member, or collection actions.
|
|
191
|
+
|
|
192
|
+
Object type names default to the model name (for example `Post`). Typed input
|
|
193
|
+
objects mirror `attributes_for_graphql` (see the per-resource section below):
|
|
194
|
+
`PostCreateInput`, `PostUpdateInput`, `PostListFilterInput`, and `PostWhereInput`
|
|
195
|
+
when the GraphQL name is `Post`.
|
|
196
|
+
|
|
197
|
+
### Composite primary keys (Rails 7.1+)
|
|
198
|
+
|
|
199
|
+
For models with `self.primary_key = [:book_code, :seq]` (and no separate `id`
|
|
200
|
+
column), Active Admin treats GraphQL as follows:
|
|
201
|
+
|
|
202
|
+
* `id` on the object type — JSON object string with all key columns in
|
|
203
|
+
model order (same format Rails uses when casting composite ids), for example
|
|
204
|
+
`{"book_code":"CPK","seq":7}`.
|
|
205
|
+
* Readable fields — each primary-key column is also exposed as its own field
|
|
206
|
+
(in addition to `id`) so clients can load scalars without parsing JSON.
|
|
207
|
+
* `LibraryEditionWhereInput` (and similar) — either optional `id` (that JSON
|
|
208
|
+
string) or every primary-key column; `id` remains required when there is
|
|
209
|
+
only one key column.
|
|
210
|
+
* Singular query field — pass the JSON `id`, pass `where`, or pass one
|
|
211
|
+
GraphQL argument per primary-key column.
|
|
212
|
+
* Create mutations — composite key attributes stay included in create
|
|
213
|
+
inputs (unlike single `id` tables, where the key is omitted from assignable
|
|
214
|
+
attributes).
|
|
215
|
+
|
|
216
|
+
For REST/HTML member routes, `ResourceController#find_resource` must resolve composite ids from `params[:id]` (JSON string) or from individual primary-key params. Stock ActiveAdmin 3.x only passes a single `params[:id]` into `find`; an ActiveAdmin fork (or patch) that implements composite-aware `find_resource` keeps the admin UI aligned with GraphQL. The GraphQL layer also works with `ActiveAdmin::GraphQL::ResourceQueryProxy` using Rails’ tuple id convention for `Relation#find`.
|
|
217
|
+
|
|
218
|
+
The gem ships `ActiveAdmin::PrimaryKey` when the core constant is missing (upstream). A fork may define `PrimaryKey` and extended `find_resource` in `ResourceController` instead.
|
|
219
|
+
|
|
220
|
+
`ActiveAdmin::GraphQL::RecordSource` batch-loads composite associations using
|
|
221
|
+
Rails `where(primary_key => [tuple, …])`.
|
|
222
|
+
|
|
223
|
+
### Collection query arguments
|
|
224
|
+
|
|
225
|
+
These align with REST index parameters where possible:
|
|
226
|
+
|
|
227
|
+
* `filter` — optional `PostListFilterInput` with `scope`, `order`, `q` (Ransack
|
|
228
|
+
predicates as JSON — kept as JSON because predicate keys are open-ended), and the nested parent id when `belongs_to` applies.
|
|
229
|
+
* `scope` — menu scope id (string); still accepted on the field for backward
|
|
230
|
+
compatibility.
|
|
231
|
+
* `q` — Ransack predicates (JSON object).
|
|
232
|
+
* `order` — index sort string (for example `id_desc`).
|
|
233
|
+
* Parent foreign key — when the resource uses `belongs_to`, the parent param
|
|
234
|
+
(for example `user_id`) stays available on the connection field as today.
|
|
235
|
+
|
|
236
|
+
### Singular query arguments
|
|
237
|
+
|
|
238
|
+
* `where` — `PostWhereInput` with required `id` (single-key tables) or,
|
|
239
|
+
for composite keys, `id` (JSON) and/or each key column; optional parent ids for
|
|
240
|
+
nested resources.
|
|
241
|
+
* Legacy `id` / parent arguments behave as before (composite models also accept
|
|
242
|
+
one argument per key column on the field itself).
|
|
243
|
+
|
|
244
|
+
### CRUD mutations
|
|
245
|
+
|
|
246
|
+
Typical names for a `Post` resource:
|
|
247
|
+
|
|
248
|
+
* `create_post(input: PostCreateInput!)` — attribute fields match assignable
|
|
249
|
+
columns (and the nested parent param when `belongs_to` is configured).
|
|
250
|
+
* `update_post(where: PostWhereInput!, input: PostUpdateInput!)`
|
|
251
|
+
* `delete_post(where: PostWhereInput!)`
|
|
252
|
+
|
|
253
|
+
Nested resources require the parent id on `where`, `input`, or list `filter`
|
|
254
|
+
when the association is required.
|
|
255
|
+
|
|
256
|
+
### Migrating GraphQL clients
|
|
257
|
+
|
|
258
|
+
If you are updating clients that targeted an older embedded GraphQL integration (or regenerated types against a previous schema), check the following:
|
|
259
|
+
|
|
260
|
+
* Rails enum GraphQL names use an `Enum` infix (for example `PostEnumStatus`, not `PostStatus`). Update persisted operations, codegen, and fragments to match; see Rails enum columns below.
|
|
261
|
+
|
|
262
|
+
* CRUD mutations use typed `input` and `where` arguments (`PostCreateInput`, `PostWhereInput`, …), not a single loose `attributes` JSON object and bare `id` on update/delete. List/detail fields may use `where` / `filter` input objects; legacy scalar arguments on fields are unchanged where still exposed.
|
|
263
|
+
|
|
264
|
+
* `registered_resource(path: …)`, batch `inputs`, and member/collection `params` use `[ActiveAdminKeyValuePair!]` (a list of `{ key, value }` strings), not arbitrary JSON objects. Express maps as list items; duplicate keys keep the last value. Ransack `q` (on list `filter` / field args) stays a JSON object.
|
|
265
|
+
|
|
266
|
+
These rules match the current schema described in this guide. The core `activeadmin` gem’s `UPGRADING.md` may only mention them if you use a fork that references this extension.
|
|
267
|
+
|
|
268
|
+
### `registered_resource` query
|
|
269
|
+
|
|
270
|
+
`registered_resource(type_name: String!, id: ID!, path: [ActiveAdminKeyValuePair!])`
|
|
271
|
+
returns `ActiveAdminRegisteredResource`, the union of all resource object types in
|
|
272
|
+
the namespace. Pass nested parent route params as ordered `path: [{ key: "user_id", value: "1" }, …]`
|
|
273
|
+
(same keys as REST path segments). Resolve with inline fragments on concrete types.
|
|
274
|
+
|
|
275
|
+
`ActiveAdminKeyValuePair` is `{ key: String!, value: String! }` — flat strings only, like query/form fields.
|
|
276
|
+
|
|
277
|
+
### Batch, member, and collection actions
|
|
278
|
+
|
|
279
|
+
For a route key of `posts`, fields look like:
|
|
280
|
+
|
|
281
|
+
* `posts_batch_action(batch_action: String!, ids: [ID!]!, inputs: [ActiveAdminKeyValuePair!], …)` —
|
|
282
|
+
runs the registered batch action; `inputs` is converted to `batch_action_inputs` (flat key/value map)
|
|
283
|
+
like the HTML form. Omitted when batch actions are disabled or none are registered.
|
|
284
|
+
* `posts_member_action(action: String!, id: ID!, params: [ActiveAdminKeyValuePair!], …)` —
|
|
285
|
+
aggregate field: pick the `member_action` by `action` string (same as today).
|
|
286
|
+
* One GraphQL field per registered action — `{plural}_member_{action_name}` with
|
|
287
|
+
underscores (e.g. `posts_member_publish`, `posts_member_append_title_bang`): same
|
|
288
|
+
`id`, `params`, and parent ids as the aggregate field, without an `action:` argument.
|
|
289
|
+
Use `graphql { member_action_mutation(:publish) { … } }` to set return type,
|
|
290
|
+
`resolve`, and extra `arguments { … }` for that action only.
|
|
291
|
+
* `posts_collection_action(action: String!, params: [ActiveAdminKeyValuePair!], …)` —
|
|
292
|
+
aggregate collection actions by name.
|
|
293
|
+
* Per collection action: `{plural}_collection_{action_name}` (e.g.
|
|
294
|
+
`posts_collection_posts_count`). Configure with `collection_action_mutation(:posts_count) { … }`.
|
|
295
|
+
|
|
296
|
+
Action names are validated against registered definitions; arbitrary controller
|
|
297
|
+
methods are not exposed.
|
|
298
|
+
|
|
299
|
+
The aggregate fields stay available for clients that prefer a single entry point;
|
|
300
|
+
per-action fields give distinct GraphQL input and output types per `member_action` /
|
|
301
|
+
`collection_action` when you need them.
|
|
302
|
+
|
|
303
|
+
By default, run-action mutations return `ActiveAdminRunActionPayload`: `ok`,
|
|
304
|
+
`status`, `location` (redirects), and `body` (rendered output when present). You
|
|
305
|
+
can replace that object type per resource (see Run action return types
|
|
306
|
+
below). Resolvers still build `ActiveAdmin::GraphQL::RunActionPayload::Result`;
|
|
307
|
+
custom types should usually subclass `RunActionPayload` so those fields keep
|
|
308
|
+
working and `graphql_schema_visible` `:run_action_payload` continues to apply.
|
|
309
|
+
|
|
310
|
+
## Per-resource `graphql` block
|
|
311
|
+
|
|
312
|
+
Inside `ActiveAdmin.register` you can narrow the GraphQL surface:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
ActiveAdmin.register Post do
|
|
316
|
+
graphql do
|
|
317
|
+
disable! # omit this resource from the schema
|
|
318
|
+
type_name "BlogPost" # GraphQL object / mutation type basename
|
|
319
|
+
only :title, :body, :published_at # expose only these attributes
|
|
320
|
+
exclude :internal_score # or `except` / `exclude`
|
|
321
|
+
configure do
|
|
322
|
+
# graphql-ruby field DSL on the object type class
|
|
323
|
+
field :computed, GraphQL::Types::String, null: true
|
|
324
|
+
define_method(:computed) { object.title&.reverse }
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
* `disable!` — resource is not included in Query or Mutation.
|
|
331
|
+
* `type_name` — overrides the default GraphQL type name derived from the model
|
|
332
|
+
(object types, mutations, and Rails enum GraphQL types use this basename).
|
|
333
|
+
* `only` / `except` (or `exclude`) — restrict columns on object types and
|
|
334
|
+
assignable mutation keys (the primary key remains readable as `id`).
|
|
335
|
+
* `configure` — `field` and resolver methods are evaluated on the generated
|
|
336
|
+
object type class.
|
|
337
|
+
|
|
338
|
+
### Run action return types (batch / member / collection mutations)
|
|
339
|
+
|
|
340
|
+
Return type and optional resolver for each kind live together on
|
|
341
|
+
`ActiveAdmin::GraphQL::RunActionMutationConfig` (`payload_type`, `resolve_proc`),
|
|
342
|
+
exposed through the DSL in a graphql-ruby-like way.
|
|
343
|
+
|
|
344
|
+
Use a `GraphQL::Schema::Object` subclass — typically subclass
|
|
345
|
+
`ActiveAdmin::GraphQL::RunActionPayload` — then add fields and resolver methods.
|
|
346
|
+
The mutation object passed to field methods is always
|
|
347
|
+
`RunActionPayload::Result` (`ok`, `status`, `location`, `body`).
|
|
348
|
+
|
|
349
|
+
Grouped (recommended):
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
graphql do
|
|
353
|
+
run_action_payload_type(AppDefaultRunPayload) # optional default for all three kinds
|
|
354
|
+
|
|
355
|
+
batch_action_mutation do
|
|
356
|
+
type(Batches::BatchRunPayload)
|
|
357
|
+
resolve do |proxy:, batch_action:, ids:, inputs:, **kw|
|
|
358
|
+
# optional; same keyword args as resolve_batch_action
|
|
359
|
+
proxy.run_batch_action(batch_action, ids, inputs: inputs)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
member_action_mutation do
|
|
364
|
+
type(Members::MemberRunPayload)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
member_action_mutation(:send_invoice) do
|
|
368
|
+
type(Members::SendInvoicePayload)
|
|
369
|
+
arguments do
|
|
370
|
+
argument :note, GraphQL::Types::String, required: false, camelize: false
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Call the named field as `posts_member_send_invoice(id: "...", note: "…")`. Extra
|
|
377
|
+
arguments are merged with `params` for the controller and passed as keyword args
|
|
378
|
+
to a custom `resolve` block.
|
|
379
|
+
|
|
380
|
+
With no symbol argument, `member_action_mutation { … }` configures only the
|
|
381
|
+
aggregate `posts_member_action` field. With a symbol, it configures the
|
|
382
|
+
per-action field `posts_member_<name>`.
|
|
383
|
+
|
|
384
|
+
Inside each `*_mutation` block, `type` (alias `payload_type`) sets the GraphQL
|
|
385
|
+
object type and `resolve` replaces the Ruby body for that field only. You can
|
|
386
|
+
set just `type`, just `resolve`, or both; order inside the block does not matter.
|
|
387
|
+
|
|
388
|
+
One-shot aliases (same underlying config):
|
|
389
|
+
|
|
390
|
+
| DSL method | Effect |
|
|
391
|
+
|------------|--------|
|
|
392
|
+
| `run_action_payload_type(MyType)` | Default return type when a kind-specific `type` is not set. |
|
|
393
|
+
| `batch_action_run_action_payload_type(MyType)` | Same as `batch_action_mutation { type(MyType) }`. |
|
|
394
|
+
| `member_action_run_action_payload_type(MyType)` | Same as `member_action_mutation { type(MyType) }`. |
|
|
395
|
+
| `collection_action_run_action_payload_type(MyType)` | Same as `collection_action_mutation { type(MyType) }`. |
|
|
396
|
+
| `resolve_batch_action { … }` | Same as `batch_action_mutation { resolve { … } }` (and likewise for member/collection). |
|
|
397
|
+
|
|
398
|
+
Per-kind `type` wins over `run_action_payload_type`; any unset kind falls back
|
|
399
|
+
to `run_action_payload_type`, then `ActiveAdminRunActionPayload`. Set
|
|
400
|
+
`graphql_name` on your subclass so introspection names stay stable.
|
|
401
|
+
|
|
402
|
+
Per action name: use `member_action_mutation(:action_name)` /
|
|
403
|
+
`collection_action_mutation(:action_name)` so each Registered action gets its
|
|
404
|
+
own mutation field, return type, resolver, and optional `arguments` block. The
|
|
405
|
+
aggregate `posts_member_action` / `posts_collection_action` fields remain for
|
|
406
|
+
backwards compatibility and for resources that do not need per-action typing.
|
|
407
|
+
|
|
408
|
+
### Resolver overrides (`resolve_*`)
|
|
409
|
+
|
|
410
|
+
Field names and GraphQL types stay defined by Active Admin; these procs only
|
|
411
|
+
replace the Ruby body that loads data or runs a mutation (similar in spirit to
|
|
412
|
+
overriding `find_resource` / `find_collection` rather than inventing new field
|
|
413
|
+
names):
|
|
414
|
+
|
|
415
|
+
| DSL method | Replaces default … |
|
|
416
|
+
|------------|-------------------|
|
|
417
|
+
| `resolve_index` (alias `resolve_collection`) | List connection resolver; must return an `ActiveRecord::Relation` (or compatible). Receives `proxy`, `context`, `auth`, `aa_resource`, `graph_params`, and the list field arguments (`filter`, `scope`, `q`, `order`, parent ids, …). |
|
|
418
|
+
| `resolve_show` (alias `resolve_member`) | Singular field and `registered_resource` for this resource; must return a record or `nil`. Receives `proxy`, `id`, `graph_params`, and for the generated `post` field also `where` / `id_argument` when present. |
|
|
419
|
+
| `resolve_create` | After class-level create authorization: build and persist. Receives `proxy`, `input`, `attributes` (assignable slice), `context`, `auth`, `aa_resource`. Must return the created record; instance-level `authorized?` runs afterward. |
|
|
420
|
+
| `resolve_update` | After load and update authorization: apply changes. Receives `record`, `input`, `attributes`, plus `proxy`, `context`, `auth`, `aa_resource`. Must return the updated record. |
|
|
421
|
+
| `resolve_destroy` | After destroy authorization: delete or archive. Receives `record`, `proxy`, `context`, `auth`, `aa_resource`. Default is `record.destroy!`. |
|
|
422
|
+
| `resolve_batch_action` | Batch mutation; receives `batch_action`, `ids`, `inputs` (string-key `Hash` after converting GraphQL key/value list), `proxy`, … Default calls `ResourceQueryProxy#run_batch_action`. |
|
|
423
|
+
| `resolve_member_action` | Member-action mutation; receives `action`, `id`, `params` (string-key `Hash`), `proxy`, … |
|
|
424
|
+
| `resolve_collection_action` | Collection-action mutation; receives `action`, `params` (string-key `Hash`), `proxy`, … |
|
|
425
|
+
|
|
426
|
+
Standard authorization checks around each field or mutation still run; hooks
|
|
427
|
+
are for customizing how data is fetched or saved, not for skipping policy.
|
|
428
|
+
|
|
429
|
+
### Rails enum columns
|
|
430
|
+
|
|
431
|
+
Attributes backed by `enum` (integer or string columns with `defined_enums`) are
|
|
432
|
+
exposed as GraphQL enums. Each enum type is named
|
|
433
|
+
|
|
434
|
+
`{GraphQLTypeBasename}Enum{ColumnNameInPascalCase}`
|
|
435
|
+
|
|
436
|
+
where `GraphQLTypeBasename` is the resource’s GraphQL type name (from `type_name`
|
|
437
|
+
or the model name), and `ColumnNameInPascalCase` is the database attribute in
|
|
438
|
+
PascalCase (for example `notification_channel` → `NotificationChannel`).
|
|
439
|
+
|
|
440
|
+
Examples:
|
|
441
|
+
|
|
442
|
+
* `User` + column `notification_channel` → `UserEnumNotificationChannel`
|
|
443
|
+
* `UserNotification` + column `channel` → `UserNotificationEnumChannel`
|
|
444
|
+
|
|
445
|
+
Think of `PostEnumStatus` as the enum for `Post`’s `status` attribute, not as a
|
|
446
|
+
single merged word like `PostStatus`. Putting `Enum` immediately after the
|
|
447
|
+
resource basename (instead of a suffix such as `PostStatusEnum`) keeps the
|
|
448
|
+
basename and column distinct when both are camelized. That avoids the duplicates
|
|
449
|
+
graphql-ruby rejects when unrelated pairs would otherwise share one type name
|
|
450
|
+
(for example joining basename and column with a single underscore and camelizing
|
|
451
|
+
the whole string, or suffixing `Enum` after a fused `basename` + `column`
|
|
452
|
+
string).
|
|
453
|
+
|
|
454
|
+
If you still need to disambiguate or narrow the surface, you can:
|
|
455
|
+
|
|
456
|
+
* set `type_name` on a resource so its enums get a different prefix;
|
|
457
|
+
* use `only` / `except` to omit an enum attribute from GraphQL;
|
|
458
|
+
* define a custom field in `configure` that uses your own shared enum type.
|
|
459
|
+
|
|
460
|
+
## Custom pages
|
|
461
|
+
|
|
462
|
+
`register_page` can add query fields:
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
ActiveAdmin.register_page "Dashboard" do
|
|
466
|
+
graphql_field :open_tickets_count, GraphQL::Types::Int, null: false,
|
|
467
|
+
description: "Tickets still open" do |_user, _ctx|
|
|
468
|
+
Ticket.open.count
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The block receives `(current_user, context)`. Authorization uses the page as the
|
|
474
|
+
subject for read access.
|
|
475
|
+
|
|
476
|
+
## Custom schema class hook
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
config.namespace :admin do |admin|
|
|
480
|
+
admin.graphql = true
|
|
481
|
+
admin.graphql_configure_schema = lambda do |schema|
|
|
482
|
+
# schema.max_complexity(...) etc. if not using namespace settings
|
|
483
|
+
# schema.use MyGraphQLExtension
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAdmin
|
|
4
|
+
module GraphQL
|
|
5
|
+
# Authorization adapter wrapper for GraphQL +context+.
|
|
6
|
+
class AuthContext
|
|
7
|
+
attr_reader :user, :namespace
|
|
8
|
+
|
|
9
|
+
def initialize(user:, namespace:)
|
|
10
|
+
@user = user
|
|
11
|
+
@namespace = namespace
|
|
12
|
+
@adapter_by_resource_id = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def adapter_for(aa_resource)
|
|
16
|
+
@adapter_by_resource_id[aa_resource.object_id] ||= begin
|
|
17
|
+
klass = namespace.authorization_adapter
|
|
18
|
+
klass = klass.constantize if klass.is_a?(String)
|
|
19
|
+
klass.new(aa_resource, user)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authorized?(aa_resource, action, subject = nil)
|
|
24
|
+
adapter_for(aa_resource).authorized?(action, subject)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Delegates to the namespace authorization adapter. Built-in resolvers scope
|
|
28
|
+
# collections through +ResourceController+ instead of calling this; it remains
|
|
29
|
+
# available for custom schema extensions or host-app glue code.
|
|
30
|
+
def scope_collection(aa_resource, relation, action = ActiveAdmin::Authorization::READ)
|
|
31
|
+
adapter_for(aa_resource).scope_collection(relation, action)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource_definition_dsl"
|
|
4
|
+
|
|
5
|
+
module ActiveAdmin
|
|
6
|
+
module GraphQL
|
|
7
|
+
# Hooks graphql configuration and routing into ActiveAdmin when this gem is loaded.
|
|
8
|
+
module Integration
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def install!
|
|
12
|
+
return if @installed
|
|
13
|
+
|
|
14
|
+
@installed = true
|
|
15
|
+
ActiveAdmin::Application.prepend(ApplicationUnloadClearsGraphQLSchema)
|
|
16
|
+
register_namespace_settings!
|
|
17
|
+
ActiveAdmin::Resource.prepend(ResourceMethods)
|
|
18
|
+
ActiveAdmin::ResourceDSL.prepend(ResourceDSLMethods)
|
|
19
|
+
ActiveAdmin::Page.prepend(PageMethods)
|
|
20
|
+
ActiveAdmin::PageDSL.prepend(PageDSLMethods)
|
|
21
|
+
ActiveAdmin::Router.prepend(RouterMethods)
|
|
22
|
+
register_railtie!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def register_namespace_settings!
|
|
26
|
+
ns = ActiveAdmin::NamespaceSettings
|
|
27
|
+
ns.register :graphql, false
|
|
28
|
+
ns.register :graphql_path, "graphql"
|
|
29
|
+
ns.register :graphql_multiplex_max, 20
|
|
30
|
+
ns.register :graphql_dataloader, nil
|
|
31
|
+
ns.register :graphql_visibility, nil
|
|
32
|
+
ns.register :graphql_visibility_profile, nil
|
|
33
|
+
ns.register :graphql_schema_visible, nil
|
|
34
|
+
ns.register :graphql_max_complexity, nil
|
|
35
|
+
ns.register :graphql_max_depth, nil
|
|
36
|
+
ns.register :graphql_default_page_size, nil
|
|
37
|
+
ns.register :graphql_default_max_page_size, nil
|
|
38
|
+
ns.register :graphql_configure_schema, nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_railtie!
|
|
42
|
+
return unless defined?(Rails::Railtie)
|
|
43
|
+
|
|
44
|
+
require_relative "railtie"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Stale cached schemas keep references to resources/controllers cleared by +unload!+
|
|
48
|
+
# (e.g. specs that reload registrations). Always drop cached schema when AA unloads.
|
|
49
|
+
module ApplicationUnloadClearsGraphQLSchema
|
|
50
|
+
def unload!
|
|
51
|
+
super
|
|
52
|
+
ActiveAdmin::GraphQL.clear_schema_cache!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module ResourceMethods
|
|
57
|
+
def graphql_config
|
|
58
|
+
@graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attributes_for_graphql
|
|
62
|
+
keys = resource_attributes.keys
|
|
63
|
+
cfg = graphql_config
|
|
64
|
+
if cfg.only_attributes
|
|
65
|
+
keys &= cfg.only_attributes
|
|
66
|
+
end
|
|
67
|
+
keys -= cfg.exclude_attributes
|
|
68
|
+
keys
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def graphql_assignable_attribute_names
|
|
72
|
+
names = attributes_for_graphql.map(&:to_s)
|
|
73
|
+
pk_cols = ActiveAdmin::PrimaryKey.columns(resource_class)
|
|
74
|
+
return names if pk_cols.size > 1
|
|
75
|
+
|
|
76
|
+
names - pk_cols
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module ResourceDSLMethods
|
|
81
|
+
def graphql(&block)
|
|
82
|
+
if block
|
|
83
|
+
ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
|
|
84
|
+
end
|
|
85
|
+
config.graphql_config
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
module PageMethods
|
|
90
|
+
def graphql_fields
|
|
91
|
+
@graphql_fields ||= []
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module PageDSLMethods
|
|
96
|
+
def graphql_field(field_name, graphql_type, null: true, description: nil, &block)
|
|
97
|
+
config.graphql_fields << {
|
|
98
|
+
name: field_name.to_s,
|
|
99
|
+
type: graphql_type,
|
|
100
|
+
null: null,
|
|
101
|
+
description: description,
|
|
102
|
+
resolver: block
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module RouterMethods
|
|
108
|
+
def apply
|
|
109
|
+
define_root_routes
|
|
110
|
+
define_graphql_routes
|
|
111
|
+
define_resources_routes
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def define_graphql_routes
|
|
117
|
+
namespaces.each do |namespace|
|
|
118
|
+
next unless namespace.graphql
|
|
119
|
+
|
|
120
|
+
segment = namespace.graphql_path.to_s.delete_prefix("/").presence || "graphql"
|
|
121
|
+
defaults = {active_admin_namespace: namespace.name}
|
|
122
|
+
|
|
123
|
+
if namespace.root?
|
|
124
|
+
router.post segment, controller: "/active_admin/graphql", action: "execute", defaults: defaults
|
|
125
|
+
else
|
|
126
|
+
router.namespace namespace.name, **namespace.route_options.dup do
|
|
127
|
+
router.post segment, controller: "/active_admin/graphql", action: "execute", defaults: defaults
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|