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
@@ -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
 
@@ -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
 
@@ -16,28 +16,28 @@ after shipping inferred id columns and RFC 0007 identity DSL.
16
16
 
17
17
  ## Effects
18
18
 
19
- Compared gem behavior to the guide’s model, query, form, and parameter rules.
19
+ Compared gem behavior to the guide's model, query, form, and parameter rules.
20
20
 
21
21
  ActiveRecord composite primary key (self.primary_key = array):
22
22
 
23
- - Aligns: model declares composite key the same way as the guide’s
23
+ - Aligns: model declares composite key the same way as the guide's
24
24
  `self.primary_key = [:store_id, :sku]` style. Dummy `LibraryEdition` uses
25
25
  `[:book_code, :seq]`.
26
26
  - Aligns: internal batch and find paths convert that string id into an ordered
27
27
  value list and use Rails tuple/`Relation#find` style lookups
28
28
  (`ActiveAdmin::PrimaryKey.dataloader_tuple`, `ResourceQueryProxy` composite
29
- branch). That matches the guide’s `Product.find([3, "XYZ12345"])` and
29
+ branch). That matches the guide's `Product.find([3, "XYZ12345"])` and
30
30
  `where(primary_key => [[...], [...]])` idea at the Ruby API layer.
31
31
  - Diverges: GraphQL and the published docs encode composite `id` as a JSON
32
- object string (`{"book_code":"CPK","seq":7}`), not as the guide’s form/URL
32
+ object string (`{"book_code":"CPK","seq":7}`), not as the guide's form/URL
33
33
  underscore packing (`/books/2_25` with `params.extract_value(:id)`). JSON
34
34
  keeps column names explicit and avoids ambiguity when a key value contains
35
35
  `_`. Underscore packing is not accepted as GraphQL `id` today.
36
36
  - Diverges: GraphQL also accepts per-key arguments and `WhereInput` fields for
37
- composite models. The guide’s controller example extracts one `:id` param
37
+ composite models. The guide's controller example extracts one `:id` param
38
38
  into an array; we keep named keys as a GraphQL-friendly alternate.
39
39
  - Aligns with guide caution: `where(id: ...)` / `find_by(id:)` on composite
40
- models can mean an `:id` attribute, not the full primary key. The gem’s
40
+ models can mean an `:id` attribute, not the full primary key. The gem's
41
41
  GraphQL `id` field is the ActiveAdminResource identity string, not
42
42
  necessarily an ActiveRecord `:id` column. Member lookup uses primary-key
43
43
  columns (or configured identity), not a bare `where(id: graphql_id)` on
@@ -45,15 +45,15 @@ ActiveRecord composite primary key (self.primary_key = array):
45
45
 
46
46
  Keyless views and RFC 0007 `graphql { identity ... }`:
47
47
 
48
- - Outside the guide’s scope. The guide assumes a declared composite primary
48
+ - Outside the guide's scope. The guide assumes a declared composite primary
49
49
  key on the table/model. AlertEvent-style views with `primary_key = nil` and
50
50
  a virtual Ruby `id` are not Rails composite primary keys.
51
51
  - `identity :a, :b, :c, separator: ":"` is an application-level encoding so
52
52
  GraphQL can share the same string as a hand-rolled `def id; [...].join(":")`.
53
- That separator form is closer in spirit to Rails’ delimited URL id than our
53
+ That separator form is closer in spirit to Rails' delimited URL id than our
54
54
  JSON composite encoding, but the delimiter and segment rules are app-chosen,
55
- not `extract_value`’s underscore default.
56
- - Without `separator`, configured multi-column identity reuses the gem’s JSON
55
+ not `extract_value`'s underscore default.
56
+ - Without `separator`, configured multi-column identity reuses the gem's JSON
57
57
  object string so keyless and AR-composite GraphQL ids stay one family.
58
58
 
59
59
  Stock ActiveAdmin HTML member routes still need a composite-aware
@@ -64,8 +64,8 @@ make Rails form URLs use JSON.
64
64
 
65
65
  Leave GraphQL composite `id` as JSON for AR composite primary keys unless a
66
66
  later RFC deliberately adds underscore (or `extract_value`-compatible) accept
