activeadmin-graphql 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CODE_OF_CONDUCT.md +14 -19
  4. data/CONTRIBUTING.md +12 -16
  5. data/README.md +19 -2
  6. data/activeadmin-graphql.gemspec +11 -7
  7. data/app/controllers/active_admin/graphql_controller.rb +50 -8
  8. data/docs/graphql-api.md +36 -11
  9. data/lib/active_admin/graphql/integration.rb +22 -23
  10. data/lib/active_admin/graphql/mutation_execution_error.rb +16 -0
  11. data/lib/active_admin/graphql/mutation_input_config.rb +11 -0
  12. data/lib/active_admin/graphql/mutation_input_definition_dsl.rb +22 -0
  13. data/lib/active_admin/graphql/policy_set_cache.rb +64 -0
  14. data/lib/active_admin/graphql/resource_config.rb +21 -2
  15. data/lib/active_admin/graphql/resource_definition_dsl.rb +19 -0
  16. data/lib/active_admin/graphql/resource_methods.rb +55 -0
  17. data/lib/active_admin/graphql/resource_query_proxy.rb +20 -0
  18. data/lib/active_admin/graphql/run_action_payload.rb +2 -1
  19. data/lib/active_admin/graphql/schema_builder/graph_params.rb +2 -2
  20. data/lib/active_admin/graphql/schema_builder/mutation_create.rb +6 -2
  21. data/lib/active_admin/graphql/schema_builder/mutation_update_destroy.rb +19 -9
  22. data/lib/active_admin/graphql/schema_builder/query_type_policies.rb +17 -5
  23. data/lib/active_admin/graphql/schema_builder/types_inputs.rb +24 -4
  24. data/lib/active_admin/graphql/schema_builder/types_object.rb +5 -2
  25. data/lib/active_admin/graphql/schema_builder/wire.rb +11 -1
  26. data/lib/active_admin/graphql/version.rb +1 -1
  27. data/lib/active_admin/graphql.rb +9 -1
  28. data/sig/active_admin/graphql.rbs +7 -0
  29. metadata +89 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15355b8548053d83cb751fc238ff2f9d4c910be902cf3aab25c2c91741a33fa9
4
- data.tar.gz: 8d76e29dcfe43079b171d19b817d52f13327c286a234688dd32c7e04efcc1522
3
+ metadata.gz: 3347160c32fcf8dea19214ff0bdac83b9c9650855dd58de9035b153c37e0b708
4
+ data.tar.gz: 757c6bcf6d659569aee20ec9a54f094795c4c0cee4700db967a294a26779ea1b
5
5
  SHA512:
