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,22 +4,29 @@ module ActiveAdmin
4
4
  module GraphQL
5
5
  class SchemaBuilder
6
6
  module Resolvers
7
- def mutation_extra_keyword_params(aa_res, kw)
7
+ def mutation_extra_keyword_params(aa_res, kw, allowlist: nil)
8
8
  skip =
9
9
  if (btc = aa_res.belongs_to_config)
10
10
  [btc.to_param.to_s]
11
11
  else
12
12
  []
13
13
  end
14
+ skip << "id"
15
+ skip << "dry_run"
16
+ allow = allowlist&.map(&:to_s)
17
+
14
18
  kw.each_with_object({}) do |(key, val), h|
15
19
  ks = key.to_s
16
20
  next if skip.include?(ks)
21
+ next if allow && !allow.include?(ks)
17
22
 
18
23
  h[ks] = val
19
24
  end
20
25
  end
21
26
 
22
27
  def graphql_resolve_index(aa_res, proxy:, context:, graph_params:, **field_kwargs)
28
+ invoke_collection_query_policy!(aa_res, context, field_kwargs)
29
+
23
30
  proc_ = aa_res.graphql_config.resolve_index_proc
24
31
  base_kwargs = {graph_params: graph_params}.merge(field_kwargs)
25
32
  if proc_
@@ -57,9 +64,26 @@ module ActiveAdmin
57
64
  end
58
65
 
59
66
  def graphql_resolve_member_action(aa_res, proxy:, context:, action:, id:, params: nil, **kw)
60
- extras = mutation_extra_keyword_params(aa_res, kw)
61
- params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
62
67
  per = aa_res.graphql_config.member_action_mutations[action.to_s]
68
+ dry_run = !!kw[:dry_run]
69
+ extras = mutation_extra_keyword_params(aa_res, kw, allowlist: per&.extra_param_keys)
70
+ params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
71
+ params_h.delete("id")
72
+
73
+ if per&.dry_run
74
+ return MemberActionDryRun.resolve(
75
+ aa_res,
76
+ proxy: proxy,
77
+ context: context,
78
+ action: action,
79
+ id: id,
80
+ params_h: params_h,
81
+ extras: extras.transform_keys(&:to_sym),
82
+ per: per,
83
+ dry_run: dry_run
84
+ )
85
+ end
86
+
63
87
  resolve = per&.resolve_proc || aa_res.graphql_config.member_run_action.resolve_proc
64
88
  if resolve
65
89
  resolve.call(
@@ -70,7 +94,7 @@ module ActiveAdmin
70
94
  action: action,
71
95
  id: id,
72
96
  params: params_h,
73
- **extras
97
+ **extras.transform_keys(&:to_sym)
74
98
  )
75
99
  else
76
100
  proxy.run_member_action(action, id, extra_params: params_h)
@@ -78,9 +102,10 @@ module ActiveAdmin
78
102
  end
79
103
 
80
104
  def graphql_resolve_collection_action(aa_res, proxy:, context:, action:, params: nil, **kw)
81
- extras = mutation_extra_keyword_params(aa_res, kw)
82
- params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
83
105
  per = aa_res.graphql_config.collection_action_mutations[action.to_s]
106
+ extras = mutation_extra_keyword_params(aa_res, kw, allowlist: per&.extra_param_keys)
107
+ params_h = coerce_action_param_map(params).merge(extras.transform_keys(&:to_s))
108
+ params_h.delete("id")
84
109
  resolve = per&.resolve_proc || aa_res.graphql_config.collection_run_action.resolve_proc
85
110
  if resolve
86
111
  resolve.call(
@@ -90,7 +115,7 @@ module ActiveAdmin
90
115
  auth: context[:auth],
91
116
  action: action,
92
117
  params: params_h,
93
- **extras
118
+ **extras.transform_keys(&:to_sym)
94
119
  )
95
120
  else
96
121
  proxy.run_collection_action(action, extra_params: params_h)
@@ -105,6 +130,20 @@ module ActiveAdmin
105
130
  h.stringify_keys