67
- and emit. Document that choice next to the Rails guide, not as “the same
68
- format Rails uses for casting composite ids” without qualification.
67
+ and emit. Document that choice next to the Rails guide, not as "the same
68
+ format Rails uses for casting composite ids" without qualification.
69
69
 
70
70
  No change required for RFC 0007 separator identity; it is the escape hatch
71
71
  when the model is not a Rails composite primary key.
@@ -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
 
@@ -23,7 +23,7 @@ as a product feature and untested in this gem.
23
23
  Searched lib, docs, rfcs, and spec for partition, shard, connects_to,
24
24
  multiple database, tenant routing, writing_role, and reading_role.
25
25
 
26
- Product code has no shard or partition API. Hits for “shard” and “partition”
26
+ Product code has no shard or partition API. Hits for "shard" and "partition"
27
27
  are Polyrun/CI test parallelism and agent IO-fault vocabulary, not databases.
28
28
 
29
29
  Dummy app uses one SQLite database (`spec/dummy/config/database.yml`). No
@@ -41,7 +41,7 @@ the Rails/ActiveAdmin request boundary (middleware, `connected_to`, custom
41
41
  finder). Do not invent gem DSL until a concrete host failure and RFC name the
42
42
  contract (shard key in GraphQL args, role switching, cross-shard batch limits).
43
43
 
44
- Optional later: document “unsupported / inherited only” next to identity in
44
+ Optional later: document "unsupported / inherited only" next to identity in
45
45
  `docs/graphql-api.md` when a reader asks again.
46
46
 
47
47
  ## Source
@@ -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
 
@@ -34,8 +34,8 @@ tested here.
34
34
  ## Next
35
35
 
36
36
  Keep federation out of scope until a host names a concrete subgraph contract
37
- and an RFC. Prefer documenting “single-schema admin API; federation
38
- unsupported” if readers keep asking.
37
+ and an RFC. Prefer documenting "single-schema admin API; federation
38
+ unsupported" if readers keep asking.
39
39
 
40
40
  ## Source
41
41
 
@@ -0,0 +1,30 @@
1
+ # Host customization gaps
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov (@amkisko)
6
+
7
+ ## Decisions
8
+
9
+ The gem owns mechanics: when a field is visible, null, or omitted; dry-run
10
+ argument wiring; filter-key policy hooks; default member-action capability;
11
+ confirm/dry_run injection on custom fields. Hosts own product grants, domain
12
+ preview bodies, and audit storage.
13
+
14
+ Capabilities remain grant names in context[:capabilities]. Named visibility
15
+ profiles stay incompatible with graphql_capabilities.
16
+
17
+ ## Effects
18
+
19
+ RFCs 0009-0013 specify the contract. Implementation landed on the feature
20
+ branch with request coverage for each hook.
21
+
22
+ ## Next
23
+
24
+ Publish a release. Hosts migrate off local prepends in the order: capability
25
+ null mode and dry-run DSL, then collection policy, then field_visible and
26
+ capability defaults, then confirm helpers.
27
+
28
+ ## Source
29
+
30
+ RFCs 0009-0013. docs/changelogs/20261012200000_host-customization-hooks.md.
@@ -0,0 +1,39 @@
1
+ # Engineering, prose, and release audit
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov (@amkisko)
6
+
7
+ ## Decisions
8
+
9
+ - Keep version 0.4.1. Fold audit fixes into the unpublished cut and date the product changelog 2026-11-03.
10
+ - Treat member and registered_resource instance-read denials as missing (null), and update/destroy instance denials as not found, so clients cannot enumerate ids from the error message.
11
+ - Wrap resolve_create hooks in a nested transaction and run instance create authorization before commit.
12
+ - Keep release confirmation EOF as rejection, require a CHANGELOG version heading, and run tests on pull requests to main.
13
+ - Leave policy run-action over-reporting, composite find_members N+1, RecordSource unscoped load, and default-open collection filter policy as follow-up work (medium severity, not cut into 0.4.1).
14
+
15
+ ## Effects
16
+
17
+ Pipeline: GraphQL HTTP ingress -> schema -> resolve -> auth/policy -> ActiveRecord -> JSON egress. Queue, worker, learned-system, and physical plant modes skipped (absent). Resource and identification: gem execute log keeps namespace, operation name, and error count only.
18
+
19
+ Ranked fixes applied:
20
+
21
+ 1. Member and registered_resource instance-read deny returns null (high, observed).
22
+ 2. Update and destroy instance deny returns not found (high, observed).
23
+ 3. resolve_create hook deny rolls back nested transaction (high, observed).
24
+ 4. Release changelog heading specs and EOF confirmation coverage (medium, observed).
25
+ 5. ASCII punctuation in README, CHANGELOG, and graphql-api guide (house prose).
26
+ 6. Docs timestamp trees: Participants handle, Source after Later pass sections.
27
+
28
+ Checks: rubocop clean on touched files; rspec 197 examples, 0 failures; gem 0.4.1 builds and verifies.
29
+
30
+ ## Next
31
+
32
+ - Commit the worktree, then run make release for tag and gem push.
33
+ - Follow-up: filter PolicyAdapter run-action lists by per-action authorized?; batch composite find_members; scope RecordSource loads; optional default collection query policy.
34
+
35
+ ## Source
36
+
37
+ docs/reviews/20261103145000_engineering-prose-release-audit.md
38
+ CHANGELOG.md section 0.4.1 (2026-11-03)
39
+ docs/reviews/20261028_engineering-audit-release-preparation.md
@@ -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
 