6
- metadata.gz: a8a5472cfd32ac45a662733ee46fe1b0f918ea96e71db5bdf25ff9191a3c808c7b9ec047b4e4e2e0b52269a2a3aba20ff7d5cce39607bdf28448f85139beff39
7
- data.tar.gz: 873a5814296d27af34ac8a6742ada804d61e4f855c33d0b5a092793ec7260625c68c54d8831977b88e59a7d7299a5cb22f77b502ab81b504e2d523b01e505cae
6
+ metadata.gz: 383ff1b8d22b9ee6e6f70aa60284ac9c832d69436bf5ee10bfa9b25ce17089aad965481f0ca7149b3742e26aef24e0b2f1b24bb9b84ab26b1076b6c4d39e855a
7
+ data.tar.gz: 8f6b647f0201d15d2dfe4e23befad9b9c05a22b226cd9d6257678eeb36d4ef39a53de4372961e6b765fe2acc2e4bc0475f835015f24bb867a0564e58b17b4824
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.3.0 (2026-09-21)
6
+
7
+ - BREAKING: omit `created_at` and `updated_at` from default create and update inputs. List them on `permit_params` to keep them writable.
8
+ - BREAKING: require graphql-ruby 2.6.9 or newer (parser cache Marshal fix).
9
+ - Add `permit_params` on the per-resource `graphql` block, with nested `create` / `update` `permit_params` and `resolve` (RFC 0006).
10
+ - Seed GraphQL mutation inputs from a static HTML `permit_params` list when graphql `permit_params` is unset.
11
+ - Add `destroy_*` as an alias of `delete_*`.
12
+ - Add `graphql_name` as an alias of `type_name`.
13
+ - Put ActiveRecord validation messages on GraphQL error `extensions.errors` when create or update fails.
14
+ - Log GraphQL execute with namespace, operation name, and error count.
15
+ - Test ActiveAdmin 4.0.0.beta23 on Rails 8. Keep ActiveAdmin 3.5 on Rails 7.2.
16
+ - Add `rfcs/` starter pack: process (RFC 0001), positioning (RFC 0002), and Standards Track design RFCs 0003–0005 for HTTP endpoint, schema DSL, and authorization.
17
+
18
+ ## 0.2.1 (2026-07-14)
19
+
20
+ - Add RBS type signatures to the published gem for Steep and other Ruby type checkers
21
+ - Add Ruby 4.0 to the supported compatibility matrix
22
+ - Return HTTP 400 with a clear error when the GraphQL request body or variables contain invalid JSON
23
+ - Return `null` for member queries when the record id does not exist
24
+ - Enforce read authorization on `belongs_to` association fields so denied parent records resolve as `null`
25
+ - Build GraphQL schemas safely when `schema_for` runs concurrently
26
+
3
27
  ## 0.2.0 (2026-04-29)
4
28
 
5
29
  - Add `activeadmin_policies` GraphQL policy surfaces:
data/CODE_OF_CONDUCT.md CHANGED
@@ -1,31 +1,26 @@
1
1
  # Code of Conduct
2
2
 
3
- ## Our Pledge
3
+ ## Pledge
4
4
 
5
- We pledge to make participation in the activeadmin-graphql community a harassment-free experience for everyone. We operate on principles of mutual respect, privacy, and authentic engagement. We value substantive contributions and clarity on intentions.
5
+ Participation is a harassment-free experience. Treat each other with respect. Keep private context private.
6
6
 
7
- ## Our Standards
7
+ ## Expected behavior
8
8
 
9
- Examples of behavior that contributes to a positive environment include:
9
+ - Engage with genuine curiosity. Admit uncertainty.
10
+ - Own every line you submit, including generated code.
11
+ - Correct errors politely.
12
+ - Use language that welcomes people and respects identity and privacy.
10
13
 
11
- - Authenticity: Engaging with genuine curiosity and admitting uncertainty rather than feigning knowledge.
12
- - Responsible innovation: Taking full responsibility for any content or code contributed, whether manually written or generated by automation tools.
13
- - Gentle correction: Responding politely to errors. We view mistakes as opportunities for learning, provided they are addressed with humility.
14
- - Inclusive language: Using language that welcomes diverse perspectives and respects the privacy and identity of all participants.
14
+ ## Unacceptable behavior
15
15
 
16
- Examples of unacceptable behavior include:
16
+ - Harassment, trolling, or insulting comments, public or private.
17
+ - Using jargon or volume, including automated spam, to silence others.
18
+ - Sharing others' data or personal context without permission.
17
19
 
18
- - Harassment: Public or private harassment, trolling, or insulting comments.
19
- - Weaponized complexity: Using jargon or overwhelming volume (including automated spam) to silence others.
20
- - Publishing private information: Sharing others' data or personal context without explicit permission.
20
+ ## Automation
21
21
 
22
- ## Artificial Intelligence & Automation
23
-
24
- In accordance with our commitment to collective awareness:
25
-
26
- - Contributors are responsible for the accuracy and security of any AI-generated artifacts they submit.
27
- - "The AI wrote it" is not a valid excuse for introducing bugs, security vulnerabilities, or bias.
22
+ You remain responsible for bugs, security holes, and bias in generated artifacts you submit.
28
23
 
29
24
  ## Enforcement
30
25
 