106
131
  end
107
132
 
133
+ def invoke_collection_query_policy!(aa_res, context, field_kwargs)
134
+ hook = context[:namespace]&.graphql_collection_query_policy
135
+ return if hook.nil?
136
+
137
+ meta = field_kwargs.merge(
138
+ aa_resource: aa_res,
139
+ q: field_kwargs[:q],
140
+ scope: field_kwargs[:scope],
141
+ order: field_kwargs[:order],
142
+ filter: field_kwargs[:filter]
143
+ )
144
+ hook.call(context, meta)
145
+ end
146
+
108
147
  public :graphql_resolve_index,
109
148
  :graphql_resolve_show,
110
149
  :graphql_resolve_batch_action,
@@ -119,7 +119,8 @@ module ActiveAdmin
119
119
  null: true,
120
120
  camelize: false,
121
121
  authorize: false,
122
- capability: aa_res.graphql_config.capability_for_field(name)
122
+ capability: aa_res.graphql_config.capability_for_field(name),
123
+ capability_mode: aa_res.graphql_config.capability_mode_for_field(name)
123
124
  )
124
125
 
125
126
  type_class.define_method(name.to_sym) { object.public_send(name) }
@@ -22,7 +22,8 @@ module ActiveAdmin
22
22
 
23
23
  type_class.field field_name, @object_types[target], null: true, camelize: false,
24
24
  authorize: false,
25
- capability: aa_res.graphql_config.capability_for_field(field_name)
25
+ capability: aa_res.graphql_config.capability_for_field(field_name),
26
+ capability_mode: aa_res.graphql_config.capability_mode_for_field(field_name)
26
27
 
27
28
  type_class.define_method(field_name) do
28
29
  fk_val = object.public_send(fk)
@@ -2,17 +2,36 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- # Subclass of {::GraphQL::Schema::Field} for namespace +Query+ / +Mutation+ fields.
5
+ # Subclass of {::GraphQL::Schema::Field} for namespace +Query+ / +Mutation+ fields
6
+ # and resource object fields.
6
7
  #
7
8
  # The +visibility:+ keyword accepts an optional Hash of metadata forwarded to
8
9
  # +graphql_schema_visible+ (second argument) when {GraphQL::Schema::Visibility} is enabled.
9
- # That hook runs from {#visible?}; it does not replace graphql-ruby's visibility system—it
10
- # composes with +super+ like any custom +Field+ class.
11
10
  #
12
- # +capability:+ (RFC 0008) gates +visible?+ and +authorized?+ via +context[:capabilities]+.
11
+ # +capability:+ (RFC 0008) and +capability_mode:+ (RFC 0009) gate +visible?+ /
12
+ # +authorized?+ via +context[:capabilities]+. +graphql_field_visible+ may omit fields.
13
+ # +dry_run:+ / +confirm:+ / +preview:+ (RFC 0013) control custom run-action fields.
13
14
  class SchemaField < ::GraphQL::Schema::Field
14
- def initialize(visibility: nil, authorize: nil, authorize_action: nil, capability: nil, **kwargs, &block)
15
+ CAPABILITY_MODES = %i[omit null].freeze
16
+
17
+ def initialize(
18
+ visibility: nil,
19
+ authorize: nil,
20
+ authorize_action: nil,
21
+ capability: nil,
22
+ capability_mode: nil,
23
+ dry_run: false,
24
+ confirm: false,
25
+ preview: nil,
26
+ extensions: ::GraphQL::EmptyObjects::EMPTY_ARRAY,
27
+ **kwargs,
28
+ &block
29
+ )
15
30
  @capability = capability&.to_sym
31
+ @capability_mode = normalize_capability_mode(capability_mode)
32
+ @inject_dry_run = dry_run
33
+ @inject_confirm = confirm
34
+ @preview_proc = preview
16
35
  @visibility =
17
36
  if @capability && visibility.is_a?(Hash) && !visibility.key?(:capability)
18
37
  visibility.merge(capability: @capability)