@@ -0,0 +1,24 @@
1
+ # Engineering audit and release preparation
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov (@amkisko)
6
+
7
+ ## Decisions
8
+
9
+ - Treat end-of-input at the release confirmation prompt as rejection.
10
+ - Run tests for pull requests targeting `main`.
11
+
12
+ ## Effects
13
+
14
+ - `make test`, RBS validation, RuboCop, and the sharded suite pass.
15
+ - The 0.4.1 gem builds and passes `Gem::Package#verify`.
16
+ - A local Rails and ActiveAdmin host cold load used 1.063 CPU seconds and 92,422,144 bytes peak RSS. These are single-machine observations, not release budgets.
17
+
18
+ ## Next
19
+
20
+ - Review the worktree, commit it, and let the release workflow create the tag and publish the gem.
21
+
22
+ ## Source
23
+
24
+ Iterative engineering audit on 2026-10-28.
@@ -0,0 +1,38 @@
1
+ # Engineering and prose audit before 0.4.1 publish
2
+
3
+ ## Participants
4
+
5
+ Andrei Makarov (@amkisko)
6
+
7
+ ## Decisions
8
+
9
+ Re-audit main for the unpublished 0.4.1 cut. Include member auth oracle shape, resolve_create transaction order, release confirmation and changelog gates, pull_request CI, and ASCII docs punctuation. Leave tag and RubyGems publish to make release after commit.
10
+
11
+ ## Effects
12
+
13
+ Pipeline: POST graphql -> GraphqlController#execute -> schema_for -> resolve -> AuthContext / ResourceQueryProxy -> ActiveRecord -> JSON. Boundary mode covered class vs instance authorization messages. Resource and identification modes: no new person identifiers in gem logs. Queue, worker, learned-system, and physical plant modes skipped (absent).
14
+
15
+ Ranked fixes applied:
16
+
17
+ 1. Null for denied member and registered_resource reads (high confidence, observed).
18
+ 2. not found for denied update and destroy (high confidence, observed).
19
+ 3. Nested transaction around resolve_create with instance authorize before commit (high confidence, observed).
20
+ 4. Changelog heading and confirmation specs; CHANGELOG dated 2026-11-03.
21
+ 5. ASCII punctuation pass on README, CHANGELOG, and docs/graphql-api.md.
22
+
23
+ Deferred (documented in the issue): PolicyAdapter run-action over-report, composite find_members N+1, RecordSource unscoped PK load, default-open collection q policy.
24
+
25
+ Checks observed in this session:
26
+
27
+ - bundle exec rubocop on touched Ruby files: 9 files, no offenses.
28
+ - bundle exec rspec: 197 examples, 0 failures (includes release_version_check and new auth examples).
29
+ - gem build activeadmin-graphql.gemspec and Gem::Package#verify for 0.4.1: ok.
30
+
31
+ ## Next
32
+
33
+ Commit, then make release.
34
+
35
+ ## Source
36
+
37
+ - docs/issues/20261103145000_engineering-prose-release-audit.md
38
+ - CHANGELOG.md section 0.4.1 (2026-11-03)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- # Block body for +capability :name do ... end+ (RFC 0008).
5
+ # Block body for +capability :name do ... end+ (RFC 0008, RFC 0009).
6
6
  class CapabilityDefinitionDSL
