activeadmin-graphql 0.4.0 → 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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -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 +5 -1
  20. data/docs/changelogs/20261013080000_release-0-4-0.md +1 -1
  21. data/docs/changelogs/20261103145000_release-0-4-1.md +26 -0
  22. data/docs/graphql-api.md +107 -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 +6 -2
  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/schema_builder/mutation_create.rb +20 -20
  38. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +2 -4
  39. data/lib/active_admin/graphql/schema_builder/query_type_member.rb +1 -4
  40. data/lib/active_admin/graphql/schema_builder/query_type_registered.rb +1 -4
  41. data/lib/active_admin/graphql/version.rb +1 -1
  42. metadata +5 -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,87 +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_field_capability_mode_default` — `:omit` or `:null` when a field has a
111
+ * `graphql_field_capability_mode_default` - `:omit` or `:null` when a field has a
112
112
  capability but no per-field mode (RFC 0009). Default `:omit`.
113
- * `graphql_field_visible` — optional `Proc` `(context, meta) -> Boolean` to omit
113
+ * `graphql_field_visible` - optional `Proc` `(context, meta) -> Boolean` to omit
114
114
  fields after capability checks (RFC 0009). Meta includes `graphql_type_name`,
115
115
  `field_name`, and `resource` when known.
116
- * `graphql_run_action_audit` — optional `Proc` `(context, meta) -> void` after
116
+ * `graphql_run_action_audit` - optional `Proc` `(context, meta) -> void` after
117
117
  member-action dry-run, refused preview, or completed run (RFC 0010).
118
- * `graphql_collection_query_policy` — optional `Proc` `(context, meta) -> void`
118
+ * `graphql_collection_query_policy` - optional `Proc` `(context, meta) -> void`
119
119
  at the start of collection resolve (RFC 0011). Meta includes `aa_resource`,
120
120
  `q`, `scope`, `order`, and `filter`.
121
- * `graphql_member_action_capability_default` — optional capability symbol applied
121
+ * `graphql_member_action_capability_default` - optional capability symbol applied
122
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
123
+ * `graphql_batch_action_capability_default` - same for the aggregate batch-action
124
124
  field and per-action authorize checks (RFC 0012).
125
- * `graphql_collection_action_capability_default` — same for collection-action
125
+ * `graphql_collection_action_capability_default` - same for collection-action
126
126
  mutation fields (RFC 0012).
127
- * `graphql_configure_schema` — `Proc` receiving the schema class after build
127
+ * `graphql_configure_schema` - `Proc` receiving the schema class after build
128
128
  (extensions, extra types, and so on).
129
129
 
130
130
  ### GraphQL::Dataloader
131
131
 
132
132
  The namespace schema calls `schema.use` with the configured dataloader (see
133
- `graphql_dataloader` above). [graphql-ruby’s
133
+ `graphql_dataloader` above). [graphql-ruby's
134
134
  dataloader](https://graphql-ruby.org/dataloader/overview.html) batches external
135
135
  work across sibling fields using Fibers; each HTTP request gets its own
136
136
  dataloader instance (available as `context.dataloader` inside resolvers).
@@ -152,12 +152,12 @@ Global / Relay-style IDs are not generated by Active Admin itself.
152
152
  ### Schema visibility (`GraphQL::Schema::Visibility`)
153
153
 
154
154
  When `graphql_visibility` is set, the namespace schema calls
155
- `schema.use(GraphQL::Schema::Visibility, …)` so introspection and validation
155
+ `schema.use(GraphQL::Schema::Visibility, ...)` so introspection and validation
156
156
  respect [visibility](https://graphql-ruby.org/schema/visibility.html).
157
157
 
158
158
  Set `graphql_schema_visible` to a proc that receives the GraphQL `context`
159
159
  and a `meta` hash and returns truthy to keep the member visible. The hash
160
- 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
161
161
  basename), `:resource` (`ActiveAdmin::Resource`), and other keys:
162
162
 
163
163
  | `kind` | Meaning |
@@ -165,29 +165,29 @@ basename), `:resource` (`ActiveAdmin::Resource`), and other keys:
165
165
  | `:resource_interface` | `ActiveAdminResource` interface |
166
166
  | `:resource_object` | Resource object type |
167
167
  | `:resource_enum` | Rails enum type; `meta` includes `:column` |
168
- | `: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`, ...) |
169
169
  | `:registered_resource_union` | `ActiveAdminRegisteredResource` union |
170
170
  | `:run_action_payload` | Batch/member/collection action payload type |
171
171
  | `:query_registered_resource` | `registered_resource` query field |
172
172
  | `:query_collection_field` / `:query_member_field` | Resource list/detail fields; `meta` includes `:field_name` |
173
173
  | `:query_page_field` | Custom `register_page` field; `meta` includes `:page` |
174
- | `:mutation_create`, `:mutation_update`, `:mutation_delete`, … | CRUD and action mutations |
174
+ | `:mutation_create`, `:mutation_update`, `:mutation_delete`, ... | CRUD and action mutations |
175
175
 
176
176
  Root `Query` / `Mutation` fields are defined with
177
177
  `ActiveAdmin::GraphQL::SchemaField`, which supports a `visibility:` keyword:
178
178
  a metadata `Hash` (same shape as the `meta` argument above) for
179
179
  `graphql_schema_visible`, not a graphql-ruby core option. Resource `graphql`
180
180
  `configure` blocks use that field class too, so custom fields may pass
181
- `visibility: { … }` when you want the hook to run.
181
+ `visibility: { ... }` when you want the hook to run.
182
182
 
183
183
  You can also subclass your own `GraphQL::Schema::Object` / `Field` types in
184
- `graphql_configure_schema` and implement `visible?` there; Active Admin’s
184
+ `graphql_configure_schema` and implement `visible?` there; Active Admin's
185
185
  helpers call `super` first so those implementations compose.
186
186
 
187
187
  Interfaces and unions: graphql-ruby may keep an interface or union visible
188
188
  when all members are hidden unless you return `false` from
189
189
  `graphql_schema_visible` for `:resource_interface` or `:registered_resource_union`
190
- (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
191
191
  notes for edge cases.
192
192
 
193
193
  ## Making requests
@@ -200,11 +200,11 @@ Send JSON with `query` and optional `variables`. For
200
200
  be a JSON array of such objects; the size is limited by `graphql_multiplex_max`.
201
201
 
202
202
  Requests are handled by `ActiveAdmin::GraphqlController` (an engine controller,
203
- not a generated `Admin::…` subclass).
203
+ not a generated `Admin::...` subclass).
204
204
 
205
205
  ### CSRF and cookie sessions
206
206
 
207
- `GraphqlController` uses Rails’ default CSRF protection
207
+ `GraphqlController` uses Rails' default CSRF protection
208
208
  (`protect_from_forgery with: :exception`). Clients that rely on session cookies
209
209
  (for example a typical Devise session) must submit a valid CSRF token with each
210
210
  `POST`, such as the `authenticity_token` used by the HTML admin. API-style
@@ -251,6 +251,11 @@ resource model (`authorized?` with the model class, in the same way as
251
251
  collection queries). They then run the same controller actions and blocks as the
252
252
  UI, including any `authorize!` or `authorize_resource!` calls.
253
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
+
254
259
  See the ActiveAdmin guide [Authorization adapter](https://activeadmin.info/13-authorization-adapter) for adapter setup; the same rules apply to GraphQL.
255
260
 
256
261
  Policy preflight fields (`activeadmin_policies`, `activeadmin_policies_for`, and
@@ -286,7 +291,7 @@ end
286
291
 
287
292
  Kwargs (`fields:`, `queries:`, `mutations:`, `member_actions:`,
288
293
  `batch_actions:`, `collection_actions:`) work without a block. Query aliases:
289
- `index` / `list` → collection, `show` → member. Mutation alias: `delete` →
294
+ `index` / `list` -> collection, `show` -> member. Mutation alias: `delete` ->
290
295
  `destroy`. Run-action sugar builds keys like `member_publish`.
291
296
 
292
297
  Grant capabilities per request with `graphql_capabilities`. Map OAuth scopes or
@@ -364,7 +369,7 @@ omit `created_at` and `updated_at`.
364
369
  GraphQL `id` always comes from `ActiveAdminResource` (`ID!`). An ActiveRecord
365
370
  `id` column is never added as a second field. When `self.primary_key` is blank
366
371
  but the model still has an `id` column (common for database views that project
367
- `… 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
368
373
  neither a primary key nor an `id` column, declare GraphQL identity on the
