activeadmin-graphql 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -3
  3. data/README.md +2 -2
  4. data/docs/changelogs/20260818111600_rfc-0002-problems.md +1 -1
  5. data/docs/changelogs/20260818114000_rfc-as-built-coverage.md +1 -1
  6. data/docs/changelogs/20260818114700_rfc-design-not-version.md +1 -1
  7. data/docs/changelogs/20260907141000_advisory-lockfile-refresh.md +1 -1
  8. data/docs/changelogs/20260921103000_mutation-attribute-allowlists.md +1 -1
  9. data/docs/changelogs/20260921111000_release-0-3-0.md +1 -1
  10. data/docs/changelogs/20260921112800_drop-standard-rubocop-direct.md +1 -1
  11. data/docs/changelogs/20260921120000_graphql-floor-and-activeadmin-4.md +1 -1
  12. data/docs/changelogs/20261010091415_graphql-resource-enabled-default.md +1 -1
  13. data/docs/changelogs/20261010103800_release-0-3-1.md +1 -1
  14. data/docs/changelogs/20261010110600_graphql-identity-dsl.md +4 -0
  15. data/docs/changelogs/20261010111800_release-0-3-2.md +1 -1
  16. data/docs/changelogs/20261012050051_graphql-surface-toggles.md +1 -1
  17. data/docs/changelogs/20261012064500_graphql-capabilities.md +1 -1
  18. data/docs/changelogs/20261012113500_release-0-3-3.md +1 -1
  19. data/docs/changelogs/20261012200000_host-customization-hooks.md +31 -0
  20. data/docs/changelogs/20261013080000_release-0-4-0.md +48 -0
  21. data/docs/changelogs/20261103145000_release-0-4-1.md +26 -0
  22. data/docs/graphql-api.md +154 -102
  23. data/docs/issues/20260907141000_engineering-and-dependency-audit.md +1 -1
  24. data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +1 -1
  25. data/docs/issues/20260921103000_mutation-attribute-allowlists.md +1 -1
  26. data/docs/issues/20260921104400_convention-alignment-audit.md +7 -7
  27. data/docs/issues/20260921114500_dependency-audit.md +13 -13
  28. data/docs/issues/20260921115500_file-length-metrics.md +1 -1
  29. data/docs/issues/20261010111159_rails-composite-primary-keys-alignment.md +13 -13
  30. data/docs/issues/20261010111300_partitions-and-shards-support.md +3 -3
  31. data/docs/issues/20261010111500_graphql-federation-support.md +3 -3
  32. data/docs/issues/20261012200000_host-customization-gaps.md +30 -0
  33. data/docs/issues/20261103145000_engineering-prose-release-audit.md +39 -0
  34. data/docs/plan/20260817201500_rfc-starter.md +1 -1
  35. data/docs/reviews/20261028_engineering-audit-release-preparation.md +24 -0
  36. data/docs/reviews/20261103145000_engineering-prose-release-audit.md +38 -0
  37. data/lib/active_admin/graphql/capability_definition_dsl.rb +9 -4
  38. data/lib/active_admin/graphql/collection_query_policy.rb +103 -0
  39. data/lib/active_admin/graphql/integration.rb +24 -0
  40. data/lib/active_admin/graphql/member_action_capability.rb +14 -0
  41. data/lib/active_admin/graphql/member_action_dry_run.rb +121 -0
  42. data/lib/active_admin/graphql/resource_capability_dsl.rb +4 -3
  43. data/lib/active_admin/graphql/resource_capability_maps.rb +20 -2
  44. data/lib/active_admin/graphql/resource_definition_dsl.rb +17 -6
  45. data/lib/active_admin/graphql/run_action_capability.rb +62 -0
  46. data/lib/active_admin/graphql/run_action_control_extension.rb +72 -0
  47. data/lib/active_admin/graphql/run_action_field.rb +103 -0
  48. data/lib/active_admin/graphql/run_action_mutation_config.rb +8 -0
  49. data/lib/active_admin/graphql/run_action_mutation_dsl.rb +18 -0
  50. data/lib/active_admin/graphql/run_action_preview.rb +20 -0
  51. data/lib/active_admin/graphql/schema_builder/mutation_batch.rb +2 -2
  52. data/lib/active_admin/graphql/schema_builder/mutation_collection.rb +4 -3
  53. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +20 -20
  54. data/lib/active_admin/graphql/schema_builder/mutation_member.rb +8 -3
  55. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -4
  56. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -4
  57. data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +1 -4
  58. data/lib/active_admin/graphql/schema_builder/resolvers.rb +46 -7
  59. data/lib/active_admin/graphql/schema_builder/types_object.rb +2 -1
  60. data/lib/active_admin/graphql/schema_builder/wire.rb +2 -1
  61. data/lib/active_admin/graphql/schema_field.rb +85 -10
  62. data/lib/active_admin/graphql/version.rb +1 -1
  63. data/lib/active_admin/graphql.rb +7 -0
  64. metadata +15 -1
data/docs/graphql-api.md CHANGED
@@ -31,11 +31,11 @@ want:
31
31
  ActiveAdmin.setup do |config|
32
32
  config.namespace :admin do |admin|
33
33
  admin.graphql = true
34
- # Optional — default is "graphql", i.e. POST /admin/graphql
34
+ # Optional - default is "graphql", i.e. POST /admin/graphql
35
35
  # admin.graphql_path = "graphql"
36
- # Optional — set false so only resources with a graphql block enter the schema
36
+ # Optional - set false so only resources with a graphql block enter the schema
37
37
  # admin.graphql_resource_enabled_default = false
38
- # Optional — trim always-on surfaces for a narrow API
38
+ # Optional - trim always-on surfaces for a narrow API
39
39
  # admin.graphql_policies_enabled = false