7
7
  QUERY_ALIASES = {
8
8
  index: :collection,
@@ -14,13 +14,18 @@ module ActiveAdmin
14
14
  delete: :destroy
15
15
  }.freeze
16
16
 
17
- def initialize(config, capability_name)
17
+ def initialize(config, capability_name, mode: nil)
18
18
  @config = config
19
19
  @capability_name = capability_name.to_sym
20
+ @mode = mode
20
21
  end
21
22
 
22
- def fields(*names)
23
- @config.assign_field_capabilities!(@capability_name, names)
23
+ def fields(*names, mode: nil)
24
+ @config.assign_field_capabilities!(
25
+ @capability_name,
26
+ names,
27
+ mode: mode.nil? ? @mode : mode
28
+ )
24
29
  end
25
30
 
26
31
  def queries(*names)
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Built-in helpers for +graphql_collection_query_policy+ (RFC 0011).
6
+ module CollectionQueryPolicy
7
+ module_function
8
+
9
+ def require_visible_fields!(
10
+ context:,
11
+ aa_resource:,
12
+ q: nil,
13
+ order: nil,
14
+ allow_q_keys: [],
15
+ field_grants: {},
16
+ infer_field_grants: true
17
+ )
18
+ allow = Array(allow_q_keys).map(&:to_s)
19
+ grants = merge_field_grants(aa_resource, field_grants, infer: infer_field_grants)
20
+
21
+ normalize_q(q).each_key do |raw_key|
22
+ key = raw_key.to_s
23
+ next if allow.include?(key)
24
+
25
+ field_name = field_from_predicate_key(key)
26
+ unless field_name
27
+ raise ::GraphQL::ExecutionError, "unsupported collection filter key #{key.inspect}"
28
+ end
29
+
30
+ assert_field_filterable!(context, aa_resource, field_name, grants)
31
+ end
32
+
33
+ assert_order_filterable!(context, aa_resource, order, grants) if order.present?
34
+ end
35
+
36
+ def merge_field_grants(aa_resource, field_grants, infer: true)
37
+ explicit = field_grants.to_h.transform_keys(&:to_s).transform_values(&:to_sym)
38
+ return explicit unless infer
39
+
40
+ inferred = aa_resource.graphql_config.field_capabilities.to_h do |name, capability|
41
+ [name.to_s, capability.to_sym]
42
+ end
43
+ inferred.merge(explicit)
44
+ end
45
+
46
+ def normalize_q(q)
47
+ return {} if q.nil?
48
+ return q.to_unsafe_h if q.respond_to?(:to_unsafe_h)
49
+
50
+ q.respond_to?(:to_h) ? q.to_h : {}
51
+ end
52
+
53
+ def field_from_predicate_key(key)
54
+ predicates = ransack_predicate_names
55
+ predicates.sort_by(&:length).reverse_each do |predicate|
56
+ suffix = "_#{predicate}"
57
+ return key[0...-suffix.length] if key.end_with?(suffix) && key.length > suffix.length
58
+ end
59
+ nil
60
+ end
61
+
62
+ def ransack_predicate_names
63
+ if defined?(Ransack) && Ransack.respond_to?(:predicates)
64
+ Ransack.predicates.keys.map(&:to_s)
65
+ else
66
+ %w[eq cont start end gt lt gteq lteq in not_in null present blank matches does_not_match]
67
+ end
68
+ end
69
+
70
+ def assert_order_filterable!(context, aa_resource, order, grants)
71
+ column = order.to_s.sub(/_(asc|desc|ascs|descs)\z/i, "")
72
+ return if column.blank?
73
+
74
+ assert_field_filterable!(context, aa_resource, column, grants)
75
+ end
76
+
77
+ def assert_field_filterable!(context, aa_resource, field_name, grants)
78
+ unless field_schema_visible?(context, aa_resource, field_name)
79
+ raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
80
+ end
81
+
82
+ capability = grants[field_name.to_s]
83
+ return if capability.nil?
84
+ return if Capability.granted?(context, capability)
85
+
86
+ raise ::GraphQL::ExecutionError, "not authorized to filter on #{field_name}"
87
+ end
88
+
89
+ def field_schema_visible?(context, aa_resource, field_name)
90
+ type_name = graphql_type_name_for(aa_resource)
91
+ type = context.schema.get_type(type_name, context)
92
+ return false unless type
93
+
94
+ !context.schema.get_field(type, field_name.to_s, context).nil?
95
+ end
96
+
97
+ def graphql_type_name_for(aa_resource)
98
+ aa_resource.graphql_config.graphql_type_name.presence ||
99
+ aa_resource.resource_class.name.delete_prefix("::").gsub("::", "__")
100
+ end
101
+ end
102
+ end
103
+ end
@@ -26,6 +26,12 @@ module ActiveAdmin
26
26
 