369
374
  resource (RFC 0007), set `self.primary_key` on the model, or omit the resource
370
375
  with `graphql { disable! }`. A single-column primary key named something other
@@ -384,7 +389,7 @@ end
384
389
  ```
385
390
 
386
391
  With `separator: ":"`, GraphQL `id` is `entry_type:entry_id:event_type`. Keep
387
- 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
388
393
  member ids. Without `separator`, multi-column identity uses the same JSON
389
394
  object string as composite ActiveRecord primary keys.
390
395
 
@@ -393,19 +398,19 @@ object string as composite ActiveRecord primary keys.
393
398
  For models with `self.primary_key = [:book_code, :seq]` (and no separate `id`
394
399
  column), Active Admin treats GraphQL as follows:
395
400
 
396
- * `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
397
402
  model order, for example `{"book_code":"CPK","seq":7}`. This is the GraphQL
398
403
  wire format. It is not the Rails form/URL underscore packing described in
399
404
  the Composite Primary Keys guide (`/books/2_25` with
400
405
  `params.extract_value(:id)`).
401
- * 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
402
407
  (in addition to `id`) so clients can load scalars without parsing JSON.
403
- * `LibraryEditionWhereInput` (and similar) — either optional `id` (that JSON
408
+ * `LibraryEditionWhereInput` (and similar) - either optional `id` (that JSON
404
409
  string) or every primary-key column; `id` remains required when there is
405
410
  only one key column.
406
- * 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
407
412
  GraphQL argument per primary-key column.
408
- * Create mutations — composite key attributes stay included in create
413
+ * Create mutations - composite key attributes stay included in create
409
414
  inputs (unlike single `id` tables, where the key is omitted from assignable
410
415
  attributes).
411
416
 
@@ -414,8 +419,8 @@ composite ids from `params[:id]` or from individual primary-key params. Stock
414
419
  ActiveAdmin 3.x only passes a single `params[:id]` into `find`; an ActiveAdmin
415
420
  fork (or patch) that implements composite-aware `find_resource` keeps the
416
421
  admin UI aligned with GraphQL. After decoding the GraphQL JSON `id`, the gem
417
- uses Rails’ array/tuple conventions for `Relation#find` and
418
- `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.
419
424
 
420
425
  The gem ships `ActiveAdmin::PrimaryKey` when the core constant is missing
421
426
  (upstream). A fork may define `PrimaryKey` and extended `find_resource` in
@@ -428,18 +433,18 @@ Alignment notes versus the Rails guide:
428
433
 
429
434
  These align with REST index parameters where possible:
430
435
 
431
- * `filter` — optional `PostListFilterInput` with `scope`, `order`, `q` (Ransack
432
- predicates as JSON — kept as JSON because predicate keys are open-ended), and the nested parent id when `belongs_to` applies.
433
- * `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
434
439
  compatibility.
435
- * `q` — Ransack predicates (JSON object).
436
- * `order` — index sort string (for example `id_desc`).
437
- * 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
438
443
  (for example `user_id`) stays available on the connection field as today.
439
444
 
440
445
  ### Singular query arguments
441
446
 
442
- * `where` — `PostWhereInput` with required `id` (single-key tables) or,
447
+ * `where` - `PostWhereInput` with required `id` (single-key tables) or,
443
448
  for composite keys, `id` (JSON) and/or each key column; optional parent ids for
444
449
  nested resources.
445
450
  * Legacy `id` / parent arguments behave as before (composite models also accept
@@ -449,12 +454,12 @@ These align with REST index parameters where possible:
449
454
 
450
455
  Typical names for a `Post` resource:
451
456
 
452
- * `create_post(input: PostCreateInput!)` — attribute fields match assignable
457
+ * `create_post(input: PostCreateInput!)` - attribute fields match assignable
453
458
  columns minus `created_at` / `updated_at` unless listed in `permit_params`
454
459
  (and the nested parent param when `belongs_to` is configured).
455
460
  * `update_post(where: PostWhereInput!, input: PostUpdateInput!)`
456
- * `delete_post(where: PostWhereInput!)` — kept for existing clients.
457
- * `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`.
458
463
 
459
464
  Nested resources require the parent id on `where`, `input`, or list `filter`
460
465
  when the association is required.
@@ -465,39 +470,39 @@ If you are updating clients that targeted an older embedded GraphQL integration
465
470
 
466
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.
467
472
 
468
- * 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.
469
474
 
470
- * `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.
471
476
 
472
- 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.
473
478
 
474
479
  ### `registered_resource` query
475
480
 
476
481
  `registered_resource(type_name: String!, id: ID!, path: [ActiveAdminKeyValuePair!])`
477
482
  returns `ActiveAdminRegisteredResource`, the union of all resource object types in
478
- 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" }, ...]`
479
484
  (same keys as REST path segments). Resolve with inline fragments on concrete types.
480
485
 
481
- `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.
482
487
 
483
488
  ### Batch, member, and collection actions
484
489
 
485
490
  For a route key of `posts`, fields look like:
486
491
 
487
- * `posts_batch_action(batch_action: String!, ids: [ID!]!, inputs: [ActiveAdminKeyValuePair!], …)` —
492
+ * `posts_batch_action(batch_action: String!, ids: [ID!]!, inputs: [ActiveAdminKeyValuePair!], ...)` -
488
493
  runs the registered batch action; `inputs` is converted to `batch_action_inputs` (flat key/value map)
489
494
  like the HTML form. Omitted when batch actions are disabled or none are registered.
490
- * `posts_member_action(action: String!, id: ID!, params: [ActiveAdminKeyValuePair!], …)` —
495
+ * `posts_member_action(action: String!, id: ID!, params: [ActiveAdminKeyValuePair!], ...)` -
491
496
  aggregate field: pick the `member_action` by `action` string (same as today).
492
- * One GraphQL field per registered action — `{plural}_member_{action_name}` with
497
+ * One GraphQL field per registered action - `{plural}_member_{action_name}` with
493
498
  underscores (e.g. `posts_member_publish`, `posts_member_append_title_bang`): same
494
499
  `id`, `params`, and parent ids as the aggregate field, without an `action:` argument.
495
- Use `graphql { member_action_mutation(:publish) { … } }` to set return type,
496
- `resolve`, and extra `arguments { … }` for that action only.
497
- * `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!], ...)` -
498
503
  aggregate collection actions by name.
499
504
  * Per collection action: `{plural}_collection_{action_name}` (e.g.
500
- `posts_collection_posts_count`). Configure with `collection_action_mutation(:posts_count) { … }`.
505
+ `posts_collection_posts_count`). Configure with `collection_action_mutation(:posts_count) { ... }`.
501
506
 
502
507
  Action names are validated against registered definitions; arbitrary controller
503
508
  methods are not exposed.
@@ -541,27 +546,27 @@ ActiveAdmin.register Post do
541
546
  end
542
547
  ```