40
40
  # admin.graphql_registered_resource_enabled = false
41
41
  # admin.graphql_belongs_to_associations_enabled = false
@@ -50,71 +50,87 @@ Run `bin/rails active_admin:install` or reload routes if needed so
50
50
 
51
51
  Each namespace inherits these options (defaults in parentheses):
52
52
 
53
- * `graphql` — set to `true` to mount the endpoint (`false` by default).
54
- * `graphql_resource_enabled_default` — whether each registered ActiveRecord
53
+ * `graphql` - set to `true` to mount the endpoint (`false` by default).
54
+ * `graphql_resource_enabled_default` - whether each registered ActiveRecord
55
55
  resource is included in the schema without a `graphql` block (`true` by
56
56
  default, matching 0.3.x). Set to `false` for opt-in: only resources that call
57
57
  `graphql { }` (or `graphql { enable! }`) appear in the schema. `disable!`
58
58
  still omits a resource.
59
- * `graphql_queries_enabled_default` — seed for per-resource read fields
59
+ * `graphql_queries_enabled_default` - seed for per-resource read fields
60
60
  (`true` by default): named list/show fields and membership in
61
61
  `registered_resource` / `ActiveAdminRegisteredResource`. Set `false` and use
62
62
  `enable_queries!` for opt-in reads. Object types may still exist for mutations.
63
- * `graphql_mutations_enabled_default` — seed for per-resource CRUD mutations
63
+ * `graphql_mutations_enabled_default` - seed for per-resource CRUD mutations
64
64
  (`true` by default). Set `false` and use `enable_mutations!` for opt-in writes.
65
- * `graphql_run_actions_enabled_default` — seed for batch/member/collection
65
+ * `graphql_run_actions_enabled_default` - seed for batch/member/collection
66
66
  action mutations (`true` by default).
67
- * `graphql_policies_enabled` — publish `activeadmin_policies` query fields and
67
+ * `graphql_policies_enabled` - publish `activeadmin_policies` query fields and
68
68
  allow per-object policy fields (`true` by default).
69
- * `graphql_policies_enabled_default` — seed for per-resource `policies_enabled`
69
+ * `graphql_policies_enabled_default` - seed for per-resource `policies_enabled`
70
70
  (`true` by default). Use `enable_policies!` / `disable_policies!` per resource.
71
- * `graphql_policy_adapter` — class that builds `activeadmin_policies` payloads
71
+ * `graphql_policy_adapter` - class that builds `activeadmin_policies` payloads
72
72
  (`ActiveAdmin::GraphQL::PolicyAdapter` by default). Set a subclass (or constant
73
73
  `String`) to replace payload logic; the default adapter still honors
74
74
  `graphql_policy_actions` / `graphql_policy_action_mapper` /
75
75
  `graphql_policy_extra` / `graphql_policy_transform`.
76
- * `graphql_registered_resource_enabled` — include `registered_resource` and the
76
+ * `graphql_registered_resource_enabled` - include `registered_resource` and the
77
77
  `ActiveAdminRegisteredResource` union when at least one queryable resource
78
78
  exists (`true` by default).
79
- * `graphql_belongs_to_associations_enabled` — wire non-polymorphic `belongs_to`
79
+ * `graphql_belongs_to_associations_enabled` - wire non-polymorphic `belongs_to`
80
80
  fields between registered resource types (`true` by default).
81
- * `graphql_path` — path segment under the namespace mount (`"graphql"`).
82
- * `graphql_multiplex_max` — maximum operations per multiplexed JSON array
81
+ * `graphql_path` - path segment under the namespace mount (`"graphql"`).
82
+ * `graphql_multiplex_max` - maximum operations per multiplexed JSON array
83
83
  request (`20`).