27
27
  def register_namespace_settings!
28
28
  ns = ActiveAdmin::NamespaceSettings
29
+ register_surface_namespace_settings!(ns)
30
+ register_capability_namespace_settings!(ns)
31
+ register_limit_namespace_settings!(ns)
32
+ end
33
+
34
+ def register_surface_namespace_settings!(ns)
29
35
  ns.register :graphql, false
30
36
  # true preserves 0.3.x: every ActiveRecord resource is in the schema once the namespace endpoint is on.
31
37
  # Set false for opt-in per resource (empty graphql block or enable!).
@@ -45,8 +51,26 @@ module ActiveAdmin
45
51
  ns.register :graphql_visibility, nil
46
52
  ns.register :graphql_visibility_profile, nil
47
53
  ns.register :graphql_schema_visible, nil
54
+ end
55
+
56
+ def register_capability_namespace_settings!(ns)
48
57
  # Optional Proc +(context_hash) -> Enumerable+ of capability names (RFC 0008).
49
58
  ns.register :graphql_capabilities, nil
59
+ # RFC 0009: default field capability mode (:omit or :null).
60
+ ns.register :graphql_field_capability_mode_default, :omit
61
+ # RFC 0009: optional Proc +(context, meta) -> Boolean+ to omit object/root fields.
62
+ ns.register :graphql_field_visible, nil
63
+ # RFC 0010: optional Proc +(context, meta) -> void+ after run-action outcomes.
64
+ ns.register :graphql_run_action_audit, nil
65
+ # RFC 0011: optional Proc +(context, meta) -> void+ at collection resolve start.
66
+ ns.register :graphql_collection_query_policy, nil
67
+ # RFC 0012: optional default capabilities for run-action mutation fields.
68
+ ns.register :graphql_member_action_capability_default, nil
69
+ ns.register :graphql_batch_action_capability_default, nil
70
+ ns.register :graphql_collection_action_capability_default, nil
71
+ end
72
+
73
+ def register_limit_namespace_settings!(ns)
50
74
  ns.register :graphql_max_complexity, 300
51
75
  ns.register :graphql_max_depth, 18