31
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@kiskolabs.com. All complaints will be reviewed and investigated promptly and fairly.
26
+ Report abusive or harassing behavior to contact@kiskolabs.com. Reports are reviewed and investigated.
data/CONTRIBUTING.md CHANGED
@@ -1,27 +1,23 @@
1
1
  # Contributing Guidelines
2
2
 
3
- Thank you for your interest in contributing to activeadmin-graphql.
3
+ ## Automation authorship
4
4
 
5
- ## How to Contribute
5
+ You may use AI and automation tools. You remain the author of every line you submit: review, debug, and understand the change.
6
6
 
7
- ### 1. Reporting Issues
7
+ Keep pull requests small enough to review. Annotate non-obvious logic.
8
8
 
9
- - Verify accuracy: Before posting, verify your information. Avoid generalizations.
10
- - Use structured inputs: Provide clear goals, constraints, and reproduction steps.
9
+ Never send project secrets or private context to public models.
11
10
 
12
- ### 2. Pull Request Process
11
+ ## How to contribute
13
12
 
14
- - Scope: Keep PRs focused on a single goal.
15
- - Context: Explain why the change is necessary.
16
- - Testing: Run the RSpec suite (see the README for Appraisal and parallel test commands).
13
+ ### Reporting issues
17
14
 
18
- ### 3. Review Process
15
+ Verify the facts before filing. Use issue templates for goals, constraints, and reproduction steps.
19
16
 
20
- - We encourage productive friction. Expect questions about your approach.
21
- - If a reviewer suggests a change, view it as mutual aid, not criticism.
17
+ ### Pull requests
22
18
 
23
- ### Artificial Intelligence & Automation
19
+ Keep one pull request to one goal. Explain why the change is needed. Run smoke tests and regression checks locally before opening the request.
24
20
 
25
- 1. You are the author: You act as the responsible agent for any code you submit. You must review, debug, and understand every line.
26
- 2. Manage cognitive load: Do not submit massive, unreviewed automated dumps.
27
- 3. Security: Never feed project secrets or private context into public AI models.
21
+ ### Review
22
+
23
+ Reviewers will ask about approach and trade-offs. Treat suggested changes as part of the work.
data/README.md CHANGED
@@ -2,14 +2,18 @@
2
2
 