84
- * `graphql_max_complexity` — passed to graphql-ruby `max_complexity` (`300` by
84
+ * `graphql_max_complexity` - passed to graphql-ruby `max_complexity` (`300` by
85
85
  default; set a higher value if legitimate queries are rejected). Set to
86
86
  `nil` to disable the limit.
87
- * `graphql_max_depth` — passed to `max_depth` (`18` by default). Set to `nil`
87
+ * `graphql_max_depth` - passed to `max_depth` (`18` by default). Set to `nil`
88
88
  to disable.
89
- * `graphql_batch_action_max_ids` — maximum length of the `ids` argument on
89
+ * `graphql_batch_action_max_ids` - maximum length of the `ids` argument on
90
90
  `*_batch_action` mutations (`5000` by default; set `0` to disable the cap).
91
- * `graphql_default_page_size` / `graphql_default_max_page_size` — schema
91
+ * `graphql_default_page_size` / `graphql_default_max_page_size` - schema
92
92
  connection defaults (`25` / `100`); needed for graphql-ruby complexity scoring
93
93
  on connection fields when `max_complexity` is enabled.
94
- * `graphql_dataloader` — graphql-ruby dataloader plugin (`nil` = use
94
+ * `graphql_dataloader` - graphql-ruby dataloader plugin (`nil` = use
95
95
  `GraphQL::Dataloader`). Set to `GraphQL::Dataloader::AsyncDataloader` when
96
96
  the [`async` gem](https://github.com/socketry/async) is loaded if you want
97
97
  parallel I/O from custom `GraphQL::Dataloader::Source` `#fetch` methods.
98
- * `graphql_visibility` — `nil` by default (no graphql-ruby visibility plugin).
98
+ * `graphql_visibility` - `nil` by default (no graphql-ruby visibility plugin).
99
99
  Set to `true` to use [`GraphQL::Schema::Visibility`](https://graphql-ruby.org/schema/visibility.html)
100
100
  with default options, or pass a `Hash` of visibility options (`profiles:`,
101
- `dynamic:`, `preload:`, `migration_errors:`, …).
102
- * `graphql_visibility_profile` — when using named visibility profiles, optional
101
+ `dynamic:`, `preload:`, `migration_errors:`, ...).
102
+ * `graphql_visibility_profile` - when using named visibility profiles, optional
103
103
  default `context[:visibility_profile]` for each HTTP request (symbols accepted).
104
- * `graphql_schema_visible` — optional `Proc` `(context, meta) -> Boolean` when
104
+ * `graphql_schema_visible` - optional `Proc` `(context, meta) -> Boolean` when
105
105
  `graphql_visibility` is enabled; see Schema visibility below.
106
- * `graphql_capabilities` — optional `Proc` `(context) -> Enumerable` of capability
106
+ * `graphql_capabilities` - optional `Proc` `(context) -> Enumerable` of capability
107
107
  names for the request (RFC 0008). Normalized into `context[:capabilities]`.
108
108
  Cannot be combined with named `graphql_visibility` `profiles:`; schema build
109
109
  forces `dynamic: true` and omits `visibility_profile` from the request context
110
110
  so introspection uses the live capability set.
111
- * `graphql_configure_schema` — `Proc` receiving the schema class after build
111
+ * `graphql_field_capability_mode_default` - `:omit` or `:null` when a field has a
112
+ capability but no per-field mode (RFC 0009). Default `:omit`.
113
+ * `graphql_field_visible` - optional `Proc` `(context, meta) -> Boolean` to omit
114
+ fields after capability checks (RFC 0009). Meta includes `graphql_type_name`,
115
+ `field_name`, and `resource` when known.
116
+ * `graphql_run_action_audit` - optional `Proc` `(context, meta) -> void` after
117
+ member-action dry-run, refused preview, or completed run (RFC 0010).
118
+ * `graphql_collection_query_policy` - optional `Proc` `(context, meta) -> void`
119
+ at the start of collection resolve (RFC 0011). Meta includes `aa_resource`,
120
+ `q`, `scope`, `order`, and `filter`.
121
+ * `graphql_member_action_capability_default` - optional capability symbol applied
122
+ to member-action mutation fields when the resource map has no entry (RFC 0012).
123
+ * `graphql_batch_action_capability_default` - same for the aggregate batch-action
124
+ field and per-action authorize checks (RFC 0012).
125
+ * `graphql_collection_action_capability_default` - same for collection-action
126
+ mutation fields (RFC 0012).
127
+ * `graphql_configure_schema` - `Proc` receiving the schema class after build
112
128
  (extensions, extra types, and so on).
113
129
 
114
130
  ### GraphQL::Dataloader
115
131
 
116
132
  The namespace schema calls `schema.use` with the configured dataloader (see
117
- `graphql_dataloader` above). [graphql-ruby’s
133
+ `graphql_dataloader` above). [graphql-ruby's
118
134
  dataloader](https://graphql-ruby.org/dataloader/overview.html) batches external
119
135
  work across sibling fields using Fibers; each HTTP request gets its own
120
136
  dataloader instance (available as `context.dataloader` inside resolvers).
@@ -136,12 +152,12 @@ Global / Relay-style IDs are not generated by Active Admin itself.
136
152
  ### Schema visibility (`GraphQL::Schema::Visibility`)
137
153
 
138
154
  When `graphql_visibility` is set, the namespace schema calls
139
- `schema.use(GraphQL::Schema::Visibility, …)` so introspection and validation
155
+ `schema.use(GraphQL::Schema::Visibility, ...)` so introspection and validation
140
156
  respect [visibility](https://graphql-ruby.org/schema/visibility.html).
141
157
 
142
158
  Set `graphql_schema_visible` to a proc that receives the GraphQL `context`
143
159
  and a `meta` hash and returns truthy to keep the member visible. The hash
144
- always includes `:kind`, and often `:graphql_type_name` (the resource’s GraphQL
160
+ always includes `:kind`, and often `:graphql_type_name` (the resource's GraphQL
145
161
  basename), `:resource` (`ActiveAdmin::Resource`), and other keys:
146
162
 
147
163
  | `kind` | Meaning |
@@ -149,29 +165,29 @@ basename), `:resource` (`ActiveAdmin::Resource`), and other keys:
149
165
  | `:resource_interface` | `ActiveAdminResource` interface |
150
166
  | `:resource_object` | Resource object type |
151
167
  | `:resource_enum` | Rails enum type; `meta` includes `:column` |
152
- | `:input_object` | Create/update/where/list-filter input; `meta` includes `:role` (`:create_input`, …) |
168
+ | `:input_object` | Create/update/where/list-filter input; `meta` includes `:role` (`:create_input`, ...) |
153
169
  | `:registered_resource_union` | `ActiveAdminRegisteredResource` union |
154
170
  | `:run_action_payload` | Batch/member/collection action payload type |
155
171
  | `:query_registered_resource` | `registered_resource` query field |
156
172
  | `:query_collection_field` / `:query_member_field` | Resource list/detail fields; `meta` includes `:field_name` |
157
173
  | `:query_page_field` | Custom `register_page` field; `meta` includes `:page` |
158
- | `:mutation_create`, `:mutation_update`, `:mutation_delete`, … | CRUD and action mutations |
174
+ | `:mutation_create`, `:mutation_update`, `:mutation_delete`, ... | CRUD and action mutations |
159
175
 
160
176
  Root `Query` / `Mutation` fields are defined with
161
177
  `ActiveAdmin::GraphQL::SchemaField`, which supports a `visibility:` keyword:
162
178
  a metadata `Hash` (same shape as the `meta` argument above) for
163
179
  `graphql_schema_visible`, not a graphql-ruby core option. Resource `graphql`
164
180
  `configure` blocks use that field class too, so custom fields may pass
165
- `visibility: { … }` when you want the hook to run.
181
+ `visibility: { ... }` when you want the hook to run.
166
182
 
167
183
  You can also subclass your own `GraphQL::Schema::Object` / `Field` types in
168
- `graphql_configure_schema` and implement `visible?` there; Active Admin’s
184
+ `graphql_configure_schema` and implement `visible?` there; Active Admin's
169
185
  helpers call `super` first so those implementations compose.
170
186
 
171
187
  Interfaces and unions: graphql-ruby may keep an interface or union visible
172
188
  when all members are hidden unless you return `false` from
173
189
  `graphql_schema_visible` for `:resource_interface` or `:registered_resource_union`
174
- (or implement `.visible?` on custom types). See graphql-ruby’s visibility migration
190
+ (or implement `.visible?` on custom types). See graphql-ruby's visibility migration
175
191
  notes for edge cases.
176
192
 
177
193
  ## Making requests
@@ -184,11 +200,11 @@ Send JSON with `query` and optional `variables`. For
184
200
  be a JSON array of such objects; the size is limited by `graphql_multiplex_max`.
185
201
 
186
202
  Requests are handled by `ActiveAdmin::GraphqlController` (an engine controller,
187
- not a generated `Admin::…` subclass).
203
+ not a generated `Admin::...` subclass).
188
204
 
189
205
  ### CSRF and cookie sessions
190
206
 
191
- `GraphqlController` uses Rails’ default CSRF protection
207
+ `GraphqlController` uses Rails' default CSRF protection
192
208
  (`protect_from_forgery with: :exception`). Clients that rely on session cookies
193
209
  (for example a typical Devise session) must submit a valid CSRF token with each
194
210
  `POST`, such as the `authenticity_token` used by the HTML admin. API-style
@@ -235,6 +251,11 @@ resource model (`authorized?` with the model class, in the same way as
235
251
  collection queries). They then run the same controller actions and blocks as the
236
252
  UI, including any `authorize!` or `authorize_resource!` calls.
237
253
 
254
+ When a record exists but instance-level read is denied, singular member and
255
+ `registered_resource` queries return `null` (same shape as a missing id). Update
256
+ and destroy mutations return the GraphQL error `not found` for the same case.
257
+ Class-level denials still raise `not authorized ...`.
258
+
238
259
  See the ActiveAdmin guide [Authorization adapter](https://activeadmin.info/13-authorization-adapter) for adapter setup; the same rules apply to GraphQL.
239
260
 
240
261
  Policy preflight fields (`activeadmin_policies`, `activeadmin_policies_for`, and
@@ -270,7 +291,7 @@ end
270
291
 
271
292
  Kwargs (`fields:`, `queries:`, `mutations:`, `member_actions:`,
272
293
  `batch_actions:`, `collection_actions:`) work without a block. Query aliases:
273
- `index` / `list` → collection, `show` → member. Mutation alias: `delete` →
294
+ `index` / `list` -> collection, `show` -> member. Mutation alias: `delete` ->
274
295
  `destroy`. Run-action sugar builds keys like `member_publish`.
275
296
 
276
297
  Grant capabilities per request with `graphql_capabilities`. Map OAuth scopes or
@@ -284,14 +305,27 @@ admin.graphql_capabilities = lambda { |ctx|
284
305
 
285
306
  A gated member fails closed when the capability is missing from that set:
286
307
  resolve returns null for the field (graphql-ruby unauthorized field). Enable
287
- `graphql_visibility` so introspection and validation also omit ungated members.
308
+ `graphql_visibility` so introspection and validation also omit ungated members
309
+ when the field mode is `:omit` (default). Use `capability_mode: :null` (or
310
+ `capability :pii, fields: %i[body], mode: :null`) so the field stays in the
311
+ schema and resolves to `null` without the grant (RFC 0009).
312
+
288
313
  `fields:` applies to generated attributes and wired `belongs_to` associations.
289
314
  Unknown field names raise at schema build. Capabilities do not replace the
290
315
  authorization adapter.
291
316
 
292
- Custom `SchemaField` definitions may pass `capability: :name` the same way.
293
- Do not use named visibility `profiles:` together with `graphql_capabilities`
294
- (cached profile context ignores per-request grants).
317
+ Custom `SchemaField` definitions may pass `capability: :name` and
318
+ `capability_mode: :null` the same way. Set `graphql_field_visible` for a second
319
+ omit gate (for example a request allowlist). Do not use named visibility
320
+ `profiles:` together with `graphql_capabilities` (cached profile context ignores
321
+ per-request grants).
322
+
323
+ Collection list fields may call a host `graphql_collection_query_policy`. The
324
+ helper `ActiveAdmin::GraphQL::CollectionQueryPolicy.require_visible_fields!`
325
+ rejects Ransack `q` keys for hidden fields and requires capabilities from the
326
+ resource field map by default (needed for `capability_mode: :null` columns).
327
+ Optional `field_grants` overrides inference; `infer_field_grants: false` turns
328
+ it off (RFC 0011).
295
329
 
296
330
  ## Schema overview
297
331
 
@@ -335,7 +369,7 @@ omit `created_at` and `updated_at`.
335
369
  GraphQL `id` always comes from `ActiveAdminResource` (`ID!`). An ActiveRecord
336
370
  `id` column is never added as a second field. When `self.primary_key` is blank
337
371
  but the model still has an `id` column (common for database views that project
338
- `… AS id`), that column supplies the GraphQL `id` value. When the model has
372
+ `... AS id`), that column supplies the GraphQL `id` value. When the model has
339
373
  neither a primary key nor an `id` column, declare GraphQL identity on the
340
374
  resource (RFC 0007), set `self.primary_key` on the model, or omit the resource
341
375
  with `graphql { disable! }`. A single-column primary key named something other
@@ -355,7 +389,7 @@ end
355
389
  ```
356
390
 
357
391
  With `separator: ":"`, GraphQL `id` is `entry_type:entry_id:event_type`. Keep
358
- the model’s `def id` (if any) in the same shape so HTML and GraphQL share
392
+ the model's `def id` (if any) in the same shape so HTML and GraphQL share
359
393
  member ids. Without `separator`, multi-column identity uses the same JSON
360
394
  object string as composite ActiveRecord primary keys.
361
395
 
@@ -364,19 +398,19 @@ object string as composite ActiveRecord primary keys.
364
398
  For models with `self.primary_key = [:book_code, :seq]` (and no separate `id`
365
399
  column), Active Admin treats GraphQL as follows:
366
400
 
367
- * `id` on the object type — JSON object string with all key columns in
401
+ * `id` on the object type - JSON object string with all key columns in
368
402
  model order, for example `{"book_code":"CPK","seq":7}`. This is the GraphQL
369
403
  wire format. It is not the Rails form/URL underscore packing described in
370
404
  the Composite Primary Keys guide (`/books/2_25` with
371
405
  `params.extract_value(:id)`).
372
- * Readable fields — each primary-key column is also exposed as its own field
406
+ * Readable fields - each primary-key column is also exposed as its own field
373
407
  (in addition to `id`) so clients can load scalars without parsing JSON.
374
- * `LibraryEditionWhereInput` (and similar) — either optional `id` (that JSON
408
+ * `LibraryEditionWhereInput` (and similar) - either optional `id` (that JSON
375
409
  string) or every primary-key column; `id` remains required when there is
376
410
  only one key column.
377
- * Singular query field — pass the JSON `id`, pass `where`, or pass one
411
+ * Singular query field - pass the JSON `id`, pass `where`, or pass one
378
412
  GraphQL argument per primary-key column.
379
- * Create mutations — composite key attributes stay included in create
413
+ * Create mutations - composite key attributes stay included in create
380
414
  inputs (unlike single `id` tables, where the key is omitted from assignable
381
415
  attributes).
382
416
 
@@ -385,8 +419,8 @@ composite ids from `params[:id]` or from individual primary-key params. Stock
385
419
  ActiveAdmin 3.x only passes a single `params[:id]` into `find`; an ActiveAdmin
386
420
  fork (or patch) that implements composite-aware `find_resource` keeps the
387
421
  admin UI aligned with GraphQL. After decoding the GraphQL JSON `id`, the gem
388
- uses Rails’ array/tuple conventions for `Relation#find` and
389
- `where(primary_key => [tuple, …])` as in the guide.
422
+ uses Rails' array/tuple conventions for `Relation#find` and
423
+ `where(primary_key => [tuple, ...])` as in the guide.
390
424
 
391
425
  The gem ships `ActiveAdmin::PrimaryKey` when the core constant is missing
392
426
  (upstream). A fork may define `PrimaryKey` and extended `find_resource` in
@@ -399,18 +433,18 @@ Alignment notes versus the Rails guide:
399
433
 
400
434
  These align with REST index parameters where possible:
401
435
 
402
- * `filter` — optional `PostListFilterInput` with `scope`, `order`, `q` (Ransack
403
- predicates as JSON — kept as JSON because predicate keys are open-ended), and the nested parent id when `belongs_to` applies.
404
- * `scope` — menu scope id (string); still accepted on the field for backward
436
+ * `filter` - optional `PostListFilterInput` with `scope`, `order`, `q` (Ransack
437
+ predicates as JSON - kept as JSON because predicate keys are open-ended), and the nested parent id when `belongs_to` applies.
438
+ * `scope` - menu scope id (string); still accepted on the field for backward
405
439
  compatibility.
406
- * `q` — Ransack predicates (JSON object).
407
- * `order` — index sort string (for example `id_desc`).
408
- * Parent foreign key — when the resource uses `belongs_to`, the parent param
440
+ * `q` - Ransack predicates (JSON object).
441
+ * `order` - index sort string (for example `id_desc`).
442
+ * Parent foreign key - when the resource uses `belongs_to`, the parent param
409
443
  (for example `user_id`) stays available on the connection field as today.
410
444
 
411
445
  ### Singular query arguments
412
446
 
413
- * `where` — `PostWhereInput` with required `id` (single-key tables) or,
447
+ * `where` - `PostWhereInput` with required `id` (single-key tables) or,
414
448
  for composite keys, `id` (JSON) and/or each key column; optional parent ids for
415
449
  nested resources.
416
450
  * Legacy `id` / parent arguments behave as before (composite models also accept
@@ -420,12 +454,12 @@ These align with REST index parameters where possible:
420
454
 
421
455
  Typical names for a `Post` resource:
422
456
 
423
- * `create_post(input: PostCreateInput!)` — attribute fields match assignable
457
+ * `create_post(input: PostCreateInput!)` - attribute fields match assignable
424
458
  columns minus `created_at` / `updated_at` unless listed in `permit_params`
425
459
  (and the nested parent param when `belongs_to` is configured).
426
460
  * `update_post(where: PostWhereInput!, input: PostUpdateInput!)`
427
- * `delete_post(where: PostWhereInput!)` — kept for existing clients.
428
- * `destroy_post(where: PostWhereInput!)` — Rails / ActiveAdmin action name; same resolver as `delete_post`.
461
+ * `delete_post(where: PostWhereInput!)` - kept for existing clients.
462
+ * `destroy_post(where: PostWhereInput!)` - Rails / ActiveAdmin action name; same resolver as `delete_post`.
429
463
 
430
464
  Nested resources require the parent id on `where`, `input`, or list `filter`
431
465
  when the association is required.
@@ -436,39 +470,39 @@ If you are updating clients that targeted an older embedded GraphQL integration
436
470
 
437
471
  * 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.
438
472
 
439
- * 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.
473
+ * 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.
440
474
 
441
- * `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.
475
+ * `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.
442
476
 
443
- 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.
477
+ 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.
444
478
 
445
479
  ### `registered_resource` query
446
480
 
447
481
  `registered_resource(type_name: String!, id: ID!, path: [ActiveAdminKeyValuePair!])`
448
482
  returns `ActiveAdminRegisteredResource`, the union of all resource object types in
449
- the namespace. Pass nested parent route params as ordered `path: [{ key: "user_id", value: "1" }, …]`
483
+ the namespace. Pass nested parent route params as ordered `path: [{ key: "user_id", value: "1" }, ...]`
450
484
  (same keys as REST path segments). Resolve with inline fragments on concrete types.
451
485
 
452
- `ActiveAdminKeyValuePair` is `{ key: String!, value: String! }` — flat strings only, like query/form fields.
486
+ `ActiveAdminKeyValuePair` is `{ key: String!, value: String! }` - flat strings only, like query/form fields.
453
487
 
454
488
  ### Batch, member, and collection actions
455
489
 
456
490
  For a route key of `posts`, fields look like:
457
491
 
458
- * `posts_batch_action(batch_action: String!, ids: [ID!]!, inputs: [ActiveAdminKeyValuePair!], …)` —
492
+ * `posts_batch_action(batch_action: String!, ids: [ID!]!, inputs: [ActiveAdminKeyValuePair!], ...)` -
459
493
  runs the registered batch action; `inputs` is converted to `batch_action_inputs` (flat key/value map)
460
494
  like the HTML form. Omitted when batch actions are disabled or none are registered.
461
- * `posts_member_action(action: String!, id: ID!, params: [ActiveAdminKeyValuePair!], …)` —
495
+ * `posts_member_action(action: String!, id: ID!, params: [ActiveAdminKeyValuePair!], ...)` -
462
496
  aggregate field: pick the `member_action` by `action` string (same as today).
463
- * One GraphQL field per registered action — `{plural}_member_{action_name}` with
497
+ * One GraphQL field per registered action - `{plural}_member_{action_name}` with
464
498
  underscores (e.g. `posts_member_publish`, `posts_member_append_title_bang`): same
465
499
  `id`, `params`, and parent ids as the aggregate field, without an `action:` argument.
466
- Use `graphql { member_action_mutation(:publish) { … } }` to set return type,
467
- `resolve`, and extra `arguments { … }` for that action only.
468
- * `posts_collection_action(action: String!, params: [ActiveAdminKeyValuePair!], …)` —
500
+ Use `graphql { member_action_mutation(:publish) { ... } }` to set return type,
501
+ `resolve`, and extra `arguments { ... }` for that action only.
502
+ * `posts_collection_action(action: String!, params: [ActiveAdminKeyValuePair!], ...)` -
469
503
  aggregate collection actions by name.
470
504
  * Per collection action: `{plural}_collection_{action_name}` (e.g.
471
- `posts_collection_posts_count`). Configure with `collection_action_mutation(:posts_count) { … }`.
505
+ `posts_collection_posts_count`). Configure with `collection_action_mutation(:posts_count) { ... }`.
472
506
 
473
507
  Action names are validated against registered definitions; arbitrary controller
474
508
  methods are not exposed.
@@ -512,27 +546,27 @@ ActiveAdmin.register Post do
512
546
  end
513
547
  ```
514
548
 
515
- * `enable!` — include this resource when `graphql_resource_enabled_default` is
549
+ * `enable!` - include this resource when `graphql_resource_enabled_default` is
516
550
  `false`. An empty `graphql` block has the same effect.
517
- * `disable!` — resource is not included in Query or Mutation.
518
- * `enable_queries!` / `disable_queries!` — list/show fields and
551
+ * `disable!` - resource is not included in Query or Mutation.
552
+ * `enable_queries!` / `disable_queries!` - list/show fields and
519
553
  `registered_resource` membership for this resource.
520
- * `enable_mutations!` / `disable_mutations!` — create/update/delete mutations.
521
- * `enable_run_actions!` / `disable_run_actions!` — batch/member/collection
554
+ * `enable_mutations!` / `disable_mutations!` - create/update/delete mutations.
555
+ * `enable_run_actions!` / `disable_run_actions!` - batch/member/collection
522
556
  action mutations.
523
- * `enable_policies!` / `disable_policies!` — object `activeadmin_policies` and
557
+ * `enable_policies!` / `disable_policies!` - object `activeadmin_policies` and
524
558
  inclusion in the `activeadmin_policies` resources matrix.
525
- * `identity` — declare GraphQL id columns (and optional `separator`, `encode`,
559
+ * `identity` - declare GraphQL id columns (and optional `separator`, `encode`,
526
560
  `decode`) when the model has no ActiveRecord primary key and no `id` column
527
561
  (RFC 0007).
528
- * `type_name` (or `graphql_name`) — overrides the default GraphQL type name
562
+ * `type_name` (or `graphql_name`) - overrides the default GraphQL type name
529
563
  derived from the model (object types, mutations, and Rails enum GraphQL
530
564
  types use this basename).
531
- * `only` / `except` (or `exclude`) — restrict columns on object types. These
565
+ * `only` / `except` (or `exclude`) - restrict columns on object types. These
532
566
  names also cap mutation inputs. This is not ActiveAdmin `only:` / `except:`
533
567
  for controller actions (`action_item only: :show`). The primary key remains
534
568
  readable as `id`.
535
- * `permit_params` (or `permit`) — restrict create and update input keys without
569
+ * `permit_params` (or `permit`) - restrict create and update input keys without
536
570
  hiding query fields, same name as ActiveAdmin HTML `permit_params`. Nested
537
571
  `create` / `update` blocks replace that shared list for one operation and
538
572
  may set `resolve`. Names outside `only` / `except` are dropped. A static HTML
@@ -540,7 +574,7 @@ end
540
574
  unset. A request-time HTML `permit_params` block is not copied. With no list
541
575
  set, mutation inputs omit `created_at` and `updated_at`. Empty `permit_params`
542
576
  yields an input with no column arguments.
543
- * `configure` — `field` and resolver methods are evaluated on the generated
577
+ * `configure` - `field` and resolver methods are evaluated on the generated
544
578
  object type class.
545
579
 
546
580
  ### Run action return types (batch / member / collection mutations)
@@ -549,8 +583,8 @@ Return type and optional resolver for each kind live together on
549
583
  `ActiveAdmin::GraphQL::RunActionMutationConfig` (`payload_type`, `resolve_proc`),
550
584
  exposed through the DSL in a graphql-ruby-like way.
551
585
 
552
- Use a `GraphQL::Schema::Object` subclass — typically subclass
553
- `ActiveAdmin::GraphQL::RunActionPayload` — then add fields and resolver methods.
586
+ Use a `GraphQL::Schema::Object` subclass - typically subclass
587
+ `ActiveAdmin::GraphQL::RunActionPayload` - then add fields and resolver methods.
554
588
  The mutation object passed to field methods is always
555
589
  `RunActionPayload::Result` (`ok`, `status`, `location`, `body`).
556
590
 
@@ -572,8 +606,12 @@ graphql do
572
606
  type(Members::MemberRunPayload)
573
607
  end
574
608
 
575
- member_action_mutation(:send_invoice) do
609
+ member_action_mutation(:send_invoice, dry_run: true, capability: :support_act) do
576
610
  type(Members::SendInvoicePayload)
611
+ preview do |record, **|
612
+ record.may_send_invoice? ? ok("would_run:send_invoice:#{record.id}") : refuse("not_ready")
613
+ end
614
+ extra_params :note
577
615
  arguments do
578
616
  argument :note, GraphQL::Types::String, required: false, camelize: false
579
617
  end
@@ -581,11 +619,25 @@ graphql do
581
619
  end
582
620
  ```
583
621
 
584
- Call the named field as `posts_member_send_invoice(id: "...", note: "…")`. Extra
622
+ Call the named field as `posts_member_send_invoice(id: "...", note: "...")`. Extra
585
623
  arguments are merged with `params` for the controller and passed as keyword args
586
- to a custom `resolve` block.
587
-
588
- With no symbol argument, `member_action_mutation { … }` configures only the
624
+ to a custom `resolve` block. With `dry_run: true`, the field gains a Boolean
625
+ `dry_run` argument: preview runs first, dry-run returns without
626
+ `run_member_action`, and a refused preview never runs the action (RFC 0010).
627
+ `extra_params` allowlists which GraphQL kwargs forward into the AA request
628
+ (`id` cannot be rebound). `capability:` on batch / member / collection mutation
629
+ DSL overrides the resource map; otherwise the matching
630
+ `graphql_*_action_capability_default` applies (RFC 0012).
631
+
632
+ Custom mutations in `graphql_configure_schema` may use
633
+ `ActiveAdmin::GraphQL::RunActionField.define` (or `SchemaField` keywords
634
+ `dry_run:`, `confirm:`, `preview:`) for confirm/dry-run wiring (RFC 0013). When
635
+ `confirm` is required and both flags are false, the field returns
636
+ `body: "confirm_required"`. When `preview` is set, dry-run and refused preview
637
+ return without calling `resolve`; without `preview`, dry-run still calls
638
+ `resolve` with `dry_run: true`.
639
+
640
+ With no symbol argument, `member_action_mutation { ... }` configures only the
589
641
  aggregate `posts_member_action` field. With a symbol, it configures the
590
642
  per-action field `posts_member_<name>`.
591
643
 
@@ -601,7 +653,7 @@ One-shot aliases (same underlying config):
601
653
  | `batch_action_run_action_payload_type(MyType)` | Same as `batch_action_mutation { type(MyType) }`. |
602
654
  | `member_action_run_action_payload_type(MyType)` | Same as `member_action_mutation { type(MyType) }`. |
603
655
  | `collection_action_run_action_payload_type(MyType)` | Same as `collection_action_mutation { type(MyType) }`. |
604
- | `resolve_batch_action { … }` | Same as `batch_action_mutation { resolve { … } }` (and likewise for member/collection). |
656
+ | `resolve_batch_action { ... }` | Same as `batch_action_mutation { resolve { ... } }` (and likewise for member/collection). |
605
657
 
606
658
  Per-kind `type` wins over `run_action_payload_type`; any unset kind falls back
607
659
  to `run_action_payload_type`, then `ActiveAdminRunActionPayload`. Set
@@ -620,16 +672,16 @@ replace the Ruby body that loads data or runs a mutation (similar in spirit to
620
672
  overriding `find_resource` / `find_collection` rather than inventing new field
621
673
  names):
622
674
 
623
- | DSL method | Replaces default … |
675
+ | DSL method | Replaces default ... |
624
676
  |------------|-------------------|
625
- | `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, …). |
677
+ | `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, ...). |
626
678
  | `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. |
627
- | `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. |
679
+ | `resolve_create` | After class-level create authorization: build and persist inside a nested transaction. Receives `proxy`, `input`, `attributes` (assignable slice), `context`, `auth`, `aa_resource`. Must return the created record; instance-level `authorized?` runs before the transaction commits, so a denied hook persist rolls back when the hook shared that transaction. |
628
680
  | `resolve_update` | After load and update authorization: apply changes. Receives `record`, `input`, `attributes`, plus `proxy`, `context`, `auth`, `aa_resource`. Must return the updated record. |
629
681
  | `resolve_destroy` | After destroy authorization: delete or archive. Receives `record`, `proxy`, `context`, `auth`, `aa_resource`. Default is `record.destroy!`. |
630
- | `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`. |
631
- | `resolve_member_action` | Member-action mutation; receives `action`, `id`, `params` (string-key `Hash`), `proxy`, … |
632
- | `resolve_collection_action` | Collection-action mutation; receives `action`, `params` (string-key `Hash`), `proxy`, … |
682
+ | `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`. |
683
+ | `resolve_member_action` | Member-action mutation; receives `action`, `id`, `params` (string-key `Hash`), `proxy`, ... |
684
+ | `resolve_collection_action` | Collection-action mutation; receives `action`, `params` (string-key `Hash`), `proxy`, ... |
633
685
 
634
686
  Standard authorization checks around each field or mutation still run; hooks
635
687
  are for customizing how data is fetched or saved, not for skipping policy.
@@ -641,16 +693,16 @@ exposed as GraphQL enums. Each enum type is named
641
693
 
642
694
  `{GraphQLTypeBasename}Enum{ColumnNameInPascalCase}`
643
695
 
644
- where `GraphQLTypeBasename` is the resource’s GraphQL type name (from `type_name`
696
+ where `GraphQLTypeBasename` is the resource's GraphQL type name (from `type_name`
645
697
  or the model name), and `ColumnNameInPascalCase` is the database attribute in
646
- PascalCase (for example `notification_channel` → `NotificationChannel`).
698
+ PascalCase (for example `notification_channel` -> `NotificationChannel`).
647
699
 
648
700
  Examples:
649
701
 
650
- * `User` + column `notification_channel` → `UserEnumNotificationChannel`
651
- * `UserNotification` + column `channel` → `UserNotificationEnumChannel`
702
+ * `User` + column `notification_channel` -> `UserEnumNotificationChannel`
703
+ * `UserNotification` + column `channel` -> `UserNotificationEnumChannel`
652
704
 
653
- Think of `PostEnumStatus` as the enum for `Post`’s `status` attribute, not as a
705
+ Think of `PostEnumStatus` as the enum for `Post`'s `status` attribute, not as a
654
706
  single merged word like `PostStatus`. Putting `Enum` immediately after the
655
707
  resource basename (instead of a suffix such as `PostStatusEnum`) keeps the
656
708
  basename and column distinct when both are camelized. That avoids the duplicates
@@ -2,7 +2,7 @@ Recorded 2026-09-07. Library gem. Scheduled CI bundler-audit failed on json CVE-
2
2
 
3
3
  ## Participants
4
4
 
5
- - amkisko
5
+ Andrei Makarov (@amkisko)
6
6
 
7
7
  ## Decisions
8
8
 
@@ -2,7 +2,7 @@ CI quality failed on a trailing blank line in spec/support/junit_formatter.rb. T
2
2
 
3
3
  ## Participants
4
4
 
5
- - amkisko
5
+ Andrei Makarov (@amkisko)
6
6
 
7
7
  ## Decisions
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Participants
4
4
 
5
- Andrei Makarov
5
+ Andrei Makarov (@amkisko)
6
6
 
7
7
  ## Decisions
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Participants
4
4
 
5
- Andrei Makarov
5
+ Andrei Makarov (@amkisko)
6
6
 
7
7
  ## Decisions
8
8
 
@@ -22,12 +22,6 @@ GraphqlController logs namespace, operation name, and error_count without the qu
22
22
 
23
23
  ResourceMethods lives in lib/active_admin/graphql/resource_methods.rb.
24
24
 
25
- ## Effects
26
-
27
- First pass used create_only. Second pass replaced it after a convention check against graphql-ruby 2.6.7, Rails 8.1 nested_attributes update_only, upsert_all update_only, and ActiveAdmin 3.5.2 permit_params.
28
-
29
- bundle exec rspec spec/requests/graphql_mutation_permit_params_spec.rb: 8 examples, 0 failures after the rename.
30
-
31
25
  ## Findings
32
26
 
33
27
  Severity high. Confidence high. Location SchemaBuilder TypesInputs default create and update inputs. Why it matters: extra-field write of created_at on create, observed in spec/requests/graphql_mutation_permit_params_spec.rb. Evidence kind observed. Security disposition confirmed. Smallest credible fix: graphql { permit_params :title, :body } in the host app. Deeper fix: omit created_at and updated_at from default mutation inputs in a later RFC, matching ActiveAdmin generators/boilerplate.rb assignable_attributes.
@@ -46,6 +40,12 @@ Severity low. Confidence high. Location GraphqlController, no request correlatio
46
40
 
47
41
  Severity low. Confidence high. Location lib/active_admin/graphql/integration.rb line count 176. Why it matters: file is above the 150 line preference. Evidence kind observed. Smallest credible fix: extract ResourceMethods. Not done this pass.
48
42
 
43
+ ## Effects
44
+
45
+ First pass used create_only. Second pass replaced it after a convention check against graphql-ruby 2.6.7, Rails 8.1 nested_attributes update_only, upsert_all update_only, and ActiveAdmin 3.5.2 permit_params.
46
+
47
+ bundle exec rspec spec/requests/graphql_mutation_permit_params_spec.rb: 8 examples, 0 failures after the rename.
48
+
49
49
  ## Next
50
50
 
51
51
  Findings from the 2026-09-21 audit pass are addressed except replacing CRUD return types with graphql-ruby payload objects, which would break RFC 0004 field types. Keep RFC 0006 Proposed. 0.3.0 metadata includes the graphql 2.6.9 floor and ActiveAdmin 4 Rails 8 matrix; tag and gem push wait on make release.