52
76
  ns.register :graphql_batch_action_max_ids, 5000
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Compatibility wrapper around {RunActionCapability} for member actions (RFC 0012).
6
+ module MemberActionCapability
7
+ module_function
8
+
9
+ def resolve(namespace, aa_res, action_name = nil)
10
+ RunActionCapability.resolve(namespace, aa_res, :member, action_name)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module GraphQL
5
+ # Preview context and resolve helpers for +member_action_mutation dry_run:+ (RFC 0010).
6
+ class MemberActionDryRun
7
+ PreviewContext = RunActionPreview
8
+
9
+ def self.resolve(
10
+ aa_res,
11
+ proxy:,
12
+ context:,
13
+ action:,
14
+ id:,
15
+ params_h:,
16
+ extras:,
17
+ per:,
18
+ dry_run:
19
+ )
20
+ new(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:).call
21
+ end
22
+
23
+ def initialize(aa_res, proxy:, context:, action:, id:, params_h:, extras:, per:, dry_run:)
24
+ @aa_res = aa_res
25
+ @proxy = proxy
26
+ @context = context
27
+ @action = action
28
+ @id = id
29
+ @params_h = params_h
30
+ @extras = extras
31
+ @per = per
32
+ @dry_run = dry_run
33
+ end
34
+
35
+ def call
36
+ record = @proxy.find_member(@id)
37
+ preview_result = run_preview(record)
38
+
39
+ if @dry_run
40
+ result = preview_result || default_dry_run_result(record)
41
+ audit!(result, dry_run: true)
42
+ return result
43
+ end
44
+
45
+ if preview_result && !preview_result.ok
46
+ audit!(preview_result, dry_run: false)
47
+ return preview_result
48
+ end
49
+
50
+ result = run_action
51
+ audit!(result, dry_run: false)
52
+ result
53
+ end
54
+
55
+ private
56
+
57
+ def run_preview(record)
58
+ return nil unless @per.preview_proc
59
+
60
+ PreviewContext.new.instance_exec(
61
+ record,
62
+ proxy: @proxy,
63
+ context: @context,
64
+ aa_resource: @aa_res,
65
+ auth: @context[:auth],
66
+ action: @action,
67
+ id: @id,
68
+ params: @params_h,
69
+ **@extras,
70
+ &@per.preview_proc
71
+ )
72
+ end
73
+
74
+ def default_dry_run_result(record)
75
+ body = if record
76
+ "would_run:#{@action}:#{record.id}"
77
+ else
78
+ "would_run:#{@action}"
79
+ end
80
+ RunActionPayload::Result.new(ok: true, status: 200, location: nil, body: body)
81
+ end
82
+
83
+ def run_action
84
+ resolve = @per.resolve_proc || @aa_res.graphql_config.member_run_action.resolve_proc
85
+ if resolve
86
+ resolve.call(
87
+ proxy: @proxy,
88
+ context: @context,
89
+ aa_resource: @aa_res,
90
+ auth: @context[:auth],
91
+ action: @action,
92
+ id: @id,
93
+ params: @params_h,
94
+ **@extras
95
+ )
96
+ else
97
+ @proxy.run_member_action(@action, @id, extra_params: @params_h)
98
+ end
99
+ end
100
+
101
+ def audit!(result, dry_run:)
102
+ hook = @context[:namespace]&.graphql_run_action_audit
103
+ return if hook.nil?
104
+
105
+ hook.call(
106
+ @context,
107
+ {
108
+ action: @action,
109
+ id: @id,
110
+ status: result.respond_to?(:status) ? result.status : nil,
111
+ dry_run: dry_run,
112
+ ok: result.respond_to?(:ok) ? result.ok : nil,
113
+ reason: result.respond_to?(:body) ? result.body : nil
114
+ }
115
+ )
116
+ rescue
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end
@@ -4,7 +4,7 @@ require_relative "capability_definition_dsl"
4
4
 
5
5
  module ActiveAdmin
6
6
  module GraphQL
7
- # +capability+ entry on {ResourceDefinitionDSL} (RFC 0008).
7
+ # +capability+ entry on {ResourceDefinitionDSL} (RFC 0008, RFC 0009).
8
8
  module ResourceCapabilityDSL
9
9
  def capability(
10
10
  name,
@@ -14,10 +14,11 @@ module ActiveAdmin
14
14
  member_actions: nil,
15
15
  batch_actions: nil,
16
16
  collection_actions: nil,
17
+ mode: nil,
17
18
  &block
18
19
  )
19
- body = CapabilityDefinitionDSL.new(@config, name)
20
- body.fields(*fields) if fields
20
+ body = CapabilityDefinitionDSL.new(@config, name, mode: mode)
21
+ body.fields(*fields, mode: mode) if fields
21
22
  body.queries(*queries) if queries
22
23
  body.mutations(*mutations) if mutations
23
24
  body.member_actions(*member_actions) if member_actions