@@ -21,16 +40,30 @@ module ActiveAdmin
21
40
  end
22
41
  @authorize = authorize
23
42
  @authorize_action = authorize_action
24
- super(**kwargs, &block)
43
+ field_extensions = Array(extensions)
44
+ if @inject_confirm || @preview_proc
45
+ field_extensions += [{
46
+ RunActionControlExtension => {confirm: @inject_confirm, preview: @preview_proc}
47
+ }]
48
+ end
49
+ if @inject_dry_run || @inject_confirm || @preview_proc || block
50
+ super(extensions: field_extensions, **kwargs) do
51
+ inject_run_action_control_arguments!
52
+ instance_exec(&block) if block
53
+ end
54
+ else
55
+ super(extensions: field_extensions, **kwargs)
56
+ end
25
57
  end
26
58
 
27
59
  def visible?(ctx)
28
60
  return false unless super
61
+ return false unless field_visible_by_hook?(ctx)
29
62
 
30
- # Gate introspection only when Schema::Visibility is enabled; otherwise
31
- # capability is enforced in +authorized?+ so hosts without the plugin
32
- # do not trip graphql-ruby's legacy Warden +visible?+ path.
33
- if ctx[:namespace]&.graphql_visibility && !Capability.granted?(ctx, @capability)
63
+ if ctx[:namespace]&.graphql_visibility &&
64
+ @capability &&
65
+ !Capability.granted?(ctx, @capability) &&
66
+ capability_mode_for(ctx) == :omit
34
67
  return false
35
68
  end
36
69
 
@@ -60,6 +93,48 @@ module ActiveAdmin
60
93
  action = @authorize_action || ActiveAdmin::Authorization::READ
61
94
  !!ctx[:auth]&.authorized?(aa_resource, action, object)
62
95
  end
96
+
97
+ private
98
+
99
+ def inject_run_action_control_arguments!
100
+ if @inject_dry_run
101
+ argument :dry_run, ::GraphQL::Types::Boolean, required: false, default_value: false, camelize: false,
102
+ description: "When true, preview without performing the side-effecting action."
103
+ end
104
+ if @inject_confirm
105
+ argument :confirm, ::GraphQL::Types::Boolean, required: false, default_value: false, camelize: false,
106
+ description: "When false and dry_run is false, refuse with body confirm_required."
107
+ end
108
+ end
109
+
110
+ def normalize_capability_mode(mode)
111
+ return nil if mode.nil?
112
+
113
+ sym = mode.to_sym
114
+ unless CAPABILITY_MODES.include?(sym)
115
+ raise ArgumentError, "capability_mode must be :omit or :null, got #{mode.inspect}"
116
+ end
117
+ sym
118
+ end
119
+
120
+ def capability_mode_for(ctx)
121
+ mode = @capability_mode || ctx[:namespace]&.graphql_field_capability_mode_default || :omit
122
+ mode.to_sym
123
+ end
124
+
125
+ def field_visible_by_hook?(ctx)
126
+ hook = ctx[:namespace]&.graphql_field_visible
127
+ return true if hook.nil?
128
+
129
+ owner = self.owner
130
+ meta = {
131
+ graphql_type_name: owner.respond_to?(:graphql_name) ? owner.graphql_name : nil,
132
+ field_name: graphql_name,
133
+ resource: owner.respond_to?(:activeadmin_graphql_resource) ? owner.activeadmin_graphql_resource : nil
134
+ }
135
+ meta = meta.merge(@visibility) if @visibility.is_a?(Hash)
136
+ !!hook.call(ctx, meta)
137
+ end
63
138
  end
64
139
  end
65
140
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module GraphQL
5
- VERSION = "0.3.3"
5
+ VERSION = "0.4.1"
6
6
  end
7
7
  end