543
548
 
544
- * `enable!` — include this resource when `graphql_resource_enabled_default` is
549
+ * `enable!` - include this resource when `graphql_resource_enabled_default` is
545
550
  `false`. An empty `graphql` block has the same effect.
546
- * `disable!` — resource is not included in Query or Mutation.
547
- * `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
548
553
  `registered_resource` membership for this resource.
549
- * `enable_mutations!` / `disable_mutations!` — create/update/delete mutations.
550
- * `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
551
556
  action mutations.
552
- * `enable_policies!` / `disable_policies!` — object `activeadmin_policies` and
557
+ * `enable_policies!` / `disable_policies!` - object `activeadmin_policies` and
553
558
  inclusion in the `activeadmin_policies` resources matrix.
554
- * `identity` — declare GraphQL id columns (and optional `separator`, `encode`,
559
+ * `identity` - declare GraphQL id columns (and optional `separator`, `encode`,
555
560
  `decode`) when the model has no ActiveRecord primary key and no `id` column
556
561
  (RFC 0007).
557
- * `type_name` (or `graphql_name`) — overrides the default GraphQL type name
562
+ * `type_name` (or `graphql_name`) - overrides the default GraphQL type name
558
563
  derived from the model (object types, mutations, and Rails enum GraphQL
559
564
  types use this basename).
560
- * `only` / `except` (or `exclude`) — restrict columns on object types. These
565
+ * `only` / `except` (or `exclude`) - restrict columns on object types. These
561
566
  names also cap mutation inputs. This is not ActiveAdmin `only:` / `except:`
562
567
  for controller actions (`action_item only: :show`). The primary key remains
563
568
  readable as `id`.
564
- * `permit_params` (or `permit`) — restrict create and update input keys without
569
+ * `permit_params` (or `permit`) - restrict create and update input keys without
565
570
  hiding query fields, same name as ActiveAdmin HTML `permit_params`. Nested
566
571
  `create` / `update` blocks replace that shared list for one operation and
567
572
  may set `resolve`. Names outside `only` / `except` are dropped. A static HTML
@@ -569,7 +574,7 @@ end
569
574
  unset. A request-time HTML `permit_params` block is not copied. With no list
570
575
  set, mutation inputs omit `created_at` and `updated_at`. Empty `permit_params`
571
576
  yields an input with no column arguments.
572
- * `configure` — `field` and resolver methods are evaluated on the generated
577
+ * `configure` - `field` and resolver methods are evaluated on the generated
573
578
  object type class.
574
579
 
575
580
  ### Run action return types (batch / member / collection mutations)
@@ -578,8 +583,8 @@ Return type and optional resolver for each kind live together on
578
583
  `ActiveAdmin::GraphQL::RunActionMutationConfig` (`payload_type`, `resolve_proc`),
579
584
  exposed through the DSL in a graphql-ruby-like way.
580
585
 
581
- Use a `GraphQL::Schema::Object` subclass — typically subclass
582
- `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.
583
588
  The mutation object passed to field methods is always
584
589
  `RunActionPayload::Result` (`ok`, `status`, `location`, `body`).
585
590
 
@@ -614,7 +619,7 @@ graphql do
614
619
  end
615
620
  ```
616
621
 
617
- 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
618
623
  arguments are merged with `params` for the controller and passed as keyword args
619
624
  to a custom `resolve` block. With `dry_run: true`, the field gains a Boolean
620
625
  `dry_run` argument: preview runs first, dry-run returns without
@@ -632,7 +637,7 @@ Custom mutations in `graphql_configure_schema` may use
632
637
  return without calling `resolve`; without `preview`, dry-run still calls
633
638
  `resolve` with `dry_run: true`.
634
639
 
635
- With no symbol argument, `member_action_mutation { … }` configures only the
640
+ With no symbol argument, `member_action_mutation { ... }` configures only the
636
641
  aggregate `posts_member_action` field. With a symbol, it configures the
637
642
  per-action field `posts_member_<name>`.
638
643
 
@@ -648,7 +653,7 @@ One-shot aliases (same underlying config):
648
653
  | `batch_action_run_action_payload_type(MyType)` | Same as `batch_action_mutation { type(MyType) }`. |
649
654
  | `member_action_run_action_payload_type(MyType)` | Same as `member_action_mutation { type(MyType) }`. |
650
655
  | `collection_action_run_action_payload_type(MyType)` | Same as `collection_action_mutation { type(MyType) }`. |
651
- | `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). |
652
657
 
653
658
  Per-kind `type` wins over `run_action_payload_type`; any unset kind falls back
654
659
  to `run_action_payload_type`, then `ActiveAdminRunActionPayload`. Set
@@ -667,16 +672,16 @@ replace the Ruby body that loads data or runs a mutation (similar in spirit to
667
672
  overriding `find_resource` / `find_collection` rather than inventing new field
668
673
  names):
669
674
 
670
- | DSL method | Replaces default … |
675
+ | DSL method | Replaces default ... |
671
676
  |------------|-------------------|
672
- | `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, ...). |
673
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. |
674
- | `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. |
675
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. |
676
681
  | `resolve_destroy` | After destroy authorization: delete or archive. Receives `record`, `proxy`, `context`, `auth`, `aa_resource`. Default is `record.destroy!`. |
677
- | `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`. |
678
- | `resolve_member_action` | Member-action mutation; receives `action`, `id`, `params` (string-key `Hash`), `proxy`, … |
679
- | `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`, ... |
680
685
 
681
686
  Standard authorization checks around each field or mutation still run; hooks
682
687
  are for customizing how data is fetched or saved, not for skipping policy.
@@ -688,16 +693,16 @@ exposed as GraphQL enums. Each enum type is named
688
693
 
689
694
  `{GraphQLTypeBasename}Enum{ColumnNameInPascalCase}`
690
695
 
691
- 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`
692
697
  or the model name), and `ColumnNameInPascalCase` is the database attribute in