3
3
  GraphQL HTTP API for [ActiveAdmin](https://activeadmin.info), built with [graphql-ruby](https://graphql-ruby.org). Register resources as usual, optionally add a `graphql do ... end` block, enable the endpoint per namespace, and get a schema with queries and mutations aligned with ActiveAdmin authorization and filters.
4
4
 
5
+ Design changes go through `rfcs/` (RFC 0001).
6
+
5
7
  ## Setup
6
8
 
7
9
  ```ruby
8
10
  # Gemfile (graphql-ruby is pulled in by activeadmin-graphql)
9
- gem "activeadmin"
11
+ gem "activeadmin" # 3.5, or pin 4.0.0.beta23
10
12
  gem "activeadmin-graphql"
11
13
  ```
12
14
 
15
+ ActiveAdmin 4.0 is still a prerelease. Pin it explicitly, for example `gem "activeadmin", "4.0.0.beta23"`. Bundler will not select a prerelease from this gem's `>= 3.2` floor. graphql-ruby must be 2.6.9 or newer.
16
+
13
17
  ```ruby
14
18
  # config/initializers/active_admin.rb
15
19
  ActiveAdmin.setup do |config|
@@ -38,12 +42,25 @@ rake rubocop
38
42
  rake spec
39
43
  ```
40
44
 
41
- Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile`, `rails8ruby34.gemfile`, and `rails8truffleruby.gemfile` pin Rails 7.2 / 8.1 (integration tests follow the `spec/dummy` app, which is tested from 7.2 upward). Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context, or `bundle exec parallel_rspec spec` locally for faster multi-process runs on the current bundle.
45
+ Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile` pins Rails 7.2 with ActiveAdmin 3.5; `rails8ruby34.gemfile` and `rails8ruby4.gemfile` pin Rails 8.1 with ActiveAdmin 4.0.0.beta23 (integration tests follow the `spec/dummy` app). Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context, or `bundle exec parallel_rspec spec` locally for faster multi-process runs on the current bundle.
42
46
 
43
47
  [Trunk](https://docs.trunk.io) config lives in `.trunk/`; CI runs `trunk` via `.github/workflows/trunk.yml`. Releases: `usr/bin/release.rb` (RuboCop, Appraisal RSpec across gemfiles, `gem build` / `gem push`, git tag, `gh release`).
44
48
 
45
49
  No local ActiveAdmin checkout is required; the dummy app depends on the published `activeadmin` gem like a normal host app.
46
50
 
51
+ ## Links
52
+
53
+ - [GitHub](https://github.com/amkisko/activeadmin-graphql.rb)
54
+ - [GitLab](https://gitlab.com/amkisko/activeadmin-graphql.rb)
55
+ - [RubyGems](https://rubygems.org/gems/activeadmin-graphql)
56
+ - [Versions Atom](https://rubygems.org/gems/activeadmin-graphql/versions.atom) (feed id `10803975489574`)
57
+ - [libraries.io](https://libraries.io/rubygems/activeadmin-graphql)
58
+ - [Deps.dev](https://deps.dev/rubygems/activeadmin-graphql)
59
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_activeadmin-graphql.rb)
60
+ - [Snyk](https://snyk.io/test/github/amkisko/activeadmin-graphql.rb)
61
+ - [Codecov](https://app.codecov.io/github/amkisko/activeadmin-graphql.rb)
62
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/activeadmin-graphql.rb)
63
+
47
64
  ## License
48
65
 
49
66
  MIT — see [LICENSE.md](LICENSE.md).
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.description = "Exposes ActiveAdmin resources and pages as a graphql-ruby schema with an HTTP endpoint per namespace."
12
12
  spec.license = "MIT"
13
13
  spec.platform = Gem::Platform::RUBY
14
- spec.required_ruby_version = ">= 3.2"
14
+ spec.required_ruby_version = ">= 3.4"
15
15
 
16
16
  repository_url = "https://github.com/amkisko/activeadmin-graphql.rb"
17
17
 
@@ -36,31 +36,35 @@ Gem::Specification.new do |spec|
36
36
  ].select { |f| File.file?(f) } +
37
37
  Dir["docs/**/*.md"] +
38
38
  Dir["app/**/*.rb"] +
39
- Dir["lib/**/*.rb"]
39
+ Dir["lib/**/*.rb"] +
40
+ Dir["sig/**/*"]
40
41
  end
41
42
 
42
43
  spec.require_paths = ["lib"]
43
44
 
44
45
  spec.add_runtime_dependency "activeadmin", ">= 3.2"
45
- spec.add_runtime_dependency "graphql", ">= 2.3"
46
+ spec.add_runtime_dependency "graphql", ">= 2.6.9"
46
47
 
47
48
  spec.add_development_dependency "appraisal", "~> 2"
48
49
  spec.add_development_dependency "bigdecimal"
49
50
  spec.add_development_dependency "bundler", ">= 2"
50
51
  spec.add_development_dependency "devise", ">= 4.9"
51
- spec.add_development_dependency "parallel_tests", "~> 4.7"
52
+ spec.add_development_dependency "json", ">= 2.21.2", "< 3"
53
+ spec.add_development_dependency "parallel_tests", "~> 5.7"
52
54
  spec.add_development_dependency "rails", ">= 6.1"
53
55
  spec.add_development_dependency "rspec", "~> 3"
56
+ spec.add_development_dependency "polyrun", ">= 2.2.0"
57
+ spec.add_development_dependency "prosopite", "~> 2.0"
54
58
  spec.add_development_dependency "rspec-rails", ">= 6"
59
+ spec.add_development_dependency "rubocop", "~> 1.91"
55
60
  spec.add_development_dependency "rubocop-rails", "~> 2.34"
56
61
  spec.add_development_dependency "rubocop-rspec", "~> 3.8"
57
62
  spec.add_development_dependency "rubocop-thread_safety", "~> 0.7"
58
- spec.add_development_dependency "simplecov", "~> 0.22"
59
63
  spec.add_development_dependency "sprockets-rails", ">= 3.4"
60
- spec.add_development_dependency "sqlite3", ">= 1"
61
- spec.add_development_dependency "standard", "~> 1.52"
64
+ spec.add_development_dependency "sqlite3", ">= 2.9.6"
62
65
  spec.add_development_dependency "standard-custom", "~> 1.0"
63
66
  spec.add_development_dependency "standard-performance", "~> 1.8"
64
67
  spec.add_development_dependency "standard-rails", "~> 1.5"
65
68
  spec.add_development_dependency "standard-rspec", "~> 0.3"
69
+ spec.add_development_dependency "rbs", "~> 4"
66
70
  end
@@ -9,6 +9,10 @@ module ActiveAdmin
9
9
  class GraphqlController < ApplicationController
10
10
  protect_from_forgery with: :exception
11
11
 
12
+ rescue_from ActionDispatch::Http::Parameters::ParseError do
13
+ render json: {errors: [{message: "Invalid JSON"}]}, status: :bad_request
14
+ end
15
+
12
16
  before_action :ensure_graphql_enabled!
13
17
  before_action :authenticate_graphql!
14
18
 
@@ -104,6 +108,8 @@ module ActiveAdmin
104
108
 
105
109
  def request_body_hash
106
110
  json = request_body_json
111
+ return nil if json.nil? || json == :invalid_json
112
+
107
113
  json if json.is_a?(Hash)
108
114
  end
109
115
 
@@ -119,13 +125,21 @@ module ActiveAdmin
119
125
  body.present? ? JSON.parse(body) : nil
120
126
  end
121
127
  rescue JSON::ParserError
122
- @request_body_json = nil
128
+ @request_body_json = :invalid_json
129
+ end
130
+
131
+ def invalid_request_json?
132
+ request_body_json == :invalid_json
123
133
  end
124
134
 
125
135
  def ensure_variables(raw)
126
136
  case raw
127
137
  when String
128
- raw.present? ? JSON.parse(raw) : {}
138
+ if raw.blank?
139
+ {}
140
+ else
141
+ JSON.parse(raw)
142
+ end
129
143
  when Hash
130
144
  raw
131
145
  when ActionController::Parameters
@@ -136,25 +150,40 @@ module ActiveAdmin
136
150
  {}
137
151
  end
138
152
  rescue JSON::ParserError
139
- {}
153
+ :invalid_json
154
+ end
155
+
156
+ def render_invalid_json!
157
+ render json: {errors: [{message: "Invalid JSON"}]}, status: :bad_request
140
158
  end
141
159
 
142
160
  def render_multiplex(schema)
161
+ return render_invalid_json! if invalid_request_json?
162
+
143
163
  operations = multiplex_operations
144
164
  return render_multiplex_limit_error!(operations.size) if exceeds_multiplex_limit?(operations)
145
165
 
146
166
  payloads = build_multiplex_payloads(operations)
167
+ return if performed?
168
+
147
169
  results = schema.multiplex(payloads)
170
+ log_graphql_result(operation_name: "multiplex", error_count: results.count { |result| Array(result.to_h["errors"]).any? })
148
171
  render json: results.map(&:to_h), status: :ok
149
172
  end
150
173
 
151
174
  def render_single(schema)
175
+ return render_invalid_json! if invalid_request_json?
176
+
177
+ variables = ensure_variables(variables_hash)
178
+ return render_invalid_json! if variables == :invalid_json
179
+
152
180
  result = schema.execute(
153
181
  query: query_string,
154
- variables: ensure_variables(variables_hash),
182
+ variables: variables,
155
183
  operation_name: operation_name,
156
184
  context: graphql_context
157
185
  )
186
+ log_graphql_result(operation_name: operation_name, error_count: Array(result.to_h["errors"]).size)
158
187
  render json: result.to_h, status: :ok
159
188
  end
160
189
 
@@ -174,14 +203,27 @@ module ActiveAdmin
174
203
 
175
204
  def build_multiplex_payloads(operations)
176
205
  context = graphql_context
177
- operations.map do |op|
206
+ operations.map do |operation|
207
+ variables = ensure_variables(operation[:variables])
208
+ if variables == :invalid_json
209
+ render_invalid_json!
210
+ return []
211
+ end
212
+
178
213
  {
179
- query: op[:query],
180
- variables: ensure_variables(op[:variables]),
181
- operation_name: op[:operation_name],
214
+ query: operation[:query],
215
+ variables: variables,
216
+ operation_name: operation[:operation_name],
182
217
  context: context
183
218
  }
184
219
  end
185
220
  end
221
+
222
+ def log_graphql_result(operation_name:, error_count:)
223
+ Rails.logger.info(
224
+ "[activeadmin-graphql] namespace=#{active_admin_namespace.name} " \
225
+ "operation=#{operation_name.presence || "anonymous"} error_count=#{error_count}"
226
+ )
227
+ end
186
228
  end
187
229
  end
data/docs/graphql-api.md CHANGED
@@ -10,10 +10,12 @@ Add the extension to your application (it pulls in `activeadmin` and `graphql` a
10
10
 
11
11
  ```ruby
12
12
  # Gemfile
13
- gem "activeadmin"
13
+ gem "activeadmin" # 3.5, or pin 4.0.0.beta23
14
14
  gem "activeadmin-graphql"
15
15
  ```
16
16
 
17
+ ActiveAdmin 4.0 is still a prerelease. Pin it in the host Gemfile (`gem "activeadmin", "4.0.0.beta23"`). Bundler will not pick a prerelease from this gem's `>= 3.2` floor. graphql-ruby must be 2.6.9 or newer.
18
+
17
19
  Bundling loads `activeadmin/graphql`, which installs the Router/Resource integration and calls `ActiveAdmin::GraphQL.load!`, so graphql-ruby is loaded at boot whenever the gem is in your bundle. The schema class for a namespace is built on demand (first request or introspection) via `ActiveAdmin::GraphQL.schema_for(namespace)`.
18
20
 
19
21
  If `graphql = true` is set for a namespace but Bundler cannot satisfy the `graphql` gem, boot fails with an ActiveAdmin dependency error until you run `bundle install`.
@@ -214,9 +216,12 @@ The `Mutation` type is present when at least one operation exists: per-resource
214
216
  resource defines the corresponding batch, member, or collection actions.
215
217
 
216
218
  Object type names default to the model name (for example `Post`). Typed input
217
- objects mirror `attributes_for_graphql` (see the per-resource section below):
219
+ objects use assignable columns (see the per-resource section below):
218
220
  `PostCreateInput`, `PostUpdateInput`, `PostListFilterInput`, and `PostWhereInput`
219
- when the GraphQL name is `Post`.
221
+ when the GraphQL name is `Post`. `only` / `except` cap both object fields and
222
+ mutation keys. `permit_params` and nested `create` / `update` blocks narrow
223
+ create and update inputs without hiding query fields. Default mutation inputs
224
+ omit `created_at` and `updated_at`.
220
225
 
221
226
  ### Composite primary keys (Rails 7.1+)
222
227
 
@@ -270,9 +275,11 @@ These align with REST index parameters where possible:
270
275
  Typical names for a `Post` resource:
271
276
 
272
277
  * `create_post(input: PostCreateInput!)` — attribute fields match assignable
273
- columns (and the nested parent param when `belongs_to` is configured).
278
+ columns minus `created_at` / `updated_at` unless listed in `permit_params`
279
+ (and the nested parent param when `belongs_to` is configured).
274
280
  * `update_post(where: PostWhereInput!, input: PostUpdateInput!)`
275
- * `delete_post(where: PostWhereInput!)`
281
+ * `delete_post(where: PostWhereInput!)` — kept for existing clients.
282
+ * `destroy_post(where: PostWhereInput!)` — Rails / ActiveAdmin action name; same resolver as `delete_post`.
276
283
 
277
284
  Nested resources require the parent id on `where`, `input`, or list `filter`
278
285
  when the association is required.
@@ -339,9 +346,16 @@ Inside `ActiveAdmin.register` you can narrow the GraphQL surface:
339
346
  ActiveAdmin.register Post do
340
347
  graphql do
341
348
  disable! # omit this resource from the schema
342
- type_name "BlogPost" # GraphQL object / mutation type basename
343
- only :title, :body, :published_at # expose only these attributes
349
+ type_name "BlogPost" # GraphQL object / mutation type basename (`graphql_name` alias)
350
+ only :title, :body, :published_at # expose only these attributes on queries
344
351
  exclude :internal_score # or `except` / `exclude`
352
+ permit_params :title, :body # create and update inputs (query fields stay)
353
+ create do
354
+ permit_params :title, :body # replace the shared list for create
355
+ end
356
+ update do
357
+ permit_params :title # replace the shared list for update
358
+ end
345
359
  configure do
346
360
  # graphql-ruby field DSL on the object type class
347
361
  field :computed, GraphQL::Types::String, null: true
@@ -352,10 +366,21 @@ end
352
366
  ```
353
367
 
354
368
  * `disable!` — resource is not included in Query or Mutation.
355
- * `type_name` — overrides the default GraphQL type name derived from the model
356
- (object types, mutations, and Rails enum GraphQL types use this basename).
357
- * `only` / `except` (or `exclude`) — restrict columns on object types and
358
- assignable mutation keys (the primary key remains readable as `id`).
369
+ * `type_name` (or `graphql_name`) — overrides the default GraphQL type name
370
+ derived from the model (object types, mutations, and Rails enum GraphQL
371
+ types use this basename).
372
+ * `only` / `except` (or `exclude`) restrict columns on object types. These
373
+ names also cap mutation inputs. This is not ActiveAdmin `only:` / `except:`
374
+ for controller actions (`action_item only: :show`). The primary key remains
375
+ readable as `id`.
376
+ * `permit_params` (or `permit`) — restrict create and update input keys without
377
+ hiding query fields, same name as ActiveAdmin HTML `permit_params`. Nested
378
+ `create` / `update` blocks replace that shared list for one operation and
379
+ may set `resolve`. Names outside `only` / `except` are dropped. A static HTML
380
+ `permit_params :title, :body` list seeds GraphQL when the graphql list is
381
+ unset. A request-time HTML `permit_params` block is not copied. With no list
382
+ set, mutation inputs omit `created_at` and `updated_at`. Empty `permit_params`
383
+ yields an input with no column arguments.
359
384
  * `configure` — `field` and resolver methods are evaluated on the generated
360
385
  object type class.
361
386
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "resource_definition_dsl"
4
+ require_relative "resource_methods"
4
5
 
5
6
  module ActiveAdmin
6
7
  module GraphQL
@@ -60,36 +61,34 @@ module ActiveAdmin
60
61
  end
61
62
  end
62
63
 
63
- module ResourceMethods
64
- def graphql_config
65
- @graphql_config ||= ActiveAdmin::GraphQL::ResourceConfig.new
64
+ module ResourceDSLMethods
65
+ def graphql(&block)
66
+ if block
67
+ ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
68
+ end
69
+ config.graphql_config
66
70
  end
67
71
 
68
- def attributes_for_graphql
69
- keys = resource_attributes.keys
70
- cfg = graphql_config
71
- if cfg.only_attributes
72
- keys &= cfg.only_attributes
72
+ def permit_params(*args, &block)
73
+ unless block
74
+ config.graphql_config.html_permit_params_attributes = flatten_html_permit_params(args)
73
75
  end
74
- keys -= cfg.exclude_attributes
75
- keys
76
+ super
76
77
  end
77
78
 
78
- def graphql_assignable_attribute_names
79
- names = attributes_for_graphql.map(&:to_s)
80
- pk_cols = ActiveAdmin::PrimaryKey.columns(resource_class)
81
- return names if pk_cols.size > 1
82
-
83
- names - pk_cols
84
- end
85
- end
79
+ private
86
80
 
87
- module ResourceDSLMethods
88
- def graphql(&block)
89
- if block
90
- ActiveAdmin::GraphQL::ResourceDefinitionDSL.new(config.graphql_config).instance_exec(&block)
81
+ def flatten_html_permit_params(args)
82
+ args.flat_map do |arg|
83
+ case arg
84
+ when Symbol, String
85
+ [arg.to_sym]
86
+ when Hash
87
+ arg.keys.map(&:to_sym)
88
+ else
89
+ []
90
+ end
91
91
  end
92
- config.graphql_config
93
92
  end
94
93
  end
95
94
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ module MutationExecutionError
6
+ module_function
7
+
8
+ def validation(record)
9
+ ::GraphQL::ExecutionError.new(
10
+ record.errors.full_messages.to_sentence,
11
+ extensions: {"errors" => record.errors.full_messages}
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Create or update input options set from +graphql { create do … }+ / +update do … }+.
6
+ class MutationInputConfig
7
+ attr_accessor :permit_params_attributes
8
+ attr_accessor :resolve_proc
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Nested DSL for +graphql { create do … }+ and +update do … }+, pairing
6
+ # ActiveAdmin +permit_params+ with graphql-ruby-style +resolve+.
7
+ class MutationInputDefinitionDSL
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def permit_params(*attrs)
13
+ @config.permit_params_attributes = attrs.flatten.map(&:to_sym)
14
+ end
15
+ alias_method :permit, :permit_params
16
+
17
+ def resolve(&block)
18
+ @config.resolve_proc = block
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Request-scoped memoization for ActiveAdmin policy sets on GraphQL objects.
6
+ class PolicySetCache
7
+ class << self
8
+ def fetch(context, subject_owner:, subject:)
9
+ namespace = context[:namespace]
10
+ unless namespace
11
+ return policy_builder(context).send(
12
+ :build_policy_set,
13
+ auth: context[:auth],
14
+ subject_owner: subject_owner,
15
+ subject: subject,
16
+ context: context
17
+ )
18
+ end
19
+
20
+ cache = context[:policy_set_cache] ||= {}
21
+ key = cache_key(subject_owner, subject)
22
+ cache[key] ||= policy_builder(context).send(
23
+ :build_policy_set,
24
+ auth: context[:auth],
25
+ subject_owner: subject_owner,
26
+ subject: subject,
27
+ context: context
28
+ )
29
+ end
30
+
31
+ def policy_builder(context)
32
+ context[:policy_schema_builder] ||= SchemaBuilder.new(context[:namespace])
33
+ end
34
+
35
+ def cache_key(subject_owner, subject)
36
+ owner_key = subject_owner_key(subject_owner)
37
+ "#{owner_key}:#{subject_key(subject)}"
38
+ end
39
+
40
+ private
41
+
42
+ def subject_owner_key(subject_owner)
43
+ if subject_owner.is_a?(ActiveAdmin::Page)
44
+ "page-owner:#{subject_owner.name}"
45
+ else
46
+ "resource-owner:#{subject_owner.resource_class.name}"
47
+ end
48
+ end
49
+
50
+ def subject_key(subject)
51
+ if subject.is_a?(Class)
52
+ "class:#{subject.name}"
53
+ elsif subject.is_a?(ActiveAdmin::Page)
54
+ "page:#{subject.name}"
55
+ elsif subject.is_a?(ActiveRecord::Base)
56
+ "record:#{subject.class.name}:#{PrimaryKey.graphql_id_value(subject)}"
57
+ else
58
+ "object:#{subject.object_id}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end