@@ -69,6 +69,8 @@ module ActiveAdmin
69
69
  %w[
70
70
  graphql/resource_interface
71
71
  graphql/capability
72
+ graphql/run_action_preview
73
+ graphql/run_action_control_extension
72
74
  graphql/schema_field
73
75
  graphql/auth_context
74
76
  graphql/record_source
@@ -77,6 +79,11 @@ module ActiveAdmin
77
79
  graphql/run_action_payload
78
80
  graphql/run_action_mutation_config
79
81
  graphql/run_action_mutation_dsl
82
+ graphql/run_action_field
83
+ graphql/member_action_dry_run
84
+ graphql/run_action_capability
85
+ graphql/member_action_capability
86
+ graphql/collection_query_policy
80
87
  graphql/key_value_pair_input
81
88
  graphql/policy_adapter
82
89
  graphql/policy_set_cache
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeadmin-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -382,6 +382,9 @@ files:
382
382
  - docs/changelogs/20261012050051_graphql-surface-toggles.md
383
383
  - docs/changelogs/20261012064500_graphql-capabilities.md
384
384
  - docs/changelogs/20261012113500_release-0-3-3.md
385
+ - docs/changelogs/20261012200000_host-customization-hooks.md
386
+ - docs/changelogs/20261013080000_release-0-4-0.md
387
+ - docs/changelogs/20261103145000_release-0-4-1.md
385
388
  - docs/dependencies/20260907141000_json-cve-2026-71847.md
386
389
  - docs/dependencies/20260907141000_sqlite3-ghsa-mwm8-39rw-8826.md
387
390
  - docs/dependencies/20260921114000_rails-json-parse-kwargs.md
@@ -397,14 +400,21 @@ files:
397
400
  - docs/issues/20261010111159_rails-composite-primary-keys-alignment.md
398
401
  - docs/issues/20261010111300_partitions-and-shards-support.md
399
402
  - docs/issues/20261010111500_graphql-federation-support.md
403
+ - docs/issues/20261012200000_host-customization-gaps.md
404
+ - docs/issues/20261103145000_engineering-prose-release-audit.md
400
405
  - docs/plan/20260817201500_rfc-starter.md
406
+ - docs/reviews/20261028_engineering-audit-release-preparation.md
407
+ - docs/reviews/20261103145000_engineering-prose-release-audit.md
401
408
  - lib/active_admin/graphql.rb
402
409
  - lib/active_admin/graphql/auth_context.rb
403
410
  - lib/active_admin/graphql/capability.rb
404
411
  - lib/active_admin/graphql/capability_definition_dsl.rb
412
+ - lib/active_admin/graphql/collection_query_policy.rb
405
413
  - lib/active_admin/graphql/engine.rb
406
414
  - lib/active_admin/graphql/integration.rb
407
415
  - lib/active_admin/graphql/key_value_pair_input.rb
416
+ - lib/active_admin/graphql/member_action_capability.rb
417
+ - lib/active_admin/graphql/member_action_dry_run.rb
408
418
  - lib/active_admin/graphql/mutation_execution_error.rb
409
419
  - lib/active_admin/graphql/mutation_input_config.rb
410
420
  - lib/active_admin/graphql/mutation_input_definition_dsl.rb
@@ -423,9 +433,13 @@ files:
423
433
  - lib/active_admin/graphql/resource_query_proxy/controller.rb
424
434
  - lib/active_admin/graphql/resource_query_proxy/identity.rb
425
435
  - lib/active_admin/graphql/resource_surface_dsl.rb
436
+ - lib/active_admin/graphql/run_action_capability.rb
437
+ - lib/active_admin/graphql/run_action_control_extension.rb
438
+ - lib/active_admin/graphql/run_action_field.rb
426
439
  - lib/active_admin/graphql/run_action_mutation_config.rb
427
440
  - lib/active_admin/graphql/run_action_mutation_dsl.rb
428
441
  - lib/active_admin/graphql/run_action_payload.rb
442
+ - lib/active_admin/graphql/run_action_preview.rb
429
443
  - lib/active_admin/graphql/schema_builder.rb
430
444
  - lib/active_admin/graphql/schema_builder/build.rb
431
445
  - lib/active_admin/graphql/schema_builder/graph_params.rb