693
- PascalCase (for example `notification_channel` → `NotificationChannel`).
698
+ PascalCase (for example `notification_channel` -> `NotificationChannel`).
694
699
 
695
700
  Examples:
696
701
 
697
- * `User` + column `notification_channel` → `UserEnumNotificationChannel`
698
- * `UserNotification` + column `channel` → `UserNotificationEnumChannel`
702
+ * `User` + column `notification_channel` -> `UserEnumNotificationChannel`
703
+ * `UserNotification` + column `channel` -> `UserNotificationEnumChannel`
699
704
 
700
- 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
701
706
  single merged word like `PostStatus`. Putting `Enum` immediately after the
702
707
  resource basename (instead of a suffix such as `PostStatusEnum`) keeps the
703
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.
@@ -4,7 +4,7 @@ Full audit: recon, security, freshness, ecosystem. Depth reached: full for hot-p
4
4
 
5
5
  ## Participants
6
6
 
7
- Andrei Makarov
7
+ Andrei Makarov (@amkisko)
8
8
 
9
9
  ## Decisions
10
10
 
@@ -20,18 +20,6 @@ Stay on ActiveAdmin 3.5.2. It is the latest stable on RubyGems. 4.0.0.beta23 (20
20
20
 
21
21
  Do not reverse the 2026-09-17 choice to keep bundle-audit off test.yml. The on-demand workflow remains the gate. Makefile still has no audit target that mirrors that workflow.
22
22
 
23
- ## Effects
24
-
25
- ruby-advisory-db update: 1245 advisories, last updated 2026-09-16 10:35:37 -0400, commit 44784c295391577f25d198a9205eae4ba73ec4da.
26
-
27
- bundle-audit check failed on all four lockfiles with GHSA-rmxg-5p3r-j6hh (graphql 2.6.7, solution >= 2.6.9). No other scanner matches.
28
-
29
- bundle outdated --strict on the root Gemfile listed 16 outdated specs. graphql 2.6.7 vs 2.6.10 is the only outdated hot-path direct runtime gem.
30
-
31
- Libyears from Gemfile.lock vs RubyGems stable (compact-graph band: about 10 low, about 100 medium): 124 locked packages, 27 outdated, total lag 17.94 years, average 0.145 years per package, 6 packages one major behind (arbre, inherited_resources, json, marcel, diff-lcs, rubocop-capybara). Coverage floor in config/polyrun_coverage.yml is 85 percent line. No upgrade spike was run this pass.
32
-
33
- No person-facing surface in this pass.
34
-
35
23
  ## Findings
36
24
 
37
25
  Tier hot path direct. Kind security. Severity high. Confidence high. Package graphql 2.6.7. Why it matters: Marshal.load of unauthenticated parser cache files is RCE when parse_file runs. Evidence kind observed (bundle-audit, GHSA text, repo grep with no parse_file). Sources: GHSA-rmxg-5p3r-j6hh, ruby-advisory-db. Smallest fix: lock 2.6.10 and gemspec >= 2.6.9. Deeper fix: none. Status: CI graphs affected; this library execute path not_affected. Disposition: upgrade. Next: apply when confirmed.
@@ -52,6 +40,18 @@ Tier dev/test. Kind freshness. Severity low. Confidence high. json 2.21.2 vs 3.0
52
40
 
53
41
  Tier automation. Kind automation gap. Severity low. Confidence high. Why it matters: Dependabot bundler directory / does not refresh gemfiles/*.gemfile.lock. scorecard.yml still pins actions/checkout 9c091bb (v7.0.0) while test.yml uses 3d3c42e5 (v7.0.1). ossf/scorecard-action 4eaacf05 is v2.4.3; v2.4.4 exists. mschilde/auto-label-merge-conflicts is commented as master. Makefile has no audit target. Evidence kind observed (workflow files, dependabot.yml). Smallest fix: optional action SHA refresh via Dependabot; keep appraisal locks in the on-demand audit. Do not put advisory scanners back on test.yml.
54
42
 
43
+ ## Effects
44
+
45
+ ruby-advisory-db update: 1245 advisories, last updated 2026-09-16 10:35:37 -0400, commit 44784c295391577f25d198a9205eae4ba73ec4da.
46
+
47
+ bundle-audit check failed on all four lockfiles with GHSA-rmxg-5p3r-j6hh (graphql 2.6.7, solution >= 2.6.9). No other scanner matches.
48
+
49
+ bundle outdated --strict on the root Gemfile listed 16 outdated specs. graphql 2.6.7 vs 2.6.10 is the only outdated hot-path direct runtime gem.
50
+
51
+ Libyears from Gemfile.lock vs RubyGems stable (compact-graph band: about 10 low, about 100 medium): 124 locked packages, 27 outdated, total lag 17.94 years, average 0.145 years per package, 6 packages one major behind (arbre, inherited_resources, json, marcel, diff-lcs, rubocop-capybara). Coverage floor in config/polyrun_coverage.yml is 85 percent line. No upgrade spike was run this pass.
52
+
53
+ No person-facing surface in this pass.
54
+
55
55
  ## Next
56
56
 
57
57
  Wait for a Rails release that includes rails/rails#58601. Then drop the development json < 3 pin and bundle update json. Leave marcel 2 and diff-lcs 2 alone until those parents move.
@@ -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